{"version":3,"file":"index.mjs","names":["DEFAULT_LONG_POLL_TIMEOUT_MS"],"sources":["../src/api/client.ts","../src/api/types.ts","../src/auth/qr-login.ts","../src/cdn/aes-ecb.ts","../src/cdn/cdn-url.ts","../src/cdn/cdn-download.ts","../src/media/download.ts","../src/util/mime.ts","../src/cdn/cdn-upload.ts","../src/media/upload.ts","../src/util/random.ts","../src/media/send.ts","../src/monitor.ts","../src/client.ts"],"sourcesContent":["/**\n * Low-level HTTP API client for the WeChat iLink bot protocol.\n *\n * Endpoints (all POST JSON):\n *   - ilink/bot/getupdates        — long-poll for new messages\n *   - ilink/bot/sendmessage       — send a message (text/image/video/file)\n *   - ilink/bot/getuploadurl      — get CDN upload pre-signed URL\n *   - ilink/bot/getconfig         — get account config (typing ticket, etc.)\n *   - ilink/bot/sendtyping        — send/cancel typing indicator\n *   - ilink/bot/get_bot_qrcode    — initiate QR code login (GET)\n *   - ilink/bot/get_qrcode_status — poll QR scan status (GET)\n */\nimport crypto from \"node:crypto\";\n\nimport type {\n  BaseInfo,\n  GetUpdatesResp,\n  GetUploadUrlReq,\n  GetUploadUrlResp,\n  GetConfigResp,\n  SendMessageReq,\n  SendTypingReq,\n  QRCodeResponse,\n  QRCodeStatusResponse,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nexport const DEFAULT_BASE_URL = \"https://ilinkai.weixin.qq.com\";\nexport const CDN_BASE_URL = \"https://novac2c.cdn.weixin.qq.com/c2c\";\n\n/** Default long-poll timeout for getUpdates requests. */\nconst DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;\n/** Default timeout for regular API requests. */\nconst DEFAULT_API_TIMEOUT_MS = 15_000;\n/** Default timeout for lightweight requests (getConfig, sendTyping). */\nconst DEFAULT_CONFIG_TIMEOUT_MS = 10_000;\n/** Default client-side timeout for get_qrcode_status long-poll. */\nconst QR_LONG_POLL_TIMEOUT_MS = 35_000;\n/** Default bot_type value. */\nexport const DEFAULT_BOT_TYPE = \"3\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction ensureTrailingSlash(url: string): string {\n  return url.endsWith(\"/\") ? url : `${url}/`;\n}\n\n/** X-WECHAT-UIN header: random uint32 -> decimal string -> base64. */\nfunction randomWechatUin(): string {\n  const uint32 = crypto.randomBytes(4).readUInt32BE(0);\n  return Buffer.from(String(uint32), \"utf-8\").toString(\"base64\");\n}\n\n// ---------------------------------------------------------------------------\n// API client options\n// ---------------------------------------------------------------------------\n\nexport interface ApiClientOptions {\n  baseUrl?: string;\n  cdnBaseUrl?: string;\n  token?: string;\n  /** Channel version string sent as base_info.channel_version. */\n  channelVersion?: string;\n  /** Optional SKRouteTag header. */\n  routeTag?: string;\n}\n\n// ---------------------------------------------------------------------------\n// ApiClient\n// ---------------------------------------------------------------------------\n\nexport class ApiClient {\n  readonly baseUrl: string;\n  readonly cdnBaseUrl: string;\n  private token?: string;\n  private channelVersion: string;\n  private routeTag?: string;\n\n  constructor(opts: ApiClientOptions = {}) {\n    this.baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;\n    this.cdnBaseUrl = opts.cdnBaseUrl ?? CDN_BASE_URL;\n    this.token = opts.token;\n    this.channelVersion = opts.channelVersion ?? \"standalone-0.1.0\";\n    this.routeTag = opts.routeTag;\n  }\n\n  /** Update the bearer token (after QR login). */\n  setToken(token: string): void {\n    this.token = token;\n  }\n\n  getToken(): string | undefined {\n    return this.token;\n  }\n\n  // -----------------------------------------------------------------------\n  // Internal helpers\n  // -----------------------------------------------------------------------\n\n  private buildBaseInfo(): BaseInfo {\n    return { channel_version: this.channelVersion };\n  }\n\n  private buildHeaders(bodyStr: string): Record<string, string> {\n    const headers: Record<string, string> = {\n      \"Content-Type\": \"application/json\",\n      AuthorizationType: \"ilink_bot_token\",\n      \"Content-Length\": String(Buffer.byteLength(bodyStr, \"utf-8\")),\n      \"X-WECHAT-UIN\": randomWechatUin(),\n    };\n    if (this.token?.trim()) {\n      headers.Authorization = `Bearer ${this.token.trim()}`;\n    }\n    if (this.routeTag) {\n      headers.SKRouteTag = this.routeTag;\n    }\n    return headers;\n  }\n\n  /**\n   * POST JSON to an iLink API endpoint with timeout + abort.\n   */\n  private async apiFetch(params: {\n    endpoint: string;\n    body: string;\n    timeoutMs: number;\n  }): Promise<string> {\n    const base = ensureTrailingSlash(this.baseUrl);\n    const url = new URL(params.endpoint, base);\n    const headers = this.buildHeaders(params.body);\n\n    const controller = new AbortController();\n    const timer = setTimeout(() => controller.abort(), params.timeoutMs);\n    try {\n      const res = await fetch(url.toString(), {\n        method: \"POST\",\n        headers,\n        body: params.body,\n        signal: controller.signal,\n      });\n      clearTimeout(timer);\n      const rawText = await res.text();\n      if (!res.ok) {\n        throw new Error(`API ${params.endpoint} ${res.status}: ${rawText}`);\n      }\n      return rawText;\n    } catch (err) {\n      clearTimeout(timer);\n      throw err;\n    }\n  }\n\n  // -----------------------------------------------------------------------\n  // Public API methods\n  // -----------------------------------------------------------------------\n\n  /**\n   * Long-poll for new messages. Returns empty response on client-side timeout\n   * (normal for long-poll).\n   */\n  async getUpdates(\n    getUpdatesBuf: string,\n    timeoutMs?: number,\n  ): Promise<GetUpdatesResp> {\n    const timeout = timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;\n    try {\n      const rawText = await this.apiFetch({\n        endpoint: \"ilink/bot/getupdates\",\n        body: JSON.stringify({\n          get_updates_buf: getUpdatesBuf,\n          base_info: this.buildBaseInfo(),\n        }),\n        timeoutMs: timeout,\n      });\n      return JSON.parse(rawText) as GetUpdatesResp;\n    } catch (err) {\n      if (err instanceof Error && err.name === \"AbortError\") {\n        return { ret: 0, msgs: [], get_updates_buf: getUpdatesBuf };\n      }\n      throw err;\n    }\n  }\n\n  /** Send a message downstream. */\n  async sendMessage(req: SendMessageReq): Promise<void> {\n    await this.apiFetch({\n      endpoint: \"ilink/bot/sendmessage\",\n      body: JSON.stringify({ ...req, base_info: this.buildBaseInfo() }),\n      timeoutMs: DEFAULT_API_TIMEOUT_MS,\n    });\n  }\n\n  /** Get a pre-signed CDN upload URL. */\n  async getUploadUrl(\n    req: GetUploadUrlReq,\n  ): Promise<GetUploadUrlResp> {\n    const rawText = await this.apiFetch({\n      endpoint: \"ilink/bot/getuploadurl\",\n      body: JSON.stringify({\n        ...req,\n        base_info: this.buildBaseInfo(),\n      }),\n      timeoutMs: DEFAULT_API_TIMEOUT_MS,\n    });\n    return JSON.parse(rawText) as GetUploadUrlResp;\n  }\n\n  /** Fetch bot config (includes typing_ticket) for a given user. */\n  async getConfig(\n    ilinkUserId: string,\n    contextToken?: string,\n  ): Promise<GetConfigResp> {\n    const rawText = await this.apiFetch({\n      endpoint: \"ilink/bot/getconfig\",\n      body: JSON.stringify({\n        ilink_user_id: ilinkUserId,\n        context_token: contextToken,\n        base_info: this.buildBaseInfo(),\n      }),\n      timeoutMs: DEFAULT_CONFIG_TIMEOUT_MS,\n    });\n    return JSON.parse(rawText) as GetConfigResp;\n  }\n\n  /** Send a typing indicator. */\n  async sendTyping(req: SendTypingReq): Promise<void> {\n    await this.apiFetch({\n      endpoint: \"ilink/bot/sendtyping\",\n      body: JSON.stringify({\n        ...req,\n        base_info: this.buildBaseInfo(),\n      }),\n      timeoutMs: DEFAULT_CONFIG_TIMEOUT_MS,\n    });\n  }\n\n  // -----------------------------------------------------------------------\n  // QR code login (these use GET, not POST)\n  // -----------------------------------------------------------------------\n\n  /** Fetch a new QR code for bot login. */\n  async getQRCode(botType?: string): Promise<QRCodeResponse> {\n    const base = ensureTrailingSlash(this.baseUrl);\n    const bt = botType ?? DEFAULT_BOT_TYPE;\n    const url = new URL(\n      `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(bt)}`,\n      base,\n    );\n\n    const headers: Record<string, string> = {};\n    if (this.routeTag) {\n      headers.SKRouteTag = this.routeTag;\n    }\n\n    const res = await fetch(url.toString(), { headers });\n    if (!res.ok) {\n      const body = await res.text().catch(() => \"(unreadable)\");\n      throw new Error(\n        `Failed to fetch QR code: ${res.status} ${res.statusText}: ${body}`,\n      );\n    }\n    return (await res.json()) as QRCodeResponse;\n  }\n\n  /**\n   * Long-poll the QR code scan status.\n   * Returns `{ status: \"wait\" }` on client-side timeout.\n   */\n  async pollQRCodeStatus(\n    qrcode: string,\n  ): Promise<QRCodeStatusResponse> {\n    const base = ensureTrailingSlash(this.baseUrl);\n    const url = new URL(\n      `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,\n      base,\n    );\n\n    const headers: Record<string, string> = {\n      \"iLink-App-ClientVersion\": \"1\",\n    };\n    if (this.routeTag) {\n      headers.SKRouteTag = this.routeTag;\n    }\n\n    const controller = new AbortController();\n    const timer = setTimeout(\n      () => controller.abort(),\n      QR_LONG_POLL_TIMEOUT_MS,\n    );\n    try {\n      const res = await fetch(url.toString(), {\n        headers,\n        signal: controller.signal,\n      });\n      clearTimeout(timer);\n      const rawText = await res.text();\n      if (!res.ok) {\n        throw new Error(\n          `Failed to poll QR status: ${res.status} ${res.statusText}: ${rawText}`,\n        );\n      }\n      return JSON.parse(rawText) as QRCodeStatusResponse;\n    } catch (err) {\n      clearTimeout(timer);\n      if (err instanceof Error && err.name === \"AbortError\") {\n        return { status: \"wait\" };\n      }\n      throw err;\n    }\n  }\n}\n","/**\n * WeChat iLink bot protocol types.\n *\n * Reverse-engineered from @tencent-weixin/openclaw-weixin.\n * The backend API uses JSON over HTTP; byte fields are base64 strings in JSON.\n */\n\n// ---------------------------------------------------------------------------\n// Enums / constants\n// ---------------------------------------------------------------------------\n\nexport const UploadMediaType = {\n  IMAGE: 1,\n  VIDEO: 2,\n  FILE: 3,\n  VOICE: 4,\n} as const;\nexport type UploadMediaType = (typeof UploadMediaType)[keyof typeof UploadMediaType];\n\nexport const MessageType = {\n  NONE: 0,\n  USER: 1,\n  BOT: 2,\n} as const;\nexport type MessageType = (typeof MessageType)[keyof typeof MessageType];\n\nexport const MessageItemType = {\n  NONE: 0,\n  TEXT: 1,\n  IMAGE: 2,\n  VOICE: 3,\n  FILE: 4,\n  VIDEO: 5,\n} as const;\nexport type MessageItemType = (typeof MessageItemType)[keyof typeof MessageItemType];\n\nexport const MessageState = {\n  NEW: 0,\n  GENERATING: 1,\n  FINISH: 2,\n} as const;\nexport type MessageState = (typeof MessageState)[keyof typeof MessageState];\n\nexport const TypingStatus = {\n  TYPING: 1,\n  CANCEL: 2,\n} as const;\nexport type TypingStatus = (typeof TypingStatus)[keyof typeof TypingStatus];\n\n// ---------------------------------------------------------------------------\n// Common\n// ---------------------------------------------------------------------------\n\n/** Metadata attached to every outgoing CGI request. */\nexport interface BaseInfo {\n  channel_version?: string;\n}\n\n// ---------------------------------------------------------------------------\n// CDN / Media references\n// ---------------------------------------------------------------------------\n\n/** CDN media reference; aes_key is base64-encoded bytes in JSON. */\nexport interface CDNMedia {\n  /** Encrypted parameters for CDN download/upload. */\n  encrypt_query_param?: string;\n  /** Base64-encoded AES-128 key. */\n  aes_key?: string;\n  /** 0 = only encrypt fileid, 1 = packed thumb/mid info. */\n  encrypt_type?: number;\n}\n\nexport interface TextItem {\n  text?: string;\n}\n\nexport interface ImageItem {\n  /** Original image CDN reference. */\n  media?: CDNMedia;\n  /** Thumbnail CDN reference. */\n  thumb_media?: CDNMedia;\n  /** Raw AES-128 key as hex string (16 bytes); preferred over media.aes_key for inbound decryption. */\n  aeskey?: string;\n  url?: string;\n  mid_size?: number;\n  thumb_size?: number;\n  thumb_height?: number;\n  thumb_width?: number;\n  hd_size?: number;\n}\n\nexport interface VoiceItem {\n  media?: CDNMedia;\n  /** Voice encoding: 1=pcm, 2=adpcm, 3=feature, 4=speex, 5=amr, 6=silk, 7=mp3, 8=ogg-speex */\n  encode_type?: number;\n  bits_per_sample?: number;\n  /** Sample rate in Hz. */\n  sample_rate?: number;\n  /** Duration in milliseconds. */\n  playtime?: number;\n  /** Speech-to-text content (if available). */\n  text?: string;\n}\n\nexport interface FileItem {\n  media?: CDNMedia;\n  file_name?: string;\n  md5?: string;\n  /** File size as string. */\n  len?: string;\n}\n\nexport interface VideoItem {\n  media?: CDNMedia;\n  video_size?: number;\n  play_length?: number;\n  video_md5?: string;\n  thumb_media?: CDNMedia;\n  thumb_size?: number;\n  thumb_height?: number;\n  thumb_width?: number;\n}\n\nexport interface RefMessage {\n  message_item?: MessageItem;\n  /** Summary text. */\n  title?: string;\n}\n\nexport interface MessageItem {\n  type?: number;\n  create_time_ms?: number;\n  update_time_ms?: number;\n  is_completed?: boolean;\n  msg_id?: string;\n  ref_msg?: RefMessage;\n  text_item?: TextItem;\n  image_item?: ImageItem;\n  voice_item?: VoiceItem;\n  file_item?: FileItem;\n  video_item?: VideoItem;\n}\n\n// ---------------------------------------------------------------------------\n// WeixinMessage — the unified message envelope\n// ---------------------------------------------------------------------------\n\nexport interface WeixinMessage {\n  seq?: number;\n  message_id?: number;\n  from_user_id?: string;\n  to_user_id?: string;\n  client_id?: string;\n  create_time_ms?: number;\n  update_time_ms?: number;\n  delete_time_ms?: number;\n  session_id?: string;\n  group_id?: string;\n  /** 1 = USER, 2 = BOT */\n  message_type?: number;\n  /** 0 = NEW, 1 = GENERATING, 2 = FINISH */\n  message_state?: number;\n  item_list?: MessageItem[];\n  /** Conversation context token — must be echoed verbatim in replies. */\n  context_token?: string;\n}\n\n// ---------------------------------------------------------------------------\n// getUpdates (long-poll)\n// ---------------------------------------------------------------------------\n\nexport interface GetUpdatesReq {\n  /** Full context buf cached locally; send \"\" on first request. */\n  get_updates_buf?: string;\n  base_info?: BaseInfo;\n}\n\nexport interface GetUpdatesResp {\n  ret?: number;\n  /** Error code from server (e.g. -14 = session timeout). */\n  errcode?: number;\n  errmsg?: string;\n  msgs?: WeixinMessage[];\n  /** Full context buf to cache locally and send on next request. */\n  get_updates_buf?: string;\n  /** Server-suggested timeout (ms) for the next long-poll. */\n  longpolling_timeout_ms?: number;\n}\n\n// ---------------------------------------------------------------------------\n// sendMessage\n// ---------------------------------------------------------------------------\n\nexport interface SendMessageReq {\n  msg?: WeixinMessage;\n  base_info?: BaseInfo;\n}\n\nexport interface SendMessageResp {\n  ret?: number;\n  errmsg?: string;\n}\n\n// ---------------------------------------------------------------------------\n// getUploadUrl\n// ---------------------------------------------------------------------------\n\nexport interface GetUploadUrlReq {\n  filekey?: string;\n  /** See UploadMediaType: 1=IMAGE, 2=VIDEO, 3=FILE, 4=VOICE */\n  media_type?: number;\n  to_user_id?: string;\n  /** Original file plaintext size. */\n  rawsize?: number;\n  /** Original file plaintext MD5 (hex). */\n  rawfilemd5?: string;\n  /** Ciphertext size after AES-128-ECB encryption. */\n  filesize?: number;\n  /** Thumbnail plaintext size (IMAGE/VIDEO). */\n  thumb_rawsize?: number;\n  /** Thumbnail plaintext MD5 (IMAGE/VIDEO). */\n  thumb_rawfilemd5?: string;\n  /** Thumbnail ciphertext size (IMAGE/VIDEO). */\n  thumb_filesize?: number;\n  /** Skip thumbnail upload URL. */\n  no_need_thumb?: boolean;\n  /** AES key (hex). */\n  aeskey?: string;\n  base_info?: BaseInfo;\n}\n\nexport interface GetUploadUrlResp {\n  /** Original image upload encrypted parameters. */\n  upload_param?: string;\n  /** Thumbnail upload encrypted parameters. */\n  thumb_upload_param?: string;\n}\n\n// ---------------------------------------------------------------------------\n// getConfig\n// ---------------------------------------------------------------------------\n\nexport interface GetConfigReq {\n  ilink_user_id?: string;\n  context_token?: string;\n  base_info?: BaseInfo;\n}\n\nexport interface GetConfigResp {\n  ret?: number;\n  errmsg?: string;\n  /** Base64-encoded typing ticket for sendTyping. */\n  typing_ticket?: string;\n}\n\n// ---------------------------------------------------------------------------\n// sendTyping\n// ---------------------------------------------------------------------------\n\nexport interface SendTypingReq {\n  ilink_user_id?: string;\n  typing_ticket?: string;\n  /** 1 = typing, 2 = cancel typing */\n  status?: number;\n  base_info?: BaseInfo;\n}\n\nexport interface SendTypingResp {\n  ret?: number;\n  errmsg?: string;\n}\n\n// ---------------------------------------------------------------------------\n// QR code login\n// ---------------------------------------------------------------------------\n\nexport interface QRCodeResponse {\n  /** Opaque QR code identifier (pass to get_qrcode_status). */\n  qrcode: string;\n  /** URL that renders/encodes the QR image. */\n  qrcode_img_content: string;\n}\n\nexport interface QRCodeStatusResponse {\n  status: \"wait\" | \"scaned\" | \"confirmed\" | \"expired\";\n  bot_token?: string;\n  /** Bot account identifier (e.g. \"hex@im.bot\"). */\n  ilink_bot_id?: string;\n  /** API base URL returned on successful login. */\n  baseurl?: string;\n  /** User ID of the person who scanned the QR code. */\n  ilink_user_id?: string;\n}\n","/**\n * QR code login flow for the WeChat iLink bot protocol.\n *\n * Flow:\n *   1. GET ilink/bot/get_bot_qrcode?bot_type=3 -> { qrcode, qrcode_img_content }\n *   2. Caller renders the QR code (library only returns the URL)\n *   3. Long-poll GET ilink/bot/get_qrcode_status?qrcode=... until \"confirmed\"\n *   4. Extract bot_token, ilink_bot_id, baseurl, ilink_user_id\n */\nimport type { ApiClient } from \"../api/client.js\";\nimport type { QRCodeStatusResponse } from \"../api/types.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface LoginResult {\n  connected: boolean;\n  botToken?: string;\n  accountId?: string;\n  baseUrl?: string;\n  userId?: string;\n  message: string;\n}\n\nexport interface QRLoginOptions {\n  /** Maximum time to wait for QR scan (ms). Default: 480_000 (8 min). */\n  timeoutMs?: number;\n  /** bot_type parameter (default: \"3\"). */\n  botType?: string;\n  /** Maximum number of QR code refreshes on expiry. Default: 3. */\n  maxRefreshes?: number;\n  /**\n   * Called when a QR code URL is available (initial and on refresh).\n   * The caller is responsible for rendering the QR code.\n   */\n  onQRCode?: (qrcodeUrl: string) => void | Promise<void>;\n  /** Called when status changes. */\n  onStatus?: (status: QRCodeStatusResponse[\"status\"]) => void;\n  /** AbortSignal for cancellation. */\n  signal?: AbortSignal;\n}\n\n// ---------------------------------------------------------------------------\n// Login implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Run the full QR code login flow. Returns a LoginResult.\n *\n * The library does NOT render QR codes — use `opts.onQRCode` to receive\n * the QR code URL and handle display yourself.\n */\nexport async function loginWithQRCode(\n  api: ApiClient,\n  opts: QRLoginOptions = {},\n): Promise<LoginResult> {\n  const timeoutMs = Math.max(opts.timeoutMs ?? 480_000, 1000);\n  const maxRefreshes = opts.maxRefreshes ?? 3;\n  const deadline = Date.now() + timeoutMs;\n  let refreshCount = 1;\n\n  // Step 1: fetch initial QR code\n  const qrResponse = await api.getQRCode(opts.botType);\n  let qrcode = qrResponse.qrcode;\n\n  // Notify caller with QR code URL\n  if (opts.onQRCode) {\n    await opts.onQRCode(qrResponse.qrcode_img_content);\n  }\n\n  // Step 2: poll until confirmed, expired (refresh), or timeout\n  while (Date.now() < deadline) {\n    if (opts.signal?.aborted) {\n      return { connected: false, message: \"Login cancelled.\" };\n    }\n\n    const status = await api.pollQRCodeStatus(qrcode);\n    opts.onStatus?.(status.status);\n\n    switch (status.status) {\n      case \"wait\":\n        break;\n\n      case \"scaned\":\n        break;\n\n      case \"expired\": {\n        refreshCount++;\n        if (refreshCount > maxRefreshes) {\n          return {\n            connected: false,\n            message: `QR code expired ${maxRefreshes} times. Please restart login.`,\n          };\n        }\n        // Fetch a new QR code\n        const refreshed = await api.getQRCode(opts.botType);\n        qrcode = refreshed.qrcode;\n        if (opts.onQRCode) {\n          await opts.onQRCode(refreshed.qrcode_img_content);\n        }\n        break;\n      }\n\n      case \"confirmed\": {\n        if (!status.ilink_bot_id) {\n          return {\n            connected: false,\n            message:\n              \"Login confirmed but server did not return ilink_bot_id.\",\n          };\n        }\n        return {\n          connected: true,\n          botToken: status.bot_token,\n          accountId: status.ilink_bot_id,\n          baseUrl: status.baseurl,\n          userId: status.ilink_user_id,\n          message: \"Login successful!\",\n        };\n      }\n    }\n\n    // Brief pause between polls\n    await new Promise((r) => setTimeout(r, 1000));\n  }\n\n  return { connected: false, message: \"Login timed out.\" };\n}\n","/**\n * AES-128-ECB crypto utilities used by the WeChat CDN for media upload/download.\n *\n * All media files are encrypted with AES-128-ECB (PKCS7 padding) before upload,\n * and must be decrypted after download.\n */\nimport { createCipheriv, createDecipheriv } from \"node:crypto\";\n\n/** Encrypt a buffer with AES-128-ECB (PKCS7 padding). */\nexport function encryptAesEcb(plaintext: Buffer, key: Buffer): Buffer {\n  const cipher = createCipheriv(\"aes-128-ecb\", key, null);\n  return Buffer.concat([cipher.update(plaintext), cipher.final()]);\n}\n\n/** Decrypt a buffer with AES-128-ECB (PKCS7 padding). */\nexport function decryptAesEcb(ciphertext: Buffer, key: Buffer): Buffer {\n  const decipher = createDecipheriv(\"aes-128-ecb\", key, null);\n  return Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n}\n\n/** Compute AES-128-ECB ciphertext size (PKCS7 pads to 16-byte boundary). */\nexport function aesEcbPaddedSize(plaintextSize: number): number {\n  return Math.ceil((plaintextSize + 1) / 16) * 16;\n}\n","/**\n * CDN URL construction for WeChat CDN upload/download.\n */\n\n/** Build a CDN download URL from encrypt_query_param. */\nexport function buildCdnDownloadUrl(\n  encryptedQueryParam: string,\n  cdnBaseUrl: string,\n): string {\n  return `${cdnBaseUrl}/download?encrypted_query_param=${encodeURIComponent(encryptedQueryParam)}`;\n}\n\n/** Build a CDN upload URL from upload_param and filekey. */\nexport function buildCdnUploadUrl(params: {\n  cdnBaseUrl: string;\n  uploadParam: string;\n  filekey: string;\n}): string {\n  return `${params.cdnBaseUrl}/upload?encrypted_query_param=${encodeURIComponent(params.uploadParam)}&filekey=${encodeURIComponent(params.filekey)}`;\n}\n","/**\n * Download and optionally decrypt media from the WeChat CDN.\n */\nimport { decryptAesEcb } from \"./aes-ecb.js\";\nimport { buildCdnDownloadUrl } from \"./cdn-url.js\";\n\n/**\n * Download raw bytes from the CDN (no decryption).\n */\nasync function fetchCdnBytes(url: string): Promise<Buffer> {\n  const res = await fetch(url);\n  if (!res.ok) {\n    const body = await res.text().catch(() => \"(unreadable)\");\n    throw new Error(`CDN download ${res.status} ${res.statusText}: ${body}`);\n  }\n  return Buffer.from(await res.arrayBuffer());\n}\n\n/**\n * Parse CDNMedia.aes_key into a raw 16-byte AES key.\n *\n * Two encodings are observed:\n *   - base64(raw 16 bytes)           -> images (aes_key from media field)\n *   - base64(hex string of 16 bytes) -> file / voice / video\n *\n * In the second case, base64-decoding yields 32 ASCII hex chars which must\n * then be parsed as hex to recover the actual 16-byte key.\n */\nexport function parseAesKey(aesKeyBase64: string): Buffer {\n  const decoded = Buffer.from(aesKeyBase64, \"base64\");\n  if (decoded.length === 16) {\n    return decoded;\n  }\n  if (\n    decoded.length === 32 &&\n    /^[0-9a-fA-F]{32}$/.test(decoded.toString(\"ascii\"))\n  ) {\n    return Buffer.from(decoded.toString(\"ascii\"), \"hex\");\n  }\n  throw new Error(\n    `aes_key must decode to 16 raw bytes or 32-char hex string, got ${decoded.length} bytes`,\n  );\n}\n\n/**\n * Download and AES-128-ECB decrypt a CDN media file. Returns plaintext Buffer.\n */\nexport async function downloadAndDecrypt(\n  encryptedQueryParam: string,\n  aesKeyBase64: string,\n  cdnBaseUrl: string,\n): Promise<Buffer> {\n  const key = parseAesKey(aesKeyBase64);\n  const url = buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl);\n  const encrypted = await fetchCdnBytes(url);\n  return decryptAesEcb(encrypted, key);\n}\n\n/**\n * Download plain (unencrypted) bytes from the CDN.\n */\nexport async function downloadPlain(\n  encryptedQueryParam: string,\n  cdnBaseUrl: string,\n): Promise<Buffer> {\n  const url = buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl);\n  return fetchCdnBytes(url);\n}\n","/**\n * High-level media download from inbound messages.\n *\n * Downloads and decrypts CDN media referenced in MessageItem fields.\n */\nimport type { MessageItem } from \"../api/types.js\";\nimport { MessageItemType } from \"../api/types.js\";\nimport {\n  downloadAndDecrypt,\n  downloadPlain,\n} from \"../cdn/cdn-download.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface DownloadedMedia {\n  /** Decrypted file content. */\n  data: Buffer;\n  /** Media type hint: \"image\", \"voice\", \"file\", \"video\". */\n  kind: \"image\" | \"voice\" | \"file\" | \"video\";\n  /** Original filename (file items only). */\n  fileName?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Download and decrypt media from a single MessageItem.\n * Returns null if the item has no downloadable media.\n */\nexport async function downloadMediaFromItem(\n  item: MessageItem,\n  cdnBaseUrl: string,\n): Promise<DownloadedMedia | null> {\n  if (item.type === MessageItemType.IMAGE) {\n    const img = item.image_item;\n    if (!img?.media?.encrypt_query_param) return null;\n    // Prefer hex aeskey from image_item, fall back to media.aes_key\n    const aesKeyBase64 = img.aeskey\n      ? Buffer.from(img.aeskey, \"hex\").toString(\"base64\")\n      : img.media.aes_key;\n\n    const data = aesKeyBase64\n      ? await downloadAndDecrypt(\n          img.media.encrypt_query_param,\n          aesKeyBase64,\n          cdnBaseUrl,\n        )\n      : await downloadPlain(\n          img.media.encrypt_query_param,\n          cdnBaseUrl,\n        );\n    return { data, kind: \"image\" };\n  }\n\n  if (item.type === MessageItemType.VOICE) {\n    const voice = item.voice_item;\n    if (\n      !voice?.media?.encrypt_query_param ||\n      !voice.media.aes_key\n    )\n      return null;\n    const data = await downloadAndDecrypt(\n      voice.media.encrypt_query_param,\n      voice.media.aes_key,\n      cdnBaseUrl,\n    );\n    return { data, kind: \"voice\" };\n  }\n\n  if (item.type === MessageItemType.FILE) {\n    const fileItem = item.file_item;\n    if (\n      !fileItem?.media?.encrypt_query_param ||\n      !fileItem.media.aes_key\n    )\n      return null;\n    const data = await downloadAndDecrypt(\n      fileItem.media.encrypt_query_param,\n      fileItem.media.aes_key,\n      cdnBaseUrl,\n    );\n    return {\n      data,\n      kind: \"file\",\n      fileName: fileItem.file_name ?? undefined,\n    };\n  }\n\n  if (item.type === MessageItemType.VIDEO) {\n    const videoItem = item.video_item;\n    if (\n      !videoItem?.media?.encrypt_query_param ||\n      !videoItem.media.aes_key\n    )\n      return null;\n    const data = await downloadAndDecrypt(\n      videoItem.media.encrypt_query_param,\n      videoItem.media.aes_key,\n      cdnBaseUrl,\n    );\n    return { data, kind: \"video\" };\n  }\n\n  return null;\n}\n","import path from \"node:path\";\n\nconst EXTENSION_TO_MIME: Record<string, string> = {\n  \".pdf\": \"application/pdf\",\n  \".doc\": \"application/msword\",\n  \".docx\": \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n  \".xls\": \"application/vnd.ms-excel\",\n  \".xlsx\": \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n  \".ppt\": \"application/vnd.ms-powerpoint\",\n  \".pptx\": \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n  \".txt\": \"text/plain\",\n  \".csv\": \"text/csv\",\n  \".zip\": \"application/zip\",\n  \".tar\": \"application/x-tar\",\n  \".gz\": \"application/gzip\",\n  \".mp3\": \"audio/mpeg\",\n  \".ogg\": \"audio/ogg\",\n  \".wav\": \"audio/wav\",\n  \".mp4\": \"video/mp4\",\n  \".mov\": \"video/quicktime\",\n  \".webm\": \"video/webm\",\n  \".mkv\": \"video/x-matroska\",\n  \".avi\": \"video/x-msvideo\",\n  \".png\": \"image/png\",\n  \".jpg\": \"image/jpeg\",\n  \".jpeg\": \"image/jpeg\",\n  \".gif\": \"image/gif\",\n  \".webp\": \"image/webp\",\n  \".bmp\": \"image/bmp\",\n};\n\nconst MIME_TO_EXTENSION: Record<string, string> = {\n  \"image/jpeg\": \".jpg\",\n  \"image/jpg\": \".jpg\",\n  \"image/png\": \".png\",\n  \"image/gif\": \".gif\",\n  \"image/webp\": \".webp\",\n  \"image/bmp\": \".bmp\",\n  \"video/mp4\": \".mp4\",\n  \"video/quicktime\": \".mov\",\n  \"video/webm\": \".webm\",\n  \"video/x-matroska\": \".mkv\",\n  \"video/x-msvideo\": \".avi\",\n  \"audio/mpeg\": \".mp3\",\n  \"audio/ogg\": \".ogg\",\n  \"audio/wav\": \".wav\",\n  \"application/pdf\": \".pdf\",\n  \"application/zip\": \".zip\",\n  \"application/x-tar\": \".tar\",\n  \"application/gzip\": \".gz\",\n  \"text/plain\": \".txt\",\n  \"text/csv\": \".csv\",\n};\n\n/** Get MIME type from filename extension. Defaults to \"application/octet-stream\". */\nexport function getMimeFromFilename(filename: string): string {\n  const ext = path.extname(filename).toLowerCase();\n  return EXTENSION_TO_MIME[ext] ?? \"application/octet-stream\";\n}\n\n/** Get file extension from MIME type. Defaults to \".bin\". */\nexport function getExtensionFromMime(mimeType: string): string {\n  const ct = mimeType.split(\";\")[0].trim().toLowerCase();\n  return MIME_TO_EXTENSION[ct] ?? \".bin\";\n}\n\n/** Get file extension from Content-Type header or URL path. Defaults to \".bin\". */\nexport function getExtensionFromContentTypeOrUrl(\n  contentType: string | null,\n  url: string,\n): string {\n  if (contentType) {\n    const ext = getExtensionFromMime(contentType);\n    if (ext !== \".bin\") return ext;\n  }\n  const ext = path.extname(new URL(url).pathname).toLowerCase();\n  const knownExts = new Set(Object.keys(EXTENSION_TO_MIME));\n  return knownExts.has(ext) ? ext : \".bin\";\n}\n","/**\n * Upload encrypted media to the WeChat CDN.\n */\nimport { encryptAesEcb } from \"./aes-ecb.js\";\nimport { buildCdnUploadUrl } from \"./cdn-url.js\";\n\nconst UPLOAD_MAX_RETRIES = 3;\n\n/**\n * Upload one buffer to the WeChat CDN with AES-128-ECB encryption.\n * Returns the download encrypted_query_param from the CDN `x-encrypted-param` header.\n */\nexport async function uploadBufferToCdn(params: {\n  buf: Buffer;\n  uploadParam: string;\n  filekey: string;\n  cdnBaseUrl: string;\n  aeskey: Buffer;\n}): Promise<{ downloadParam: string }> {\n  const { buf, uploadParam, filekey, cdnBaseUrl, aeskey } = params;\n  const ciphertext = encryptAesEcb(buf, aeskey);\n  const cdnUrl = buildCdnUploadUrl({ cdnBaseUrl, uploadParam, filekey });\n\n  let downloadParam: string | undefined;\n  let lastError: unknown;\n\n  for (let attempt = 1; attempt <= UPLOAD_MAX_RETRIES; attempt++) {\n    try {\n      const res = await fetch(cdnUrl, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/octet-stream\" },\n        body: new Uint8Array(ciphertext),\n      });\n\n      if (res.status >= 400 && res.status < 500) {\n        const errMsg =\n          res.headers.get(\"x-error-message\") ?? (await res.text());\n        throw new Error(`CDN upload client error ${res.status}: ${errMsg}`);\n      }\n      if (res.status !== 200) {\n        const errMsg =\n          res.headers.get(\"x-error-message\") ?? `status ${res.status}`;\n        throw new Error(`CDN upload server error: ${errMsg}`);\n      }\n\n      downloadParam = res.headers.get(\"x-encrypted-param\") ?? undefined;\n      if (!downloadParam) {\n        throw new Error(\n          \"CDN upload response missing x-encrypted-param header\",\n        );\n      }\n      break;\n    } catch (err) {\n      lastError = err;\n      if (\n        err instanceof Error &&\n        err.message.includes(\"client error\")\n      ) {\n        throw err;\n      }\n      if (attempt >= UPLOAD_MAX_RETRIES) {\n        break;\n      }\n    }\n  }\n\n  if (!downloadParam) {\n    throw lastError instanceof Error\n      ? lastError\n      : new Error(\n          `CDN upload failed after ${UPLOAD_MAX_RETRIES} attempts`,\n        );\n  }\n  return { downloadParam };\n}\n","/**\n * High-level media upload pipeline for the WeChat CDN.\n *\n * Flow:\n *   1. Read file -> compute MD5, plaintext size, ciphertext size\n *   2. Generate random 16-byte AES key and filekey\n *   3. Call getUploadUrl to get upload_param\n *   4. Encrypt with AES-128-ECB and POST to CDN\n *   5. Return uploaded file info (download param, key, sizes)\n */\nimport crypto from \"node:crypto\";\nimport fs from \"node:fs/promises\";\n\nimport type { ApiClient } from \"../api/client.js\";\nimport { UploadMediaType } from \"../api/types.js\";\nimport { aesEcbPaddedSize } from \"../cdn/aes-ecb.js\";\nimport { uploadBufferToCdn } from \"../cdn/cdn-upload.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface UploadedFileInfo {\n  filekey: string;\n  /** CDN download encrypted_query_param (fill into CDNMedia.encrypt_query_param). */\n  downloadEncryptedQueryParam: string;\n  /** AES-128-ECB key, hex-encoded; convert to base64 for CDNMedia.aes_key. */\n  aeskey: string;\n  /** Plaintext file size in bytes. */\n  fileSize: number;\n  /** Ciphertext file size in bytes (after AES-128-ECB + PKCS7). */\n  fileSizeCiphertext: number;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helper\n// ---------------------------------------------------------------------------\n\nasync function uploadMedia(params: {\n  filePath: string;\n  toUserId: string;\n  api: ApiClient;\n  cdnBaseUrl: string;\n  mediaType: (typeof UploadMediaType)[keyof typeof UploadMediaType];\n}): Promise<UploadedFileInfo> {\n  const { filePath, toUserId, api, cdnBaseUrl, mediaType } = params;\n\n  const plaintext = await fs.readFile(filePath);\n  const rawsize = plaintext.length;\n  const rawfilemd5 = crypto\n    .createHash(\"md5\")\n    .update(plaintext)\n    .digest(\"hex\");\n  const filesize = aesEcbPaddedSize(rawsize);\n  const filekey = crypto.randomBytes(16).toString(\"hex\");\n  const aeskey = crypto.randomBytes(16);\n\n  const uploadUrlResp = await api.getUploadUrl({\n    filekey,\n    media_type: mediaType,\n    to_user_id: toUserId,\n    rawsize,\n    rawfilemd5,\n    filesize,\n    no_need_thumb: true,\n    aeskey: aeskey.toString(\"hex\"),\n  });\n\n  const uploadParam = uploadUrlResp.upload_param;\n  if (!uploadParam) {\n    throw new Error(\n      `getUploadUrl returned no upload_param: ${JSON.stringify(uploadUrlResp)}`,\n    );\n  }\n\n  const { downloadParam } = await uploadBufferToCdn({\n    buf: plaintext,\n    uploadParam,\n    filekey,\n    cdnBaseUrl,\n    aeskey,\n  });\n\n  return {\n    filekey,\n    downloadEncryptedQueryParam: downloadParam,\n    aeskey: aeskey.toString(\"hex\"),\n    fileSize: rawsize,\n    fileSizeCiphertext: filesize,\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Upload a local image file to the WeChat CDN. */\nexport async function uploadImage(params: {\n  filePath: string;\n  toUserId: string;\n  api: ApiClient;\n  cdnBaseUrl: string;\n}): Promise<UploadedFileInfo> {\n  return uploadMedia({ ...params, mediaType: UploadMediaType.IMAGE });\n}\n\n/** Upload a local video file to the WeChat CDN. */\nexport async function uploadVideo(params: {\n  filePath: string;\n  toUserId: string;\n  api: ApiClient;\n  cdnBaseUrl: string;\n}): Promise<UploadedFileInfo> {\n  return uploadMedia({ ...params, mediaType: UploadMediaType.VIDEO });\n}\n\n/** Upload a local file attachment to the WeChat CDN. */\nexport async function uploadFile(params: {\n  filePath: string;\n  toUserId: string;\n  api: ApiClient;\n  cdnBaseUrl: string;\n}): Promise<UploadedFileInfo> {\n  return uploadMedia({ ...params, mediaType: UploadMediaType.FILE });\n}\n","import crypto from \"node:crypto\";\n\n/**\n * Generate a prefixed unique ID: `{prefix}:{timestamp}-{8-char hex}`.\n */\nexport function generateId(prefix: string): string {\n  return `${prefix}:${Date.now()}-${crypto.randomBytes(4).toString(\"hex\")}`;\n}\n\n/**\n * Generate a temporary file name: `{prefix}-{timestamp}-{8-char hex}{ext}`.\n */\nexport function tempFileName(prefix: string, ext: string): string {\n  return `${prefix}-${Date.now()}-${crypto.randomBytes(4).toString(\"hex\")}${ext}`;\n}\n","/**\n * High-level message sending helpers.\n *\n * Builds SendMessageReq payloads for text, image, video, and file messages,\n * and dispatches them through the ApiClient.\n */\nimport path from \"node:path\";\n\nimport type { ApiClient } from \"../api/client.js\";\nimport type {\n  MessageItem,\n  SendMessageReq,\n} from \"../api/types.js\";\nimport {\n  MessageItemType,\n  MessageState,\n  MessageType,\n} from \"../api/types.js\";\nimport { getMimeFromFilename } from \"../util/mime.js\";\nimport type { UploadedFileInfo } from \"./upload.js\";\nimport { uploadImage, uploadVideo, uploadFile } from \"./upload.js\";\nimport { generateId } from \"../util/random.js\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction generateClientId(): string {\n  return generateId(\"wechat-ilink\");\n}\n\nfunction buildReq(params: {\n  to: string;\n  contextToken?: string;\n  items: MessageItem[];\n}): SendMessageReq {\n  return {\n    msg: {\n      from_user_id: \"\",\n      to_user_id: params.to,\n      client_id: generateClientId(),\n      message_type: MessageType.BOT,\n      message_state: MessageState.FINISH,\n      item_list: params.items.length ? params.items : undefined,\n      context_token: params.contextToken ?? undefined,\n    },\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Public send helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Send a text message. contextToken is required (echoed from getUpdates).\n */\nexport async function sendText(\n  api: ApiClient,\n  to: string,\n  text: string,\n  contextToken: string,\n): Promise<string> {\n  const clientId = generateClientId();\n  const req: SendMessageReq = {\n    msg: {\n      from_user_id: \"\",\n      to_user_id: to,\n      client_id: clientId,\n      message_type: MessageType.BOT,\n      message_state: MessageState.FINISH,\n      item_list: text\n        ? [{ type: MessageItemType.TEXT, text_item: { text } }]\n        : undefined,\n      context_token: contextToken,\n    },\n  };\n  await api.sendMessage(req);\n  return clientId;\n}\n\n/**\n * Send an image message with a previously uploaded file.\n */\nexport async function sendImage(\n  api: ApiClient,\n  to: string,\n  uploaded: UploadedFileInfo,\n  contextToken: string,\n  caption?: string,\n): Promise<string> {\n  const items: MessageItem[] = [];\n  if (caption) {\n    items.push({\n      type: MessageItemType.TEXT,\n      text_item: { text: caption },\n    });\n  }\n  items.push({\n    type: MessageItemType.IMAGE,\n    image_item: {\n      media: {\n        encrypt_query_param: uploaded.downloadEncryptedQueryParam,\n        aes_key: Buffer.from(uploaded.aeskey).toString(\"base64\"),\n        encrypt_type: 1,\n      },\n      mid_size: uploaded.fileSizeCiphertext,\n    },\n  });\n\n  // Send each item as its own request (text first, then image)\n  let lastClientId = \"\";\n  for (const item of items) {\n    const req = buildReq({ to, contextToken, items: [item] });\n    lastClientId = req.msg?.client_id ?? lastClientId;\n    await api.sendMessage(req);\n  }\n  return lastClientId;\n}\n\n/**\n * Send a video message with a previously uploaded file.\n */\nexport async function sendVideo(\n  api: ApiClient,\n  to: string,\n  uploaded: UploadedFileInfo,\n  contextToken: string,\n  caption?: string,\n): Promise<string> {\n  const items: MessageItem[] = [];\n  if (caption) {\n    items.push({\n      type: MessageItemType.TEXT,\n      text_item: { text: caption },\n    });\n  }\n  items.push({\n    type: MessageItemType.VIDEO,\n    video_item: {\n      media: {\n        encrypt_query_param: uploaded.downloadEncryptedQueryParam,\n        aes_key: Buffer.from(uploaded.aeskey).toString(\"base64\"),\n        encrypt_type: 1,\n      },\n      video_size: uploaded.fileSizeCiphertext,\n    },\n  });\n\n  let lastClientId = \"\";\n  for (const item of items) {\n    const req = buildReq({ to, contextToken, items: [item] });\n    lastClientId = req.msg?.client_id ?? lastClientId;\n    await api.sendMessage(req);\n  }\n  return lastClientId;\n}\n\n/**\n * Send a file attachment with a previously uploaded file.\n */\nexport async function sendFileMessage(\n  api: ApiClient,\n  to: string,\n  fileName: string,\n  uploaded: UploadedFileInfo,\n  contextToken: string,\n  caption?: string,\n): Promise<string> {\n  const items: MessageItem[] = [];\n  if (caption) {\n    items.push({\n      type: MessageItemType.TEXT,\n      text_item: { text: caption },\n    });\n  }\n  items.push({\n    type: MessageItemType.FILE,\n    file_item: {\n      media: {\n        encrypt_query_param: uploaded.downloadEncryptedQueryParam,\n        aes_key: Buffer.from(uploaded.aeskey).toString(\"base64\"),\n        encrypt_type: 1,\n      },\n      file_name: fileName,\n      len: String(uploaded.fileSize),\n    },\n  });\n\n  let lastClientId = \"\";\n  for (const item of items) {\n    const req = buildReq({ to, contextToken, items: [item] });\n    lastClientId = req.msg?.client_id ?? lastClientId;\n    await api.sendMessage(req);\n  }\n  return lastClientId;\n}\n\n/**\n * Upload and send a local file as a media message. Routing by MIME type:\n *   - video/*  -> video message\n *   - image/*  -> image message\n *   - else     -> file attachment\n */\nexport async function sendMediaFile(\n  api: ApiClient,\n  to: string,\n  filePath: string,\n  contextToken: string,\n  caption?: string,\n): Promise<string> {\n  const mime = getMimeFromFilename(filePath);\n  const cdnBaseUrl = api.cdnBaseUrl;\n\n  if (mime.startsWith(\"video/\")) {\n    const uploaded = await uploadVideo({\n      filePath,\n      toUserId: to,\n      api,\n      cdnBaseUrl,\n    });\n    return sendVideo(api, to, uploaded, contextToken, caption);\n  }\n\n  if (mime.startsWith(\"image/\")) {\n    const uploaded = await uploadImage({\n      filePath,\n      toUserId: to,\n      api,\n      cdnBaseUrl,\n    });\n    return sendImage(api, to, uploaded, contextToken, caption);\n  }\n\n  // File attachment\n  const fileName = path.basename(filePath);\n  const uploaded = await uploadFile({\n    filePath,\n    toUserId: to,\n    api,\n    cdnBaseUrl,\n  });\n  return sendFileMessage(\n    api,\n    to,\n    fileName,\n    uploaded,\n    contextToken,\n    caption,\n  );\n}\n","/**\n * Long-poll monitor loop.\n *\n * Continuously calls getUpdates and emits inbound messages via a callback.\n * Handles error backoff and session expiry.\n *\n * Sync buf (cursor) persistence is fully controlled by the caller via\n * optional `loadSyncBuf` / `saveSyncBuf` callbacks in MonitorOptions.\n */\nimport type { ApiClient } from \"./api/client.js\";\nimport type { WeixinMessage, GetUpdatesResp } from \"./api/types.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;\nconst MAX_CONSECUTIVE_FAILURES = 3;\nconst BACKOFF_DELAY_MS = 30_000;\nconst RETRY_DELAY_MS = 2_000;\n\n/** Error code returned by the server when the bot session has expired. */\nexport const SESSION_EXPIRED_ERRCODE = -14;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MonitorOptions {\n  /** Long-poll timeout in ms. Server may override via longpolling_timeout_ms. */\n  longPollTimeoutMs?: number;\n  /** AbortSignal for stopping the loop. */\n  signal?: AbortSignal;\n  /**\n   * Called once at startup to load a previously persisted sync cursor.\n   * Return the cursor string, or undefined/empty to start fresh.\n   */\n  loadSyncBuf?: () => string | undefined | Promise<string | undefined>;\n  /**\n   * Called after each successful getUpdates with the new cursor value.\n   * The caller can persist this for resume across restarts.\n   */\n  saveSyncBuf?: (buf: string) => void | Promise<void>;\n}\n\nexport interface MonitorCallbacks {\n  /** Called for each inbound message. */\n  onMessage: (msg: WeixinMessage) => void | Promise<void>;\n  /** Called when getUpdates returns an error response. */\n  onError?: (err: Error) => void;\n  /** Called when the bot session has expired (errcode -14). */\n  onSessionExpired?: () => void;\n  /** Called after each successful getUpdates response. */\n  onPoll?: (resp: GetUpdatesResp) => void;\n}\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n  return new Promise((resolve, reject) => {\n    const t = setTimeout(resolve, ms);\n    signal?.addEventListener(\n      \"abort\",\n      () => {\n        clearTimeout(t);\n        reject(new Error(\"aborted\"));\n      },\n      { once: true },\n    );\n  });\n}\n\n/**\n * Start the long-poll monitor loop. Runs until the AbortSignal fires.\n */\nexport async function startMonitor(\n  api: ApiClient,\n  opts: MonitorOptions,\n  callbacks: MonitorCallbacks,\n): Promise<void> {\n  const { signal } = opts;\n\n  // Load persisted cursor via caller-provided callback\n  let getUpdatesBuf = \"\";\n  if (opts.loadSyncBuf) {\n    const loaded = await opts.loadSyncBuf();\n    if (loaded) {\n      getUpdatesBuf = loaded;\n    }\n  }\n\n  let nextTimeoutMs =\n    opts.longPollTimeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;\n  let consecutiveFailures = 0;\n\n  while (!signal?.aborted) {\n    try {\n      const resp = await api.getUpdates(getUpdatesBuf, nextTimeoutMs);\n\n      // Server-suggested timeout\n      if (\n        resp.longpolling_timeout_ms != null &&\n        resp.longpolling_timeout_ms > 0\n      ) {\n        nextTimeoutMs = resp.longpolling_timeout_ms;\n      }\n\n      // Check for API errors\n      const isApiError =\n        (resp.ret !== undefined && resp.ret !== 0) ||\n        (resp.errcode !== undefined && resp.errcode !== 0);\n\n      if (isApiError) {\n        const isSessionExpired =\n          resp.errcode === SESSION_EXPIRED_ERRCODE ||\n          resp.ret === SESSION_EXPIRED_ERRCODE;\n\n        if (isSessionExpired) {\n          callbacks.onSessionExpired?.();\n          // Pause for 1 hour\n          await sleep(60 * 60 * 1000, signal);\n          consecutiveFailures = 0;\n          continue;\n        }\n\n        consecutiveFailures++;\n        callbacks.onError?.(\n          new Error(\n            `getUpdates failed: ret=${resp.ret} errcode=${resp.errcode} errmsg=${resp.errmsg ?? \"\"}`,\n          ),\n        );\n\n        if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {\n          consecutiveFailures = 0;\n          await sleep(BACKOFF_DELAY_MS, signal);\n        } else {\n          await sleep(RETRY_DELAY_MS, signal);\n        }\n        continue;\n      }\n\n      // Success\n      consecutiveFailures = 0;\n      callbacks.onPoll?.(resp);\n\n      // Persist cursor via caller-provided callback\n      if (\n        resp.get_updates_buf != null &&\n        resp.get_updates_buf !== \"\"\n      ) {\n        getUpdatesBuf = resp.get_updates_buf;\n        if (opts.saveSyncBuf) {\n          await opts.saveSyncBuf(getUpdatesBuf);\n        }\n      }\n\n      // Dispatch messages\n      const msgs = resp.msgs ?? [];\n      for (const msg of msgs) {\n        await callbacks.onMessage(msg);\n      }\n    } catch (err) {\n      if (signal?.aborted) return;\n      consecutiveFailures++;\n      callbacks.onError?.(\n        err instanceof Error\n          ? err\n          : new Error(String(err)),\n      );\n\n      if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {\n        consecutiveFailures = 0;\n        await sleep(BACKOFF_DELAY_MS, signal);\n      } else {\n        await sleep(RETRY_DELAY_MS, signal);\n      }\n    }\n  }\n}\n","/**\n * WeChatClient — high-level client for the WeChat iLink bot protocol.\n *\n * Wraps the low-level API, QR login, long-poll monitor, media upload/download,\n * and message sending into a single EventEmitter-based interface.\n *\n * This is a pure in-memory client. It does NOT persist any data to disk.\n * The caller is responsible for:\n *   - Storing/loading the token and accountId across restarts\n *   - Storing/loading the sync buf (long-poll cursor) for message resume\n *   - Rendering QR codes during login\n *\n * Usage:\n *   const client = new WeChatClient({ token, accountId });\n *   client.on(\"message\", (msg) => { ... });\n *   await client.start();\n */\nimport { EventEmitter } from \"node:events\";\n\nimport { ApiClient } from \"./api/client.js\";\nimport type { ApiClientOptions } from \"./api/client.js\";\nimport type {\n  WeixinMessage,\n  MessageItem,\n  GetUpdatesResp,\n  SendTypingReq,\n} from \"./api/types.js\";\nimport { MessageItemType, TypingStatus } from \"./api/types.js\";\nimport { loginWithQRCode } from \"./auth/qr-login.js\";\nimport type { LoginResult, QRLoginOptions } from \"./auth/qr-login.js\";\nimport { downloadMediaFromItem } from \"./media/download.js\";\nimport type { DownloadedMedia } from \"./media/download.js\";\nimport {\n  sendText,\n  sendImage,\n  sendVideo,\n  sendFileMessage,\n  sendMediaFile,\n} from \"./media/send.js\";\nimport type { UploadedFileInfo } from \"./media/upload.js\";\nimport {\n  uploadImage,\n  uploadVideo,\n  uploadFile,\n} from \"./media/upload.js\";\nimport { startMonitor } from \"./monitor.js\";\nimport type { MonitorOptions, MonitorCallbacks } from \"./monitor.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface WeChatClientOptions extends ApiClientOptions {\n  /** Account ID. Set after login if not provided. */\n  accountId?: string;\n}\n\nexport interface WeChatClientEvents {\n  message: [msg: WeixinMessage];\n  error: [err: Error];\n  sessionExpired: [];\n  poll: [resp: GetUpdatesResp];\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Extract text body from a message's item_list. */\nfunction extractTextBody(itemList?: MessageItem[]): string {\n  if (!itemList?.length) return \"\";\n  for (const item of itemList) {\n    if (\n      item.type === MessageItemType.TEXT &&\n      item.text_item?.text != null\n    ) {\n      return String(item.text_item.text);\n    }\n    // Voice-to-text\n    if (\n      item.type === MessageItemType.VOICE &&\n      item.voice_item?.text\n    ) {\n      return item.voice_item.text;\n    }\n  }\n  return \"\";\n}\n\n/**\n * Normalize a raw account ID (e.g. \"hex@im.bot\") to a safe key\n * (e.g. \"hex-im-bot\").\n */\nexport function normalizeAccountId(raw: string): string {\n  return raw.trim().toLowerCase().replace(/[@.]/g, \"-\");\n}\n\n// ---------------------------------------------------------------------------\n// Client\n// ---------------------------------------------------------------------------\n\nexport class WeChatClient extends EventEmitter {\n  readonly api: ApiClient;\n  private accountId?: string;\n  private abortController?: AbortController;\n\n  /** In-process cache: userId -> contextToken (echoed from getUpdates). */\n  private contextTokens = new Map<string, string>();\n\n  constructor(opts: WeChatClientOptions = {}) {\n    super();\n    this.api = new ApiClient(opts);\n    this.accountId = opts.accountId;\n  }\n\n  // -----------------------------------------------------------------------\n  // Getters / setters\n  // -----------------------------------------------------------------------\n\n  getAccountId(): string | undefined {\n    return this.accountId;\n  }\n\n  /** Get the cached context token for a user (needed for sending replies). */\n  getContextToken(userId: string): string | undefined {\n    return this.contextTokens.get(userId);\n  }\n\n  // -----------------------------------------------------------------------\n  // QR login\n  // -----------------------------------------------------------------------\n\n  /**\n   * Run the QR code login flow. On success, configures the API client\n   * with the new token and sets the accountId.\n   *\n   * The library does NOT render QR codes. Use `opts.onQRCode` to receive\n   * the QR code URL and handle display yourself.\n   *\n   * The library does NOT persist credentials. The caller should save\n   * `result.botToken`, `result.accountId`, and `result.baseUrl` themselves.\n   */\n  async login(opts: QRLoginOptions = {}): Promise<LoginResult> {\n    const result = await loginWithQRCode(this.api, opts);\n\n    if (result.connected && result.botToken && result.accountId) {\n      this.accountId = normalizeAccountId(result.accountId);\n      this.api.setToken(result.botToken);\n    }\n\n    return result;\n  }\n\n  // -----------------------------------------------------------------------\n  // Long-poll monitor\n  // -----------------------------------------------------------------------\n\n  /**\n   * Start the long-poll monitor loop. Emits \"message\" events for each\n   * inbound message.\n   *\n   * Sync buf persistence is opt-in via `opts.loadSyncBuf` / `opts.saveSyncBuf`.\n   *\n   * Call `stop()` to terminate.\n   */\n  async start(opts: Omit<MonitorOptions, \"accountId\"> = {}): Promise<void> {\n    if (!this.accountId) {\n      throw new Error(\n        \"No accountId set. Call login() first or pass accountId in constructor.\",\n      );\n    }\n    if (!this.api.getToken()) {\n      throw new Error(\n        \"No token set. Call login() first or pass token in constructor options.\",\n      );\n    }\n\n    this.abortController = new AbortController();\n\n    const monitorOpts: MonitorOptions = {\n      signal: this.abortController.signal,\n      ...opts,\n    };\n\n    const callbacks: MonitorCallbacks = {\n      onMessage: async (msg) => {\n        // Cache context_token\n        if (msg.context_token && msg.from_user_id) {\n          this.contextTokens.set(\n            msg.from_user_id,\n            msg.context_token,\n          );\n        }\n        this.emit(\"message\", msg);\n      },\n      onError: (err) => {\n        this.emit(\"error\", err);\n      },\n      onSessionExpired: () => {\n        this.emit(\"sessionExpired\");\n      },\n      onPoll: (resp) => {\n        this.emit(\"poll\", resp);\n      },\n    };\n\n    await startMonitor(this.api, monitorOpts, callbacks);\n  }\n\n  /** Stop the long-poll monitor loop. */\n  stop(): void {\n    this.abortController?.abort();\n    this.abortController = undefined;\n  }\n\n  // -----------------------------------------------------------------------\n  // Sending messages\n  // -----------------------------------------------------------------------\n\n  /**\n   * Send a text message. Uses the cached context token for the target user.\n   * Pass an explicit contextToken to override.\n   */\n  async sendText(\n    to: string,\n    text: string,\n    contextToken?: string,\n  ): Promise<string> {\n    const ct =\n      contextToken ?? this.contextTokens.get(to);\n    if (!ct) {\n      throw new Error(\n        `No context_token for user ${to}. Receive a message from them first.`,\n      );\n    }\n    return sendText(this.api, to, text, ct);\n  }\n\n  /**\n   * Upload a local file and send it as the appropriate media type.\n   * (image/*, video/*, or file attachment based on MIME type.)\n   */\n  async sendMedia(\n    to: string,\n    filePath: string,\n    caption?: string,\n    contextToken?: string,\n  ): Promise<string> {\n    const ct =\n      contextToken ?? this.contextTokens.get(to);\n    if (!ct) {\n      throw new Error(\n        `No context_token for user ${to}. Receive a message from them first.`,\n      );\n    }\n    return sendMediaFile(this.api, to, filePath, ct, caption);\n  }\n\n  /**\n   * Send an already-uploaded image.\n   */\n  async sendUploadedImage(\n    to: string,\n    uploaded: UploadedFileInfo,\n    caption?: string,\n    contextToken?: string,\n  ): Promise<string> {\n    const ct =\n      contextToken ?? this.contextTokens.get(to);\n    if (!ct) {\n      throw new Error(\n        `No context_token for user ${to}.`,\n      );\n    }\n    return sendImage(this.api, to, uploaded, ct, caption);\n  }\n\n  /**\n   * Send an already-uploaded video.\n   */\n  async sendUploadedVideo(\n    to: string,\n    uploaded: UploadedFileInfo,\n    caption?: string,\n    contextToken?: string,\n  ): Promise<string> {\n    const ct =\n      contextToken ?? this.contextTokens.get(to);\n    if (!ct) {\n      throw new Error(\n        `No context_token for user ${to}.`,\n      );\n    }\n    return sendVideo(this.api, to, uploaded, ct, caption);\n  }\n\n  /**\n   * Send an already-uploaded file attachment.\n   */\n  async sendUploadedFile(\n    to: string,\n    fileName: string,\n    uploaded: UploadedFileInfo,\n    caption?: string,\n    contextToken?: string,\n  ): Promise<string> {\n    const ct =\n      contextToken ?? this.contextTokens.get(to);\n    if (!ct) {\n      throw new Error(\n        `No context_token for user ${to}.`,\n      );\n    }\n    return sendFileMessage(\n      this.api,\n      to,\n      fileName,\n      uploaded,\n      ct,\n      caption,\n    );\n  }\n\n  // -----------------------------------------------------------------------\n  // Typing indicator\n  // -----------------------------------------------------------------------\n\n  /**\n   * Send a \"typing\" indicator to the user. Requires a typing_ticket\n   * (obtained from getConfig).\n   */\n  async sendTyping(\n    userId: string,\n    typingTicket: string,\n    status: \"typing\" | \"cancel\" = \"typing\",\n  ): Promise<void> {\n    const req: SendTypingReq = {\n      ilink_user_id: userId,\n      typing_ticket: typingTicket,\n      status:\n        status === \"typing\"\n          ? TypingStatus.TYPING\n          : TypingStatus.CANCEL,\n    };\n    await this.api.sendTyping(req);\n  }\n\n  /**\n   * Get the typing ticket for a user (calls getConfig).\n   */\n  async getTypingTicket(\n    userId: string,\n    contextToken?: string,\n  ): Promise<string> {\n    const resp = await this.api.getConfig(userId, contextToken);\n    return resp.typing_ticket ?? \"\";\n  }\n\n  // -----------------------------------------------------------------------\n  // Media upload helpers\n  // -----------------------------------------------------------------------\n\n  async uploadImage(\n    filePath: string,\n    toUserId: string,\n  ): Promise<UploadedFileInfo> {\n    return uploadImage({\n      filePath,\n      toUserId,\n      api: this.api,\n      cdnBaseUrl: this.api.cdnBaseUrl,\n    });\n  }\n\n  async uploadVideo(\n    filePath: string,\n    toUserId: string,\n  ): Promise<UploadedFileInfo> {\n    return uploadVideo({\n      filePath,\n      toUserId,\n      api: this.api,\n      cdnBaseUrl: this.api.cdnBaseUrl,\n    });\n  }\n\n  async uploadFile(\n    filePath: string,\n    toUserId: string,\n  ): Promise<UploadedFileInfo> {\n    return uploadFile({\n      filePath,\n      toUserId,\n      api: this.api,\n      cdnBaseUrl: this.api.cdnBaseUrl,\n    });\n  }\n\n  // -----------------------------------------------------------------------\n  // Media download\n  // -----------------------------------------------------------------------\n\n  /**\n   * Download and decrypt a media item from an inbound message.\n   */\n  async downloadMedia(\n    item: MessageItem,\n  ): Promise<DownloadedMedia | null> {\n    return downloadMediaFromItem(item, this.api.cdnBaseUrl);\n  }\n\n  // -----------------------------------------------------------------------\n  // Static helpers\n  // -----------------------------------------------------------------------\n\n  /** Extract the text body from a WeixinMessage. */\n  static extractText(msg: WeixinMessage): string {\n    return extractTextBody(msg.item_list);\n  }\n\n  /** Check if a message item is a media type. */\n  static isMediaItem(item: MessageItem): boolean {\n    return (\n      item.type === MessageItemType.IMAGE ||\n      item.type === MessageItemType.VIDEO ||\n      item.type === MessageItemType.FILE ||\n      item.type === MessageItemType.VOICE\n    );\n  }\n}\n"],"mappings":"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"}