tca-js 0.3.0 → 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.
@@ -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 };