@wecom/aibot-node-sdk
Advanced tools
| /** | ||
| * WeCom 加解密通用核心 | ||
| * 独立于 Webhook、WebSocket、Agent 的具体协议形态,统一提供基于 AES-256-CBC | ||
| * 的加解密与 SHA1 签名计算能力。 | ||
| */ | ||
| /** | ||
| * 解码企业微信提供的 Base64 encodingAESKey | ||
| */ | ||
| export declare function decodeEncodingAESKey(encodingAESKey: string): Buffer; | ||
| /** | ||
| * PKCS#7 填充 | ||
| */ | ||
| export declare function pkcs7Pad(buf: Buffer, blockSize: number): Buffer; | ||
| /** | ||
| * PKCS#7 解除填充 | ||
| */ | ||
| export declare function pkcs7Unpad(buf: Buffer, blockSize: number): Buffer; | ||
| export declare class WecomCrypto { | ||
| private token; | ||
| private encodingAESKey; | ||
| private receiveId?; | ||
| private aesKey; | ||
| private iv; | ||
| constructor(token: string, encodingAESKey: string, receiveId?: string | undefined); | ||
| /** | ||
| * 计算 WeCom 消息签名 | ||
| */ | ||
| computeSignature(timestamp: string, nonce: string, encrypt: string): string; | ||
| /** | ||
| * 验证 WeCom 消息签名 | ||
| */ | ||
| verifySignature(signature: string, timestamp: string, nonce: string, encrypt: string): boolean; | ||
| /** | ||
| * 消息解密 | ||
| * 返回纯文本字符串(XML 或 JSON 根据上层业务而定) | ||
| */ | ||
| decrypt(encryptText: string): string; | ||
| /** | ||
| * 消息加密 | ||
| * 加密明文并返回 base64 格式密文与对应的新签名 | ||
| */ | ||
| encrypt(plainText: string, timestamp: string, nonce: string): { | ||
| encrypt: string; | ||
| signature: string; | ||
| }; | ||
| } |
+145
-0
@@ -9,2 +9,3 @@ 'use strict'; | ||
| var WebSocket = require('ws'); | ||
| var crypto$1 = require('node:crypto'); | ||
@@ -1402,2 +1403,142 @@ /** | ||
| /** | ||
| * WeCom 加解密通用核心 | ||
| * 独立于 Webhook、WebSocket、Agent 的具体协议形态,统一提供基于 AES-256-CBC | ||
| * 的加解密与 SHA1 签名计算能力。 | ||
| */ | ||
| const CRYPTO_CONSTANTS = { | ||
| /** PKCS#7 块大小 */ | ||
| PKCS7_BLOCK_SIZE: 32, | ||
| /** AES Key 长度 */ | ||
| AES_KEY_LENGTH: 32, | ||
| }; | ||
| /** | ||
| * 解码企业微信提供的 Base64 encodingAESKey | ||
| */ | ||
| function decodeEncodingAESKey(encodingAESKey) { | ||
| const trimmed = encodingAESKey.trim(); | ||
| if (!trimmed) | ||
| throw new Error("encodingAESKey missing"); | ||
| const withPadding = trimmed.endsWith("=") ? trimmed : `${trimmed}=`; | ||
| const key = Buffer.from(withPadding, "base64"); | ||
| if (key.length !== CRYPTO_CONSTANTS.AES_KEY_LENGTH) { | ||
| throw new Error(`invalid encodingAESKey (expected ${CRYPTO_CONSTANTS.AES_KEY_LENGTH} bytes, got ${key.length})`); | ||
| } | ||
| return key; | ||
| } | ||
| /** | ||
| * PKCS#7 填充 | ||
| */ | ||
| function pkcs7Pad(buf, blockSize) { | ||
| const mod = buf.length % blockSize; | ||
| const pad = mod === 0 ? blockSize : blockSize - mod; | ||
| const padByte = Buffer.alloc(1, pad); | ||
| return Buffer.concat([buf, Buffer.alloc(pad, padByte[0])]); | ||
| } | ||
| /** | ||
| * PKCS#7 解除填充 | ||
| */ | ||
| function pkcs7Unpad(buf, blockSize) { | ||
| if (buf.length === 0) | ||
| throw new Error("invalid pkcs7 payload"); | ||
| const pad = buf[buf.length - 1]; | ||
| if (pad < 1 || pad > blockSize) { | ||
| throw new Error("invalid pkcs7 padding value"); | ||
| } | ||
| if (pad > buf.length) { | ||
| throw new Error("invalid pkcs7 payload length"); | ||
| } | ||
| for (let i = 0; i < pad; i += 1) { | ||
| if (buf[buf.length - 1 - i] !== pad) { | ||
| throw new Error("invalid pkcs7 padding byte"); | ||
| } | ||
| } | ||
| return buf.subarray(0, buf.length - pad); | ||
| } | ||
| /** | ||
| * 计算 SHA1 哈希 | ||
| */ | ||
| function sha1Hex(input) { | ||
| return crypto$1.createHash("sha1").update(input).digest("hex"); | ||
| } | ||
| class WecomCrypto { | ||
| constructor(token, encodingAESKey, receiveId // 对应企业微信的 corpId 或 botId (用于校验与追加) | ||
| ) { | ||
| this.token = token; | ||
| this.encodingAESKey = encodingAESKey; | ||
| this.receiveId = receiveId; | ||
| if (!token) | ||
| throw new Error("token is required"); | ||
| this.aesKey = decodeEncodingAESKey(encodingAESKey); | ||
| this.iv = this.aesKey.subarray(0, 16); | ||
| } | ||
| /** | ||
| * 计算 WeCom 消息签名 | ||
| */ | ||
| computeSignature(timestamp, nonce, encrypt) { | ||
| const parts = [this.token, timestamp, nonce, encrypt] | ||
| .map((v) => String(v ?? "")) | ||
| .sort(); | ||
| return sha1Hex(parts.join("")); | ||
| } | ||
| /** | ||
| * 验证 WeCom 消息签名 | ||
| */ | ||
| verifySignature(signature, timestamp, nonce, encrypt) { | ||
| const expected = this.computeSignature(timestamp, nonce, encrypt); | ||
| return expected === signature; | ||
| } | ||
| /** | ||
| * 消息解密 | ||
| * 返回纯文本字符串(XML 或 JSON 根据上层业务而定) | ||
| */ | ||
| decrypt(encryptText) { | ||
| const decipher = crypto$1.createDecipheriv("aes-256-cbc", this.aesKey, this.iv); | ||
| decipher.setAutoPadding(false); | ||
| const decryptedPadded = Buffer.concat([ | ||
| decipher.update(Buffer.from(encryptText, "base64")), | ||
| decipher.final(), | ||
| ]); | ||
| const decrypted = pkcs7Unpad(decryptedPadded, CRYPTO_CONSTANTS.PKCS7_BLOCK_SIZE); | ||
| if (decrypted.length < 20) { | ||
| throw new Error(`invalid payload (expected >=20 bytes, got ${decrypted.length})`); | ||
| } | ||
| // 16 bytes random + 4 bytes length + msg + receiveId | ||
| const msgLen = decrypted.readUInt32BE(16); | ||
| const msgStart = 20; | ||
| const msgEnd = msgStart + msgLen; | ||
| if (msgEnd > decrypted.length) { | ||
| throw new Error(`invalid msg length (msgEnd=${msgEnd}, total=${decrypted.length})`); | ||
| } | ||
| const msg = decrypted.subarray(msgStart, msgEnd).toString("utf8"); | ||
| const receiveId = this.receiveId ?? ""; | ||
| if (receiveId) { | ||
| const trailing = decrypted.subarray(msgEnd).toString("utf8"); | ||
| if (trailing !== receiveId) { | ||
| throw new Error(`receiveId mismatch (expected "${receiveId}", got "${trailing}")`); | ||
| } | ||
| } | ||
| return msg; | ||
| } | ||
| /** | ||
| * 消息加密 | ||
| * 加密明文并返回 base64 格式密文与对应的新签名 | ||
| */ | ||
| encrypt(plainText, timestamp, nonce) { | ||
| const random16 = crypto$1.randomBytes(16); | ||
| const msgBuf = Buffer.from(plainText ?? "", "utf8"); | ||
| const msgLen = Buffer.alloc(4); | ||
| msgLen.writeUInt32BE(msgBuf.length, 0); | ||
| const receiveIdBuf = Buffer.from(this.receiveId ?? "", "utf8"); | ||
| const raw = Buffer.concat([random16, msgLen, msgBuf, receiveIdBuf]); | ||
| const padded = pkcs7Pad(raw, CRYPTO_CONSTANTS.PKCS7_BLOCK_SIZE); | ||
| const cipher = crypto$1.createCipheriv("aes-256-cbc", this.aesKey, this.iv); | ||
| cipher.setAutoPadding(false); | ||
| const encryptedBuf = Buffer.concat([cipher.update(padded), cipher.final()]); | ||
| const encryptBase64 = encryptedBuf.toString("base64"); | ||
| const signature = this.computeSignature(timestamp, nonce, encryptBase64); | ||
| return { encrypt: encryptBase64, signature }; | ||
| } | ||
| } | ||
| /** 默认导出 AiBot 命名空间 */ | ||
@@ -1414,4 +1555,6 @@ const AiBot = { | ||
| exports.WeComApiClient = WeComApiClient; | ||
| exports.WecomCrypto = WecomCrypto; | ||
| exports.WsCmd = WsCmd; | ||
| exports.WsConnectionManager = WsConnectionManager; | ||
| exports.decodeEncodingAESKey = decodeEncodingAESKey; | ||
| exports.decryptFile = decryptFile; | ||
@@ -1421,2 +1564,4 @@ exports.default = AiBot; | ||
| exports.generateReqId = generateReqId; | ||
| exports.pkcs7Pad = pkcs7Pad; | ||
| exports.pkcs7Unpad = pkcs7Unpad; | ||
| //# sourceMappingURL=index.cjs.js.map |
+48
-1
@@ -1225,2 +1225,49 @@ import { EventEmitter } from 'eventemitter3'; | ||
| /** | ||
| * WeCom 加解密通用核心 | ||
| * 独立于 Webhook、WebSocket、Agent 的具体协议形态,统一提供基于 AES-256-CBC | ||
| * 的加解密与 SHA1 签名计算能力。 | ||
| */ | ||
| /** | ||
| * 解码企业微信提供的 Base64 encodingAESKey | ||
| */ | ||
| declare function decodeEncodingAESKey(encodingAESKey: string): Buffer; | ||
| /** | ||
| * PKCS#7 填充 | ||
| */ | ||
| declare function pkcs7Pad(buf: Buffer, blockSize: number): Buffer; | ||
| /** | ||
| * PKCS#7 解除填充 | ||
| */ | ||
| declare function pkcs7Unpad(buf: Buffer, blockSize: number): Buffer; | ||
| declare class WecomCrypto { | ||
| private token; | ||
| private encodingAESKey; | ||
| private receiveId?; | ||
| private aesKey; | ||
| private iv; | ||
| constructor(token: string, encodingAESKey: string, receiveId?: string | undefined); | ||
| /** | ||
| * 计算 WeCom 消息签名 | ||
| */ | ||
| computeSignature(timestamp: string, nonce: string, encrypt: string): string; | ||
| /** | ||
| * 验证 WeCom 消息签名 | ||
| */ | ||
| verifySignature(signature: string, timestamp: string, nonce: string, encrypt: string): boolean; | ||
| /** | ||
| * 消息解密 | ||
| * 返回纯文本字符串(XML 或 JSON 根据上层业务而定) | ||
| */ | ||
| decrypt(encryptText: string): string; | ||
| /** | ||
| * 消息加密 | ||
| * 加密明文并返回 base64 格式密文与对应的新签名 | ||
| */ | ||
| encrypt(plainText: string, timestamp: string, nonce: string): { | ||
| encrypt: string; | ||
| signature: string; | ||
| }; | ||
| } | ||
| /** | ||
| * 加解密工具模块 | ||
@@ -1277,3 +1324,3 @@ * 提供文件加解密相关的功能函数 | ||
| export { DefaultLogger, EventType, MessageHandler, MessageType, TemplateCardType, WSAuthFailureError, WSClient, WSReconnectExhaustedError, WeComApiClient, WsCmd, WsConnectionManager, decryptFile, AiBot as default, generateRandomString, generateReqId }; | ||
| export { DefaultLogger, EventType, MessageHandler, MessageType, TemplateCardType, WSAuthFailureError, WSClient, WSReconnectExhaustedError, WeComApiClient, WecomCrypto, WsCmd, WsConnectionManager, decodeEncodingAESKey, decryptFile, AiBot as default, generateRandomString, generateReqId, pkcs7Pad, pkcs7Unpad }; | ||
| export type { BaseMessage, EnterChatEvent, EventContent, EventFrom, EventMessage, EventMessageWith, FeedbackEventData, FileContent, FileMessage, ImageContent, ImageMessage, Logger, MessageFrom, MixedContent, MixedMessage, MixedMsgItem, QuoteContent, ReplyFeedback, ReplyMsgItem, ReplyOptions, SendMarkdownMsgBody, SendMarkdownParams, SendMediaMsgBody, SendMsgBody, SendTemplateCardMsgBody, SendTextParams, StreamReplyBody, StreamWithTemplateCardReplyBody, TemplateCard, TemplateCardAction, TemplateCardActionMenu, TemplateCardButton, TemplateCardCheckbox, TemplateCardEmphasisContent, TemplateCardEventData, TemplateCardHorizontalContent, TemplateCardImage, TemplateCardImageTextArea, TemplateCardJumpAction, TemplateCardMainTitle, TemplateCardQuoteArea, TemplateCardReplyBody, TemplateCardSelectionItem, TemplateCardSource, TemplateCardSubmitButton, TemplateCardVerticalContent, TextContent, TextMessage, UpdateTemplateCardBody, UploadMediaChunkBody, UploadMediaFinishBody, UploadMediaFinishResult, UploadMediaInitBody, UploadMediaInitResult, UploadMediaOptions, VideoContent, VideoMessage, VoiceContent, VoiceMessage, WSClientEventMap, WSClientOptions, WeComMediaType, WelcomeReplyBody, WelcomeTemplateCardReplyBody, WelcomeTextReplyBody, WsFrame, WsFrameHeaders }; |
+142
-1
@@ -5,2 +5,3 @@ import { EventEmitter } from 'eventemitter3'; | ||
| import WebSocket from 'ws'; | ||
| import crypto$1 from 'node:crypto'; | ||
@@ -1398,2 +1399,142 @@ /** | ||
| /** | ||
| * WeCom 加解密通用核心 | ||
| * 独立于 Webhook、WebSocket、Agent 的具体协议形态,统一提供基于 AES-256-CBC | ||
| * 的加解密与 SHA1 签名计算能力。 | ||
| */ | ||
| const CRYPTO_CONSTANTS = { | ||
| /** PKCS#7 块大小 */ | ||
| PKCS7_BLOCK_SIZE: 32, | ||
| /** AES Key 长度 */ | ||
| AES_KEY_LENGTH: 32, | ||
| }; | ||
| /** | ||
| * 解码企业微信提供的 Base64 encodingAESKey | ||
| */ | ||
| function decodeEncodingAESKey(encodingAESKey) { | ||
| const trimmed = encodingAESKey.trim(); | ||
| if (!trimmed) | ||
| throw new Error("encodingAESKey missing"); | ||
| const withPadding = trimmed.endsWith("=") ? trimmed : `${trimmed}=`; | ||
| const key = Buffer.from(withPadding, "base64"); | ||
| if (key.length !== CRYPTO_CONSTANTS.AES_KEY_LENGTH) { | ||
| throw new Error(`invalid encodingAESKey (expected ${CRYPTO_CONSTANTS.AES_KEY_LENGTH} bytes, got ${key.length})`); | ||
| } | ||
| return key; | ||
| } | ||
| /** | ||
| * PKCS#7 填充 | ||
| */ | ||
| function pkcs7Pad(buf, blockSize) { | ||
| const mod = buf.length % blockSize; | ||
| const pad = mod === 0 ? blockSize : blockSize - mod; | ||
| const padByte = Buffer.alloc(1, pad); | ||
| return Buffer.concat([buf, Buffer.alloc(pad, padByte[0])]); | ||
| } | ||
| /** | ||
| * PKCS#7 解除填充 | ||
| */ | ||
| function pkcs7Unpad(buf, blockSize) { | ||
| if (buf.length === 0) | ||
| throw new Error("invalid pkcs7 payload"); | ||
| const pad = buf[buf.length - 1]; | ||
| if (pad < 1 || pad > blockSize) { | ||
| throw new Error("invalid pkcs7 padding value"); | ||
| } | ||
| if (pad > buf.length) { | ||
| throw new Error("invalid pkcs7 payload length"); | ||
| } | ||
| for (let i = 0; i < pad; i += 1) { | ||
| if (buf[buf.length - 1 - i] !== pad) { | ||
| throw new Error("invalid pkcs7 padding byte"); | ||
| } | ||
| } | ||
| return buf.subarray(0, buf.length - pad); | ||
| } | ||
| /** | ||
| * 计算 SHA1 哈希 | ||
| */ | ||
| function sha1Hex(input) { | ||
| return crypto$1.createHash("sha1").update(input).digest("hex"); | ||
| } | ||
| class WecomCrypto { | ||
| constructor(token, encodingAESKey, receiveId // 对应企业微信的 corpId 或 botId (用于校验与追加) | ||
| ) { | ||
| this.token = token; | ||
| this.encodingAESKey = encodingAESKey; | ||
| this.receiveId = receiveId; | ||
| if (!token) | ||
| throw new Error("token is required"); | ||
| this.aesKey = decodeEncodingAESKey(encodingAESKey); | ||
| this.iv = this.aesKey.subarray(0, 16); | ||
| } | ||
| /** | ||
| * 计算 WeCom 消息签名 | ||
| */ | ||
| computeSignature(timestamp, nonce, encrypt) { | ||
| const parts = [this.token, timestamp, nonce, encrypt] | ||
| .map((v) => String(v ?? "")) | ||
| .sort(); | ||
| return sha1Hex(parts.join("")); | ||
| } | ||
| /** | ||
| * 验证 WeCom 消息签名 | ||
| */ | ||
| verifySignature(signature, timestamp, nonce, encrypt) { | ||
| const expected = this.computeSignature(timestamp, nonce, encrypt); | ||
| return expected === signature; | ||
| } | ||
| /** | ||
| * 消息解密 | ||
| * 返回纯文本字符串(XML 或 JSON 根据上层业务而定) | ||
| */ | ||
| decrypt(encryptText) { | ||
| const decipher = crypto$1.createDecipheriv("aes-256-cbc", this.aesKey, this.iv); | ||
| decipher.setAutoPadding(false); | ||
| const decryptedPadded = Buffer.concat([ | ||
| decipher.update(Buffer.from(encryptText, "base64")), | ||
| decipher.final(), | ||
| ]); | ||
| const decrypted = pkcs7Unpad(decryptedPadded, CRYPTO_CONSTANTS.PKCS7_BLOCK_SIZE); | ||
| if (decrypted.length < 20) { | ||
| throw new Error(`invalid payload (expected >=20 bytes, got ${decrypted.length})`); | ||
| } | ||
| // 16 bytes random + 4 bytes length + msg + receiveId | ||
| const msgLen = decrypted.readUInt32BE(16); | ||
| const msgStart = 20; | ||
| const msgEnd = msgStart + msgLen; | ||
| if (msgEnd > decrypted.length) { | ||
| throw new Error(`invalid msg length (msgEnd=${msgEnd}, total=${decrypted.length})`); | ||
| } | ||
| const msg = decrypted.subarray(msgStart, msgEnd).toString("utf8"); | ||
| const receiveId = this.receiveId ?? ""; | ||
| if (receiveId) { | ||
| const trailing = decrypted.subarray(msgEnd).toString("utf8"); | ||
| if (trailing !== receiveId) { | ||
| throw new Error(`receiveId mismatch (expected "${receiveId}", got "${trailing}")`); | ||
| } | ||
| } | ||
| return msg; | ||
| } | ||
| /** | ||
| * 消息加密 | ||
| * 加密明文并返回 base64 格式密文与对应的新签名 | ||
| */ | ||
| encrypt(plainText, timestamp, nonce) { | ||
| const random16 = crypto$1.randomBytes(16); | ||
| const msgBuf = Buffer.from(plainText ?? "", "utf8"); | ||
| const msgLen = Buffer.alloc(4); | ||
| msgLen.writeUInt32BE(msgBuf.length, 0); | ||
| const receiveIdBuf = Buffer.from(this.receiveId ?? "", "utf8"); | ||
| const raw = Buffer.concat([random16, msgLen, msgBuf, receiveIdBuf]); | ||
| const padded = pkcs7Pad(raw, CRYPTO_CONSTANTS.PKCS7_BLOCK_SIZE); | ||
| const cipher = crypto$1.createCipheriv("aes-256-cbc", this.aesKey, this.iv); | ||
| cipher.setAutoPadding(false); | ||
| const encryptedBuf = Buffer.concat([cipher.update(padded), cipher.final()]); | ||
| const encryptBase64 = encryptedBuf.toString("base64"); | ||
| const signature = this.computeSignature(timestamp, nonce, encryptBase64); | ||
| return { encrypt: encryptBase64, signature }; | ||
| } | ||
| } | ||
| /** 默认导出 AiBot 命名空间 */ | ||
@@ -1404,3 +1545,3 @@ const AiBot = { | ||
| export { DefaultLogger, EventType, MessageHandler, MessageType, TemplateCardType, WSAuthFailureError, WSClient, WSReconnectExhaustedError, WeComApiClient, WsCmd, WsConnectionManager, decryptFile, AiBot as default, generateRandomString, generateReqId }; | ||
| export { DefaultLogger, EventType, MessageHandler, MessageType, TemplateCardType, WSAuthFailureError, WSClient, WSReconnectExhaustedError, WeComApiClient, WecomCrypto, WsCmd, WsConnectionManager, decodeEncodingAESKey, decryptFile, AiBot as default, generateRandomString, generateReqId, pkcs7Pad, pkcs7Unpad }; | ||
| //# sourceMappingURL=index.esm.js.map |
+5
-4
| { | ||
| "name": "@wecom/aibot-node-sdk", | ||
| "version": "1.0.5", | ||
| "version": "1.0.6-beta.0", | ||
| "description": "企业微信智能机器人 Node.js SDK - WebSocket 长连接通道", | ||
@@ -18,3 +18,4 @@ "main": "dist/index.cjs.js", | ||
| "release:dry": "node scripts/publish-all.mjs --dry-run", | ||
| "example": "ts-node examples/basic.ts" | ||
| "example": "ts-node examples/basic.ts", | ||
| "test": "vitest run" | ||
| }, | ||
@@ -39,6 +40,6 @@ "keywords": [ | ||
| "dependencies": { | ||
| "ws": "^8.16.0", | ||
| "axios": "^1.6.7", | ||
| "eventemitter3": "^5.0.1" | ||
| "eventemitter3": "^5.0.1", | ||
| "ws": "^8.16.0" | ||
| } | ||
| } |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
516162
7.24%21
5%5719
7.08%