tca-js 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,6 +25,7 @@ Read and send TikTok direct messages, react, and receive new messages in real ti
25
25
  - [API Reference](#api-reference)
26
26
  - [Listener](#listener)
27
27
  - [Error Handling](#error-handling)
28
+ - [Examples](#examples)
28
29
  - [Contributing](#contributing)
29
30
  - [Support Us](#support-us)
30
31
  - [License](#license)
@@ -38,7 +39,7 @@ Read and send TikTok direct messages, react, and receive new messages in real ti
38
39
  - ❤️ Read and set reactions, mark conversations as read
39
40
  - 🔐 Cookie-based session — no browser at runtime
40
41
  - 📱 Optional QR-code login
41
- - 📦 Written in TypeScript, ships type definitions, one runtime dependency (`ws`)
42
+ - 📦 Written in TypeScript, ships ESM + CommonJS builds with type definitions, one runtime dependency (`ws`)
42
43
 
43
44
  ## Installation
44
45
 
@@ -46,7 +47,14 @@ Read and send TikTok direct messages, react, and receive new messages in real ti
46
47
  npm install tca-js
47
48
  ```
48
49
 
49
- Requires **Node.js 22+**. QR login also needs a local Chrome or Chromium; cookie login does not.
50
+ Requires **Node.js 22+**. Both ESM and CommonJS are supported:
51
+
52
+ ```ts
53
+ import { TikTok } from 'tca-js'; // ESM / TypeScript
54
+ const { TikTok } = require('tca-js'); // CommonJS
55
+ ```
56
+
57
+ QR login also needs a local Chrome or Chromium; cookie login does not.
50
58
 
51
59
  ## Quick Start
52
60
 
@@ -70,9 +78,9 @@ api.listener.start();
70
78
  Log in with the cookies of a `tiktok.com` session. Accepted formats:
71
79
 
72
80
  ```ts
73
- await new TikTok().login({ cookies: [{ name: 'sessionid', value: '…' }, /* … */] }); // array
74
- await new TikTok().login({ cookies: { sessionid: '…', ttwid: '…' } }); // object
75
- await new TikTok().login({ cookies: 'sessionid=…; ttwid=…' }); // header string
81
+ await new TikTok().login({ cookies: [{ name: 'sessionid', value: '…' } /* … */] }); // array
82
+ await new TikTok().login({ cookies: { sessionid: '…', ttwid: '…' } }); // object
83
+ await new TikTok().login({ cookies: 'sessionid=…; ttwid=…' }); // header string
76
84
  ```
77
85
 
78
86
  Required cookies: `sessionid`, `sid_tt`, `sid_guard`, `uid_tt`, `ttwid`, `odin_tt`, `store-idc`, `tt-target-idc`.
@@ -87,7 +95,6 @@ scanned and confirmed on the phone.
87
95
  import { loginQR, TikTok } from 'tca-js';
88
96
 
89
97
  const credentials = await loginQR({
90
- chromePath: '/usr/bin/chromium-browser',
91
98
  onEvent: (e) => {
92
99
  if (e.type === 'qr') showQr(e.image); // data:image/png;base64,…
93
100
  },
@@ -97,14 +104,14 @@ const api = await new TikTok().login(credentials);
97
104
  // Save `credentials` to skip scanning next time.
98
105
  ```
99
106
 
100
- | Option | Type | Default | Description |
101
- |---|---|---|---|
102
- | `chromePath` | `string` | — | Path to Chrome / Chromium |
103
- | `onEvent` | `(e) => void` | — | `qr` · `scanned` · `confirmed` |
104
- | `signal` | `AbortSignal` | — | Cancel the login |
105
- | `timeoutMs` | `number` | `300000` | Total time to wait for a scan |
106
- | `maxRefresh` | `number` | `3` | Max QR regenerations after expiry |
107
- | `userAgent` | `string` | — | Browser user agent |
107
+ | Option | Type | Default | Description |
108
+ | ------------ | ------------- | ----------- | ---------------------------------------------------------------------------------- |
109
+ | `chromePath` | `string` | auto-detect | Path to Chrome / Chromium (falls back to `CHROME_PATH`, then common install paths) |
110
+ | `onEvent` | `(e) => void` | — | `qr` · `scanned` · `confirmed` |
111
+ | `signal` | `AbortSignal` | — | Cancel the login |
112
+ | `timeoutMs` | `number` | `300000` | Total time to wait for a scan |
113
+ | `maxRefresh` | `number` | `3` | Max QR regenerations after expiry |
114
+ | `userAgent` | `string` | — | Browser user agent |
108
115
 
109
116
  ## API Reference
110
117
 
@@ -113,46 +120,46 @@ const api = await new TikTok().login(credentials);
113
120
 
114
121
  ### Conversations & messages
115
122
 
116
- | Method | Returns | Description |
117
- |---|---|---|
118
- | `getConversations(cursor?)` | `ConversationPage` | 10 conversations per page with their latest messages |
119
- | `getMessages(conv, { limit?, before? })` | `MessagePage` | Messages of a conversation, newest first |
120
- | `sendMessage(conv, text)` | `SendResult` | Send a text message |
121
- | `sendImage(conv, { data, filename })` | `SendResult` | Send an image (`Buffer`) |
122
- | `setReaction(msg, emoji \| null)` | `void` | Set your reaction; `null` removes it |
123
- | `markRead(msg)` | `void` | Mark the conversation as read up to `msg` |
123
+ | Method | Returns | Description |
124
+ | ---------------------------------------- | ------------------ | ---------------------------------------------------- |
125
+ | `getConversations(cursor?)` | `ConversationPage` | 10 conversations per page with their latest messages |
126
+ | `getMessages(conv, { limit?, before? })` | `MessagePage` | Messages of a conversation, newest first |
127
+ | `sendMessage(conv, text)` | `SendResult` | Send a text message |
128
+ | `sendImage(conv, { data, filename })` | `SendResult` | Send an image (`Buffer`) |
129
+ | `setReaction(msg, emoji \| null)` | `void` | Set your reaction; `null` removes it |
130
+ | `markRead(msg)` | `void` | Mark the conversation as read up to `msg` |
124
131
 
125
132
  `conv` is any object with `conversationId` and `conversationShortId` — every message satisfies it, so you can reply
126
133
  with `sendMessage(msg, …)`.
127
134
 
128
135
  ### Users & media
129
136
 
130
- | Method | Returns | Description |
131
- |---|---|---|
132
- | `getUsers(uids)` | `TikTokUser[]` | Nickname, `@username`, avatar |
133
- | `downloadImage(msg, 'origin' \| 'thumb')` | `Buffer` | Download and decrypt a private image |
134
- | `downloadSticker(sticker)` | `{ data, type }` | Download a sticker image |
135
- | `downloadAvatar(user)` | `{ data, type } \| null` | Download a user's avatar |
137
+ | Method | Returns | Description |
138
+ | ----------------------------------------- | ------------------------ | ------------------------------------ |
139
+ | `getUsers(uids)` | `TikTokUser[]` | Nickname, `@username`, avatar |
140
+ | `downloadImage(msg, 'origin' \| 'thumb')` | `Buffer` | Download and decrypt a private image |
141
+ | `downloadSticker(sticker)` | `{ data, type }` | Download a sticker image |
142
+ | `downloadAvatar(user)` | `{ data, type } \| null` | Download a user's avatar |
136
143
 
137
144
  ### Session
138
145
 
139
- | Method / property | Description |
140
- |---|---|
141
- | `account` | Logged-in account: `uid`, `username`, `screenName`, `avatar` |
142
- | `getOwnId()` | Own user id |
143
- | `getCookies()` | Current cookies — persist them to resume the session |
144
- | `isAlive()` | `true` while the session is still valid |
146
+ | Method / property | Description |
147
+ | ----------------- | ------------------------------------------------------------ |
148
+ | `account` | Logged-in account: `uid`, `username`, `screenName`, `avatar` |
149
+ | `getOwnId()` | Own user id |
150
+ | `getCookies()` | Current cookies — persist them to resume the session |
151
+ | `isAlive()` | `true` while the session is still valid |
145
152
 
146
153
  ### Message object
147
154
 
148
- | Field | Type | Description |
149
- |---|---|---|
150
- | `kind` | `'text' \| 'image' \| 'sticker' \| 'video' \| 'system' \| 'other'` | Message kind |
151
- | `text` | `string` | Text content |
152
- | `image` / `sticker` | `object \| null` | Attachment details |
153
- | `reactions` | `{ emoji, userIds }[]` | Reactions on the message |
154
- | `senderId` / `isSelf` | `string` / `boolean` | Sender |
155
- | `createdAt` | `number` | Epoch milliseconds |
155
+ | Field | Type | Description |
156
+ | --------------------- | ------------------------------------------------------------------ | ------------------------ |
157
+ | `kind` | `'text' \| 'image' \| 'sticker' \| 'video' \| 'system' \| 'other'` | Message kind |
158
+ | `text` | `string` | Text content |
159
+ | `image` / `sticker` | `object \| null` | Attachment details |
160
+ | `reactions` | `{ emoji, userIds }[]` | Reactions on the message |
161
+ | `senderId` / `isSelf` | `string` / `boolean` | Sender |
162
+ | `createdAt` | `number` | Epoch milliseconds |
156
163
 
157
164
  Full typings are shipped in `dist/index.d.ts`.
158
165
 
@@ -160,12 +167,16 @@ Full typings are shipped in `dist/index.d.ts`.
160
167
 
161
168
  ```ts
162
169
  api.listener.on('connected', () => console.log('connected'));
163
- api.listener.on('message', (msg) => { /* new message */ });
164
- api.listener.on('command', (msg) => { /* read receipts and other hidden commands */ });
170
+ api.listener.on('message', (msg) => {
171
+ /* new message */
172
+ });
173
+ api.listener.on('command', (msg) => {
174
+ /* read receipts and other hidden commands */
175
+ });
165
176
  api.listener.on('closed', (code, reason) => {});
166
177
  api.listener.on('error', (err) => {});
167
178
 
168
- api.listener.start(); // reconnects automatically; start({ retryOnClose: false }) to disable
179
+ api.listener.start(); // reconnects automatically; start({ retryOnClose: false }) to disable
169
180
  api.listener.stop();
170
181
  ```
171
182
 
@@ -173,13 +184,13 @@ api.listener.stop();
173
184
 
174
185
  All library errors are instances of `TikTokError` with a `code`:
175
186
 
176
- | Code | Meaning |
177
- |---|---|
178
- | `SESSION_EXPIRED` | Cookies are no longer valid — log in again |
179
- | `IM_ERROR` | TikTok rejected the request |
180
- | `HTTP_ERROR` · `BAD_RESPONSE` | Network or unexpected response |
181
- | `UPLOAD_FAILED` · `DOWNLOAD_FAILED` | Media transfer failed |
182
- | `LOGIN_FAILED` · `LOGIN_ABORTED` | QR login failed or was cancelled |
187
+ | Code | Meaning |
188
+ | ----------------------------------- | ------------------------------------------ |
189
+ | `SESSION_EXPIRED` | Cookies are no longer valid — log in again |
190
+ | `IM_ERROR` | TikTok rejected the request |
191
+ | `HTTP_ERROR` · `BAD_RESPONSE` | Network or unexpected response |
192
+ | `UPLOAD_FAILED` · `DOWNLOAD_FAILED` | Media transfer failed |
193
+ | `LOGIN_FAILED` · `LOGIN_ABORTED` | QR login failed or was cancelled |
183
194
 
184
195
  ```ts
185
196
  import { TikTokError } from 'tca-js';
@@ -193,12 +204,19 @@ try {
193
204
  }
194
205
  ```
195
206
 
207
+ ## Examples
208
+
209
+ - [`examples/qr-login.ts`](examples/qr-login.ts) — log in with a QR code and save the credentials to a file
210
+ - [`examples/echo-bot.ts`](examples/echo-bot.ts) — reply to every incoming text message
211
+
196
212
  ## Contributing
197
213
 
198
- Issues and pull requests are welcome. Protocol notes for maintainers are in [docs/PROTOCOL.md](docs/PROTOCOL.md).
214
+ Issues and pull requests are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Protocol notes for maintainers are in [docs/PROTOCOL.md](docs/PROTOCOL.md).
199
215
 
200
216
  ```bash
201
217
  npm install
218
+ npm test # unit tests (no network)
219
+ npm run lint && npm run typecheck
202
220
  npm run build
203
221
  node scripts/smoke.mjs --cookies cookies.json # read-only smoke test against a real account
204
222
  ```
@@ -0,0 +1,369 @@
1
+ import { EventEmitter } from 'node:events';
2
+
3
+ /**
4
+ * Minimal protobuf, no `.proto` file needed.
5
+ *
6
+ * TikTok doesn't publish its IM `.proto` — field numbers in `constants.ts` were reconstructed from bytes captured
7
+ * in the browser (09/11/2026). Only three wire types are needed: varint (0), length-delimited (2), and skipping 1/5.
8
+ */
9
+ interface Field {
10
+ n: number;
11
+ wire: number;
12
+ int: bigint;
13
+ bytes: Buffer;
14
+ }
15
+ /** Decoded message — read by field number. */
16
+ declare class Msg {
17
+ readonly fields: Field[];
18
+ private constructor();
19
+ static parse(b: Buffer): Msg;
20
+ static tryParse(b: Buffer | undefined): Msg | null;
21
+ has(n: number): boolean;
22
+ int(n: number): bigint;
23
+ num(n: number): number;
24
+ /** 64-bit integer as a string — uids / message IDs exceed `Number.MAX_SAFE_INTEGER`. */
25
+ id(n: number): string;
26
+ bytes(n: number): Buffer;
27
+ str(n: number): string;
28
+ strs(n: number): string[];
29
+ msg(n: number): Msg | null;
30
+ msgs(n: number): Msg[];
31
+ /** `map<string,string>` — each entry is a message `{1: key, 2: value}`. */
32
+ map(n: number): Record<string, string>;
33
+ }
34
+
35
+ /** A cookie — same shape as `chrome.cookies.getAll` / CDP `Storage.getCookies` returns (extra fields are fine). */
36
+ interface CookieInput {
37
+ name: string;
38
+ value: string;
39
+ }
40
+ /** What you need to log in again — save it after each login. */
41
+ interface Credentials {
42
+ /** `.tiktok.com` cookies: array of `{name, value}`, object `{name: value}`, or string `a=b; c=d`. */
43
+ cookies: CookieInput[] | Record<string, string> | string;
44
+ /** IM device id (`wid`) — leave empty and `login` asks TikTok using `ttwid`. Only used when that fails. */
45
+ deviceId?: string;
46
+ userAgent?: string;
47
+ }
48
+ interface TikTokOptions {
49
+ /** Debug logging — silent by default. */
50
+ logger?: (level: 'debug' | 'warn', msg: string) => void;
51
+ /** Timeout per HTTP request (ms). */
52
+ timeoutMs?: number;
53
+ }
54
+ /** Enough to target a conversation: send, read, mark as read. Every `TikTokMessage` satisfies it. */
55
+ interface ConversationRef {
56
+ conversationId: string;
57
+ conversationShortId: string;
58
+ }
59
+ interface TikTokImage {
60
+ /** Image id in TikTok storage (encrypted). */
61
+ uri: string;
62
+ /** Original download link — needs cookies, content is AES-256-GCM. */
63
+ url: string;
64
+ thumbUrl: string;
65
+ width: number;
66
+ height: number;
67
+ /** AES-256-GCM key (hex) to decrypt `url` — `api.downloadImage` handles this. */
68
+ decryptKey: string;
69
+ }
70
+ /** Sticker — including user-made stickers (`stickerType` 15). Public CDN link, signed + expires in a few days. */
71
+ interface TikTokSticker {
72
+ stickerId: string;
73
+ /** 14 = built-in TikTok sticker, 15 = user-made sticker (memes, AI…). */
74
+ stickerType: number;
75
+ url: string;
76
+ width: number;
77
+ height: number;
78
+ }
79
+ /** A reaction on a message: emoji + users who reacted (uid). The mobile app's `e:love` is merged into ❤️. */
80
+ interface TikTokReaction {
81
+ emoji: string;
82
+ userIds: string[];
83
+ }
84
+ interface TikTokMessage extends ConversationRef {
85
+ serverMessageId: string;
86
+ /** Position in the conversation (µs) — cursor for paging older messages. */
87
+ index: string;
88
+ /** `MessageType` — 7 text, 1802 image, 1805 sticker, 8 video share, 1 system. */
89
+ type: number;
90
+ /** `command` = hidden command (read receipt…) — listener emits it via the `command` event, not `message`. */
91
+ kind: 'text' | 'image' | 'sticker' | 'video' | 'system' | 'command' | 'other';
92
+ senderId: string;
93
+ isSelf: boolean;
94
+ /** Text content (`kind = text`), empty for other kinds. */
95
+ text: string;
96
+ image: TikTokImage | null;
97
+ sticker: TikTokSticker | null;
98
+ reactions: TikTokReaction[];
99
+ /** Raw JSON content (`{"aweType":0,"text":"…"}`…). */
100
+ content: unknown;
101
+ /** Send time, ms epoch. */
102
+ createdAt: number;
103
+ clientMessageId: string;
104
+ ext: Record<string, string>;
105
+ }
106
+ interface TikTokConversation extends ConversationRef {
107
+ participants: string[];
108
+ /** The other participant in a 1-1 conversation. */
109
+ peerId: string;
110
+ }
111
+ interface TikTokUser {
112
+ uid: string;
113
+ /** `@username` (without @). */
114
+ uniqueId: string;
115
+ nickname: string;
116
+ /** Best avatar (prefers jpeg/webp/png, avoids heic which browsers can't read). */
117
+ avatar: string;
118
+ /** All avatar links TikTok returns, by priority — signed, expiring links; `downloadAvatar` tries them in order. */
119
+ avatars: string[];
120
+ }
121
+ interface TikTokAccount {
122
+ uid: string;
123
+ username: string;
124
+ screenName: string;
125
+ avatar: string;
126
+ }
127
+ /** Image attachment. */
128
+ interface ImageInput {
129
+ data: Buffer;
130
+ /** Only the extension matters so TikTok knows the format: `photo.jpg`, `bill.png`. */
131
+ filename: string;
132
+ }
133
+ interface MessagePage {
134
+ messages: TikTokMessage[];
135
+ hasMore: boolean;
136
+ }
137
+ interface ConversationPage {
138
+ conversations: TikTokConversation[];
139
+ /** Latest messages included — handy for building an inbox list. */
140
+ messages: TikTokMessage[];
141
+ cursor: string;
142
+ hasMore: boolean;
143
+ }
144
+
145
+ /** Shared state of a session: cookies, device id, uid, HTTP calls. */
146
+ declare class Context {
147
+ readonly options: TikTokOptions;
148
+ readonly cookies: Map<string, string>;
149
+ /**
150
+ * IM device id = the `wid` TikTok issues for the `ttwid` cookie (`login` fetches it). The websocket only
151
+ * accepts this id — a made-up id gets 400 (HTTP IM isn't picky).
152
+ */
153
+ deviceId: string;
154
+ readonly userAgent: string;
155
+ ownId: string;
156
+ private seq;
157
+ constructor(creds: Credentials, options?: TikTokOptions);
158
+ cookieHeader(): string;
159
+ log(level: 'debug' | 'warn', msg: string): void;
160
+ /** `fetch` with cookies + UA + timeout. */
161
+ fetch(url: string, init?: RequestInit & {
162
+ headers?: Record<string, string>;
163
+ }): Promise<Response>;
164
+ getJson<T>(url: string): Promise<T>;
165
+ /** IM `Request` envelope: command + body + common headers. */
166
+ buildRequest(cmd: number, bodyField: number, body: Buffer): Buffer;
167
+ /**
168
+ * Call an IM command over HTTP. Returns the unwrapped body (`ResponseBody[cmd]`).
169
+ * NO X-Gnarly/X-Bogus/msToken signature needed — the IM server only checks cookies (tested 09/11/2026).
170
+ */
171
+ im(path: string, cmd: number, body: Buffer, bodyField?: number): Promise<Msg>;
172
+ }
173
+
174
+ /** Close code used when we call `stop()` ourselves. */
175
+ declare const MANUAL_CLOSE = 4000;
176
+ interface ListenerEvents {
177
+ connected: [];
178
+ /** A real message in a conversation (including our own — see `isSelf`). */
179
+ message: [TikTokMessage];
180
+ /** Hidden command: mark as read… (`content.command_type`). */
181
+ command: [TikTokMessage];
182
+ /** Close code + reason. Unless `MANUAL_CLOSE`, the listener reconnects automatically (if enabled). */
183
+ closed: [number, string];
184
+ error: [Error];
185
+ }
186
+ /**
187
+ * Listen for new messages via the `im-ws-sg` websocket (aid 1459) — cookies only.
188
+ * Events: `connected`, `message`, `closed`, `error`.
189
+ */
190
+ declare class Listener extends EventEmitter<ListenerEvents> {
191
+ private readonly ctx;
192
+ private ws;
193
+ private heartbeat;
194
+ private idle;
195
+ private reconnectTimer;
196
+ private backoff;
197
+ private retry;
198
+ private readonly seen;
199
+ constructor(ctx: Context);
200
+ get connected(): boolean;
201
+ url(): string;
202
+ /** Open the connection. `retryOnClose` (on by default): reconnect on drop, backing off up to 1 minute. */
203
+ start(opts?: {
204
+ retryOnClose?: boolean;
205
+ }): void;
206
+ stop(): void;
207
+ private connect;
208
+ private touch;
209
+ private clearTimers;
210
+ private onFrame;
211
+ }
212
+
213
+ /** Send result: client-side message id (TikTok echoes it back). The server id arrives via the listener (`isSelf`). */
214
+ interface SendResult {
215
+ clientMessageId: string;
216
+ }
217
+ /**
218
+ * A logged-in TikTok session — create with `new TikTok().login(credentials)`.
219
+ * All calls go directly over HTTP/WS to TikTok using cookies, no browser needed.
220
+ */
221
+ declare class API {
222
+ private readonly ctx;
223
+ readonly account: TikTokAccount;
224
+ readonly listener: Listener;
225
+ private readonly media;
226
+ constructor(ctx: Context, account: TikTokAccount);
227
+ getOwnId(): string;
228
+ /** Current cookies — save them so the next `login` needs no QR scan. */
229
+ getCookies(): {
230
+ name: string;
231
+ value: string;
232
+ }[];
233
+ getContext(): {
234
+ deviceId: string;
235
+ userAgent: string;
236
+ };
237
+ /** Whether the session is still alive — call periodically to detect expiry early. */
238
+ isAlive(): Promise<boolean>;
239
+ /** Recent conversations + their latest messages, paginated with `cursor`. */
240
+ getConversations(cursor?: string): Promise<ConversationPage>;
241
+ /** Messages of a conversation, newest first. For older messages: `before = <index of oldest message>`. */
242
+ getMessages(conv: ConversationRef, opts?: {
243
+ limit?: number;
244
+ before?: string;
245
+ }): Promise<MessagePage>;
246
+ /** User profiles by uid (display name, @username, avatar). */
247
+ getUsers(uids: string[]): Promise<TikTokUser[]>;
248
+ /**
249
+ * Download an avatar (tries each URL in turn, skips heic). No cookie / Referer sent: the image CDN is cross-origin and
250
+ * URLs are already signed. `null` if no URL could be downloaded.
251
+ */
252
+ downloadAvatar(user: Pick<TikTokUser, 'avatars'>): Promise<{
253
+ data: Buffer;
254
+ type: string;
255
+ } | null>;
256
+ /** Mark as read up to message `msg`. */
257
+ markRead(msg: ConversationRef & {
258
+ index: string;
259
+ }): Promise<void>;
260
+ /** Send a text message to a conversation (usually the just-received `msg` — it carries the conversation ids). */
261
+ sendMessage(conv: ConversationRef, text: string): Promise<SendResult>;
262
+ /** Send an image: upload to the private (encrypted) store, then send an image message. */
263
+ sendImage(conv: ConversationRef, image: ImageInput): Promise<SendResult>;
264
+ /**
265
+ * Set YOUR reaction on a message — like TikTok Web, one reaction per user: a new `emoji` replaces the old one,
266
+ * `null` = remove. Pass the whole `msg` just read (current `reactions` are needed to know which key to remove).
267
+ * Sends `ModifyMessagePropertyRequestBody` (command 705) — see README.
268
+ */
269
+ setReaction(msg: TikTokMessage, emoji: string | null): Promise<void>;
270
+ /** Download a sticker image (public CDN, unencrypted, no cookie needed). */
271
+ downloadSticker(sticker: Pick<TikTokSticker, 'url'>): Promise<{
272
+ data: Buffer;
273
+ type: string;
274
+ }>;
275
+ /** Download + decrypt a message's image (pass the whole `msg` — listener messages lack the URL, this looks it up). */
276
+ downloadImage(source: TikTokMessage | TikTokImage, which?: 'origin' | 'thumb'): Promise<Buffer>;
277
+ private send;
278
+ }
279
+
280
+ declare class TikTok {
281
+ private readonly options;
282
+ constructor(options?: TikTokOptions);
283
+ /** Log in with existing cookies. Expired cookies → `TikTokError('SESSION_EXPIRED')`. */
284
+ login(credentials: Credentials): Promise<API>;
285
+ }
286
+
287
+ /** `MessageBody.message_type`. */
288
+ declare const MessageType: {
289
+ readonly System: 1;
290
+ readonly Text: 7;
291
+ readonly ShareVideo: 8;
292
+ readonly PrivateImage: 1802;
293
+ /** Sticker — public image on the sticker CDN, not encrypted. */
294
+ readonly Sticker: 1805;
295
+ /** Hidden command (read receipt…) — not shown in the conversation, but still pushed over websocket. */
296
+ readonly Command: 50001;
297
+ };
298
+
299
+ /** Error from TikTok or from the library itself. `code` lets callers branch (e.g. session expired → rescan). */
300
+ declare class TikTokError extends Error {
301
+ readonly code: TikTokErrorCode;
302
+ /** Original TikTok code (status_code / error_code), if any. */
303
+ readonly upstream?: number | string | undefined;
304
+ constructor(code: TikTokErrorCode, message: string,
305
+ /** Original TikTok code (status_code / error_code), if any. */
306
+ upstream?: number | string | undefined);
307
+ }
308
+ type TikTokErrorCode =
309
+ /** Cookie is no longer logged in — a new session is required. */
310
+ 'SESSION_EXPIRED' | 'HTTP_ERROR'
311
+ /** IM returned a non-zero `status_code`. */
312
+ | 'IM_ERROR' | 'UPLOAD_FAILED' | 'DOWNLOAD_FAILED' | 'BAD_RESPONSE'
313
+ /** QR login failed (Chrome failed to launch, QR expired, no cookie found…). */
314
+ | 'LOGIN_FAILED'
315
+ /** Caller aborted QR login (`signal`). */
316
+ | 'LOGIN_ABORTED';
317
+
318
+ /**
319
+ * QR login via headless Chrome.
320
+ *
321
+ * Why a browser: TikTok's QR API (`passport/web/check_qrconnect`) requires a JS signature
322
+ * (X-Gnarly / msToken) — calling it bare from Node yields `error_code 7`. So we open the real
323
+ * `tiktok.com/login/qrcode` page in headless Chrome and let it sign itself; we only LISTEN to
324
+ * responses via CDP: the QR image from the `get_qrcode` body, the status from `check_qrconnect`.
325
+ * After login we grab the cookies and close Chrome — everything after (`login`, chat) needs no browser.
326
+ */
327
+ type QrLoginEvent =
328
+ /** New QR image (`data:image/png;base64,…`) — show it to the user to scan. A new one comes on expiry. */
329
+ {
330
+ type: 'qr';
331
+ image: string;
332
+ expiresAt: number;
333
+ }
334
+ /** Scanned, waiting for confirmation on the phone. */
335
+ | {
336
+ type: 'scanned';
337
+ }
338
+ /** Confirmed — fetching cookies. */
339
+ | {
340
+ type: 'confirmed';
341
+ };
342
+ interface QrLoginOptions {
343
+ /**
344
+ * Path to Chrome / Chromium (e.g. `/usr/bin/chromium-browser`). Optional — when omitted, `CHROME_PATH` and the
345
+ * usual install locations are searched.
346
+ */
347
+ chromePath?: string;
348
+ onEvent?: (e: QrLoginEvent) => void;
349
+ signal?: AbortSignal;
350
+ /** Total time to wait for a scan (ms) — default 5 minutes. */
351
+ timeoutMs?: number;
352
+ /** When the QR expires (~1 minute), regenerate it at most this many times. */
353
+ maxRefresh?: number;
354
+ userAgent?: string;
355
+ }
356
+ /**
357
+ * Log in to TikTok via QR. Returns `Credentials` (`.tiktok.com` cookies + UA) — pass them to
358
+ * `new TikTok().login(creds)` and SAVE them so you don't need to scan next time.
359
+ */
360
+ declare function loginQR(opts: QrLoginOptions): Promise<Credentials>;
361
+
362
+ /**
363
+ * Locate a Chrome / Chromium executable: `CHROME_PATH` env, then well-known install paths for the platform, then
364
+ * `PATH`. Returns `null` when none is found.
365
+ */
366
+ declare function findChrome(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, exists?: (path: string) => boolean): string | null;
367
+
368
+ export { API, Listener, MANUAL_CLOSE, MessageType, TikTok, TikTokError, findChrome, loginQR };
369
+ export type { ConversationPage, ConversationRef, CookieInput, Credentials, ImageInput, ListenerEvents, MessagePage, QrLoginEvent, QrLoginOptions, SendResult, TikTokAccount, TikTokConversation, TikTokErrorCode, TikTokImage, TikTokMessage, TikTokOptions, TikTokReaction, TikTokSticker, TikTokUser };