tca-js 0.4.0 → 0.5.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 +21 -14
- package/dist/index.d.mts +29 -3
- package/dist/index.d.ts +29 -3
- package/dist/index.js +102 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +102 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,6 +86,11 @@ await new TikTok().login({ cookies: 'sessionid=…; ttwid=…' }); // header str
|
|
|
86
86
|
Required cookies: `sessionid`, `sid_tt`, `sid_guard`, `uid_tt`, `ttwid`, `odin_tt`, `store-idc`, `tt-target-idc`.
|
|
87
87
|
A session lasts about 6 months, and logging in does not sign out other devices.
|
|
88
88
|
|
|
89
|
+
> **Sending needs `ticketGuard`** (since 12/11/2026). TikTok signs `message/send` with a per-login key pair; a session
|
|
90
|
+
> that sends unsigned gets blocked after its first message (`SESSION_BLOCKED`). Cookies alone are enough to READ, but
|
|
91
|
+
> to send use credentials from `loginQR` (which captures `ticketGuard`) and persist them as a whole — including
|
|
92
|
+
> `api.getContext().ticketGuard`. Treat it as secret as the cookies.
|
|
93
|
+
|
|
89
94
|
### QR code
|
|
90
95
|
|
|
91
96
|
`loginQR` opens a headless Chrome on TikTok's QR page, emits the QR image, and resolves with credentials once the code is
|
|
@@ -101,7 +106,7 @@ const credentials = await loginQR({
|
|
|
101
106
|
});
|
|
102
107
|
|
|
103
108
|
const api = await new TikTok().login(credentials);
|
|
104
|
-
// Save `credentials` to skip scanning next time.
|
|
109
|
+
// Save `credentials` (cookies + ticketGuard) to skip scanning next time — both are secrets.
|
|
105
110
|
```
|
|
106
111
|
|
|
107
112
|
| Option | Type | Default | Description |
|
|
@@ -143,12 +148,13 @@ with `sendMessage(msg, …)`.
|
|
|
143
148
|
|
|
144
149
|
### Session
|
|
145
150
|
|
|
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
|
-
| `
|
|
151
|
+
| Method / property | Description |
|
|
152
|
+
| ----------------- | ------------------------------------------------------------- |
|
|
153
|
+
| `account` | Logged-in account: `uid`, `username`, `screenName`, `avatar` |
|
|
154
|
+
| `getOwnId()` | Own user id |
|
|
155
|
+
| `getCookies()` | Current cookies — persist them to resume the session |
|
|
156
|
+
| `getContext()` | `deviceId`, `userAgent`, `ticketGuard` — persist with cookies |
|
|
157
|
+
| `isAlive()` | `true` while the session is still valid |
|
|
152
158
|
|
|
153
159
|
### Message object
|
|
154
160
|
|
|
@@ -184,13 +190,14 @@ api.listener.stop();
|
|
|
184
190
|
|
|
185
191
|
All library errors are instances of `TikTokError` with a `code`:
|
|
186
192
|
|
|
187
|
-
| Code | Meaning
|
|
188
|
-
| ----------------------------------- |
|
|
189
|
-
| `SESSION_EXPIRED` | Cookies are no longer valid — log in again
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
193
|
+
| Code | Meaning |
|
|
194
|
+
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
195
|
+
| `SESSION_EXPIRED` | Cookies are no longer valid — log in again |
|
|
196
|
+
| `SESSION_BLOCKED` | TikTok's gateway refuses this session (JSON `status_code: 7`) — usually sent without `ticketGuard`; log in again with `loginQR` |
|
|
197
|
+
| `IM_ERROR` | TikTok rejected the request |
|
|
198
|
+
| `HTTP_ERROR` · `BAD_RESPONSE` | Network or unexpected response |
|
|
199
|
+
| `UPLOAD_FAILED` · `DOWNLOAD_FAILED` | Media transfer failed |
|
|
200
|
+
| `LOGIN_FAILED` · `LOGIN_ABORTED` | QR login failed or was cancelled |
|
|
194
201
|
|
|
195
202
|
```ts
|
|
196
203
|
import { TikTokError } from 'tca-js';
|
package/dist/index.d.mts
CHANGED
|
@@ -44,6 +44,22 @@ interface Credentials {
|
|
|
44
44
|
/** IM device id (`wid`) — leave empty and `login` asks TikTok using `ttwid`. Only used when that fails. */
|
|
45
45
|
deviceId?: string;
|
|
46
46
|
userAgent?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Request-signing material captured at login (`loginQR` fills it). Required to SEND messages since 12/11/2026 —
|
|
49
|
+
* without it TikTok blocks the session's sends after the first one (`SESSION_BLOCKED`). Keep it with the cookies.
|
|
50
|
+
*/
|
|
51
|
+
ticketGuard?: TicketGuard;
|
|
52
|
+
}
|
|
53
|
+
/** TikTok "ticket guard" signing material — see `protocol/ticket-guard.ts`. Secret: treat like the session cookie. */
|
|
54
|
+
interface TicketGuard {
|
|
55
|
+
/** EC P-256 private key, PKCS#8 PEM. */
|
|
56
|
+
privateKey: string;
|
|
57
|
+
/** Matching public key, SPKI PEM. */
|
|
58
|
+
publicKey: string;
|
|
59
|
+
/** Server-issued `ts_sign` bound to the public key. */
|
|
60
|
+
tsSign: string;
|
|
61
|
+
/** Server-issued ticket (plain text). */
|
|
62
|
+
ticket: string;
|
|
47
63
|
}
|
|
48
64
|
interface TikTokOptions {
|
|
49
65
|
/** Debug logging — silent by default. */
|
|
@@ -152,6 +168,8 @@ declare class Context {
|
|
|
152
168
|
*/
|
|
153
169
|
deviceId: string;
|
|
154
170
|
readonly userAgent: string;
|
|
171
|
+
/** Signing material for `SIGNED_PATHS` — absent for sessions saved before 0.5.0 (sends then go unsigned). */
|
|
172
|
+
readonly ticketGuard?: TicketGuard;
|
|
155
173
|
ownId: string;
|
|
156
174
|
private seq;
|
|
157
175
|
constructor(creds: Credentials, options?: TikTokOptions);
|
|
@@ -166,7 +184,8 @@ declare class Context {
|
|
|
166
184
|
buildRequest(cmd: number, bodyField: number, body: Buffer): Buffer;
|
|
167
185
|
/**
|
|
168
186
|
* Call an IM command over HTTP. Returns the unwrapped body (`ResponseBody[cmd]`).
|
|
169
|
-
* NO X-Gnarly/X-Bogus/msToken signature needed
|
|
187
|
+
* NO X-Gnarly/X-Bogus/msToken signature needed. `message/send` additionally needs the ticket-guard headers
|
|
188
|
+
* (enforced 12/11/2026) — added from `ticketGuard` when present.
|
|
170
189
|
*/
|
|
171
190
|
im(path: string, cmd: number, body: Buffer, bodyField?: number): Promise<Msg>;
|
|
172
191
|
}
|
|
@@ -230,9 +249,11 @@ declare class API {
|
|
|
230
249
|
name: string;
|
|
231
250
|
value: string;
|
|
232
251
|
}[];
|
|
252
|
+
/** Non-cookie session state to save alongside the cookies (pass back into `login`). */
|
|
233
253
|
getContext(): {
|
|
234
254
|
deviceId: string;
|
|
235
255
|
userAgent: string;
|
|
256
|
+
ticketGuard?: TicketGuard;
|
|
236
257
|
};
|
|
237
258
|
/** Whether the session is still alive — call periodically to detect expiry early. */
|
|
238
259
|
isAlive(): Promise<boolean>;
|
|
@@ -307,7 +328,12 @@ declare class TikTokError extends Error {
|
|
|
307
328
|
}
|
|
308
329
|
type TikTokErrorCode =
|
|
309
330
|
/** Cookie is no longer logged in — a new session is required. */
|
|
310
|
-
'SESSION_EXPIRED'
|
|
331
|
+
'SESSION_EXPIRED'
|
|
332
|
+
/**
|
|
333
|
+
* TikTok's gateway rejects this session for the command (JSON `status_code: 7` instead of protobuf). Seen on
|
|
334
|
+
* `message/send` while reads keep working — log in again to get a fresh session.
|
|
335
|
+
*/
|
|
336
|
+
| 'SESSION_BLOCKED' | 'HTTP_ERROR'
|
|
311
337
|
/** IM returned a non-zero `status_code`. */
|
|
312
338
|
| 'IM_ERROR' | 'UPLOAD_FAILED' | 'DOWNLOAD_FAILED' | 'BAD_RESPONSE'
|
|
313
339
|
/** QR login failed (Chrome failed to launch, QR expired, no cookie found…). */
|
|
@@ -366,4 +392,4 @@ declare function loginQR(opts: QrLoginOptions): Promise<Credentials>;
|
|
|
366
392
|
declare function findChrome(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, exists?: (path: string) => boolean): string | null;
|
|
367
393
|
|
|
368
394
|
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 };
|
|
395
|
+
export type { ConversationPage, ConversationRef, CookieInput, Credentials, ImageInput, ListenerEvents, MessagePage, QrLoginEvent, QrLoginOptions, SendResult, TicketGuard, TikTokAccount, TikTokConversation, TikTokErrorCode, TikTokImage, TikTokMessage, TikTokOptions, TikTokReaction, TikTokSticker, TikTokUser };
|
package/dist/index.d.ts
CHANGED
|
@@ -44,6 +44,22 @@ interface Credentials {
|
|
|
44
44
|
/** IM device id (`wid`) — leave empty and `login` asks TikTok using `ttwid`. Only used when that fails. */
|
|
45
45
|
deviceId?: string;
|
|
46
46
|
userAgent?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Request-signing material captured at login (`loginQR` fills it). Required to SEND messages since 12/11/2026 —
|
|
49
|
+
* without it TikTok blocks the session's sends after the first one (`SESSION_BLOCKED`). Keep it with the cookies.
|
|
50
|
+
*/
|
|
51
|
+
ticketGuard?: TicketGuard;
|
|
52
|
+
}
|
|
53
|
+
/** TikTok "ticket guard" signing material — see `protocol/ticket-guard.ts`. Secret: treat like the session cookie. */
|
|
54
|
+
interface TicketGuard {
|
|
55
|
+
/** EC P-256 private key, PKCS#8 PEM. */
|
|
56
|
+
privateKey: string;
|
|
57
|
+
/** Matching public key, SPKI PEM. */
|
|
58
|
+
publicKey: string;
|
|
59
|
+
/** Server-issued `ts_sign` bound to the public key. */
|
|
60
|
+
tsSign: string;
|
|
61
|
+
/** Server-issued ticket (plain text). */
|
|
62
|
+
ticket: string;
|
|
47
63
|
}
|
|
48
64
|
interface TikTokOptions {
|
|
49
65
|
/** Debug logging — silent by default. */
|
|
@@ -152,6 +168,8 @@ declare class Context {
|
|
|
152
168
|
*/
|
|
153
169
|
deviceId: string;
|
|
154
170
|
readonly userAgent: string;
|
|
171
|
+
/** Signing material for `SIGNED_PATHS` — absent for sessions saved before 0.5.0 (sends then go unsigned). */
|
|
172
|
+
readonly ticketGuard?: TicketGuard;
|
|
155
173
|
ownId: string;
|
|
156
174
|
private seq;
|
|
157
175
|
constructor(creds: Credentials, options?: TikTokOptions);
|
|
@@ -166,7 +184,8 @@ declare class Context {
|
|
|
166
184
|
buildRequest(cmd: number, bodyField: number, body: Buffer): Buffer;
|
|
167
185
|
/**
|
|
168
186
|
* Call an IM command over HTTP. Returns the unwrapped body (`ResponseBody[cmd]`).
|
|
169
|
-
* NO X-Gnarly/X-Bogus/msToken signature needed
|
|
187
|
+
* NO X-Gnarly/X-Bogus/msToken signature needed. `message/send` additionally needs the ticket-guard headers
|
|
188
|
+
* (enforced 12/11/2026) — added from `ticketGuard` when present.
|
|
170
189
|
*/
|
|
171
190
|
im(path: string, cmd: number, body: Buffer, bodyField?: number): Promise<Msg>;
|
|
172
191
|
}
|
|
@@ -230,9 +249,11 @@ declare class API {
|
|
|
230
249
|
name: string;
|
|
231
250
|
value: string;
|
|
232
251
|
}[];
|
|
252
|
+
/** Non-cookie session state to save alongside the cookies (pass back into `login`). */
|
|
233
253
|
getContext(): {
|
|
234
254
|
deviceId: string;
|
|
235
255
|
userAgent: string;
|
|
256
|
+
ticketGuard?: TicketGuard;
|
|
236
257
|
};
|
|
237
258
|
/** Whether the session is still alive — call periodically to detect expiry early. */
|
|
238
259
|
isAlive(): Promise<boolean>;
|
|
@@ -307,7 +328,12 @@ declare class TikTokError extends Error {
|
|
|
307
328
|
}
|
|
308
329
|
type TikTokErrorCode =
|
|
309
330
|
/** Cookie is no longer logged in — a new session is required. */
|
|
310
|
-
'SESSION_EXPIRED'
|
|
331
|
+
'SESSION_EXPIRED'
|
|
332
|
+
/**
|
|
333
|
+
* TikTok's gateway rejects this session for the command (JSON `status_code: 7` instead of protobuf). Seen on
|
|
334
|
+
* `message/send` while reads keep working — log in again to get a fresh session.
|
|
335
|
+
*/
|
|
336
|
+
| 'SESSION_BLOCKED' | 'HTTP_ERROR'
|
|
311
337
|
/** IM returned a non-zero `status_code`. */
|
|
312
338
|
| 'IM_ERROR' | 'UPLOAD_FAILED' | 'DOWNLOAD_FAILED' | 'BAD_RESPONSE'
|
|
313
339
|
/** QR login failed (Chrome failed to launch, QR expired, no cookie found…). */
|
|
@@ -366,4 +392,4 @@ declare function loginQR(opts: QrLoginOptions): Promise<Credentials>;
|
|
|
366
392
|
declare function findChrome(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, exists?: (path: string) => boolean): string | null;
|
|
367
393
|
|
|
368
394
|
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 };
|
|
395
|
+
export type { ConversationPage, ConversationRef, CookieInput, Credentials, ImageInput, ListenerEvents, MessagePage, QrLoginEvent, QrLoginOptions, SendResult, TicketGuard, TikTokAccount, TikTokConversation, TikTokErrorCode, TikTokImage, TikTokMessage, TikTokOptions, TikTokReaction, TikTokSticker, TikTokUser };
|
package/dist/index.js
CHANGED
|
@@ -793,8 +793,9 @@ var API = class {
|
|
|
793
793
|
getCookies() {
|
|
794
794
|
return [...this.ctx.cookies].map(([name, value]) => ({ name, value }));
|
|
795
795
|
}
|
|
796
|
+
/** Non-cookie session state to save alongside the cookies (pass back into `login`). */
|
|
796
797
|
getContext() {
|
|
797
|
-
return { deviceId: this.ctx.deviceId, userAgent: this.ctx.userAgent };
|
|
798
|
+
return { deviceId: this.ctx.deviceId, userAgent: this.ctx.userAgent, ticketGuard: this.ctx.ticketGuard };
|
|
798
799
|
}
|
|
799
800
|
/** Whether the session is still alive — call periodically to detect expiry early. */
|
|
800
801
|
async isAlive() {
|
|
@@ -924,6 +925,56 @@ var API = class {
|
|
|
924
925
|
}
|
|
925
926
|
};
|
|
926
927
|
|
|
928
|
+
// src/protocol/ticket-guard.ts
|
|
929
|
+
var import_node_crypto4 = require("crypto");
|
|
930
|
+
var SIGNED_PATHS = /* @__PURE__ */ new Set(["/v1/message/send"]);
|
|
931
|
+
function ticketGuardHeaders(guard, path, now = Date.now()) {
|
|
932
|
+
const timestamp = Math.floor(now / 1e3);
|
|
933
|
+
const message = `ticket=${guard.ticket}&path=${path}×tamp=${timestamp}`;
|
|
934
|
+
const reqSign = (0, import_node_crypto4.sign)("sha256", Buffer.from(message), (0, import_node_crypto4.createPrivateKey)(guard.privateKey)).toString("base64");
|
|
935
|
+
const clientData = { ts_sign: guard.tsSign, req_content: "ticket,path,timestamp", req_sign: reqSign, timestamp };
|
|
936
|
+
return {
|
|
937
|
+
"tt-ticket-guard-client-data": Buffer.from(JSON.stringify(clientData)).toString("base64"),
|
|
938
|
+
"tt-ticket-guard-public-key": publicPoint(guard.publicKey),
|
|
939
|
+
"tt-ticket-guard-version": "2",
|
|
940
|
+
"tt-ticket-guard-web-version": "1",
|
|
941
|
+
"tt-ticket-guard-iteration-version": "0"
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function publicPoint(pem) {
|
|
945
|
+
const jwk = (0, import_node_crypto4.createPublicKey)(pem).export({ format: "jwk" });
|
|
946
|
+
return Buffer.concat([
|
|
947
|
+
Buffer.from([4]),
|
|
948
|
+
Buffer.from(jwk.x ?? "", "base64url"),
|
|
949
|
+
Buffer.from(jwk.y ?? "", "base64url")
|
|
950
|
+
]).toString("base64");
|
|
951
|
+
}
|
|
952
|
+
var READ_TICKET_GUARD_JS = `(async () => {
|
|
953
|
+
try {
|
|
954
|
+
const db = await new Promise((ok, ko) => { const r = indexedDB.open('secure-store'); r.onsuccess = () => ok(r.result); r.onerror = ko; });
|
|
955
|
+
if (!db.objectStoreNames.contains('cryptvalues')) return null;
|
|
956
|
+
const get = (k) => new Promise((ok) => { const q = db.transaction('cryptvalues').objectStore('cryptvalues').get(k); q.onsuccess = () => ok(q.result); q.onerror = () => ok(null); });
|
|
957
|
+
const keysRaw = await get('security-sdk/s_sdk_crypt_sdk');
|
|
958
|
+
const signRaw = await get('security-sdk/s_sdk_sign_data_key/tt_fetch');
|
|
959
|
+
if (!keysRaw || !signRaw) return null;
|
|
960
|
+
const keys = JSON.parse(JSON.parse(keysRaw).data);
|
|
961
|
+
const signData = JSON.parse(JSON.parse(signRaw).data);
|
|
962
|
+
if (!keys.ec_privateKey || !keys.ec_publicKey || !signData.ts_sign || !signData.encrypt_ticket) return null;
|
|
963
|
+
const te = new TextEncoder();
|
|
964
|
+
const base = await crypto.subtle.importKey('raw', te.encode('tt-ticket-guard-iv'), 'PBKDF2', false, ['deriveKey']);
|
|
965
|
+
const aes = await crypto.subtle.deriveKey({ name: 'PBKDF2', salt: te.encode('secure-salt'), iterations: 1000, hash: 'SHA-256' }, base, { name: 'AES-GCM', length: 128 }, false, ['decrypt']);
|
|
966
|
+
const enc = Uint8Array.from(atob(signData.encrypt_ticket), (c) => c.charCodeAt(0));
|
|
967
|
+
const ticket = new TextDecoder().decode(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: enc.slice(0, 12) }, aes, enc.slice(12)));
|
|
968
|
+
return { privateKey: keys.ec_privateKey, publicKey: keys.ec_publicKey, tsSign: signData.ts_sign, ticket };
|
|
969
|
+
} catch (e) {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
})()`;
|
|
973
|
+
function isTicketGuard(v) {
|
|
974
|
+
const g = v;
|
|
975
|
+
return !!g && typeof g.privateKey === "string" && typeof g.publicKey === "string" && typeof g.tsSign === "string" && typeof g.ticket === "string" && g.ticket.length > 0;
|
|
976
|
+
}
|
|
977
|
+
|
|
927
978
|
// src/protocol/context.ts
|
|
928
979
|
function normalizeCookies(input) {
|
|
929
980
|
const jar = /* @__PURE__ */ new Map();
|
|
@@ -945,6 +996,7 @@ var Context = class {
|
|
|
945
996
|
this.cookies = normalizeCookies(creds.cookies);
|
|
946
997
|
this.deviceId = creds.deviceId ?? "";
|
|
947
998
|
this.userAgent = creds.userAgent || DEFAULT_USER_AGENT;
|
|
999
|
+
if (isTicketGuard(creds.ticketGuard)) this.ticketGuard = creds.ticketGuard;
|
|
948
1000
|
}
|
|
949
1001
|
options;
|
|
950
1002
|
cookies;
|
|
@@ -954,6 +1006,8 @@ var Context = class {
|
|
|
954
1006
|
*/
|
|
955
1007
|
deviceId;
|
|
956
1008
|
userAgent;
|
|
1009
|
+
/** Signing material for `SIGNED_PATHS` — absent for sessions saved before 0.5.0 (sends then go unsigned). */
|
|
1010
|
+
ticketGuard;
|
|
957
1011
|
ownId = "";
|
|
958
1012
|
seq = 1e4;
|
|
959
1013
|
cookieHeader() {
|
|
@@ -999,17 +1053,24 @@ var Context = class {
|
|
|
999
1053
|
}
|
|
1000
1054
|
/**
|
|
1001
1055
|
* Call an IM command over HTTP. Returns the unwrapped body (`ResponseBody[cmd]`).
|
|
1002
|
-
* NO X-Gnarly/X-Bogus/msToken signature needed
|
|
1056
|
+
* NO X-Gnarly/X-Bogus/msToken signature needed. `message/send` additionally needs the ticket-guard headers
|
|
1057
|
+
* (enforced 12/11/2026) — added from `ticketGuard` when present.
|
|
1003
1058
|
*/
|
|
1004
1059
|
async im(path, cmd, body, bodyField = cmd) {
|
|
1005
1060
|
const url = `${IM_API}${path}?aid=${AID_WEB}&version_code=1.0.0&app_name=tiktok_web&device_platform=web_pc`;
|
|
1006
1061
|
const res = await this.fetch(url, {
|
|
1007
1062
|
method: "POST",
|
|
1008
|
-
headers: {
|
|
1063
|
+
headers: {
|
|
1064
|
+
"Content-Type": "application/x-protobuf",
|
|
1065
|
+
Accept: "application/x-protobuf",
|
|
1066
|
+
...this.ticketGuard && SIGNED_PATHS.has(path) ? ticketGuardHeaders(this.ticketGuard, path) : {}
|
|
1067
|
+
},
|
|
1009
1068
|
body: new Uint8Array(this.buildRequest(cmd, bodyField, body))
|
|
1010
1069
|
});
|
|
1011
1070
|
if (!res.ok) throw new TikTokError("HTTP_ERROR", `${path}: HTTP ${res.status}`, res.status);
|
|
1012
|
-
const
|
|
1071
|
+
const raw = Buffer.from(await res.arrayBuffer());
|
|
1072
|
+
if (/json/i.test(res.headers.get("content-type") ?? "")) throw gatewayError(path, raw);
|
|
1073
|
+
const resp = Msg.tryParse(raw);
|
|
1013
1074
|
if (!resp) throw new TikTokError("BAD_RESPONSE", `${path}: response is not protobuf`);
|
|
1014
1075
|
const status = resp.num(Resp.status);
|
|
1015
1076
|
if (status !== 0) {
|
|
@@ -1020,6 +1081,19 @@ var Context = class {
|
|
|
1020
1081
|
return resp.msg(Resp.body)?.msg(cmd) ?? resp.msg(Resp.body)?.msg(bodyField) ?? Msg.parse(Buffer.alloc(0));
|
|
1021
1082
|
}
|
|
1022
1083
|
};
|
|
1084
|
+
function gatewayError(path, raw) {
|
|
1085
|
+
let status;
|
|
1086
|
+
try {
|
|
1087
|
+
status = JSON.parse(raw.toString("utf8")).status_code;
|
|
1088
|
+
} catch {
|
|
1089
|
+
}
|
|
1090
|
+
if (status === 7) return new TikTokError("SESSION_BLOCKED", `${path}: gateway status_code 7 \u2014 session blocked`, 7);
|
|
1091
|
+
return new TikTokError(
|
|
1092
|
+
"BAD_RESPONSE",
|
|
1093
|
+
`${path}: gateway JSON status_code ${String(status)}`,
|
|
1094
|
+
typeof status === "number" ? status : void 0
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1023
1097
|
|
|
1024
1098
|
// src/client.ts
|
|
1025
1099
|
var TikTok = class {
|
|
@@ -1227,6 +1301,28 @@ async function loginQR(opts) {
|
|
|
1227
1301
|
return null;
|
|
1228
1302
|
}
|
|
1229
1303
|
};
|
|
1304
|
+
const readTicketGuard = async () => {
|
|
1305
|
+
const read = async () => {
|
|
1306
|
+
const r = await tab.send("Runtime.evaluate", {
|
|
1307
|
+
expression: READ_TICKET_GUARD_JS,
|
|
1308
|
+
awaitPromise: true,
|
|
1309
|
+
returnByValue: true
|
|
1310
|
+
}).catch(() => null);
|
|
1311
|
+
return isTicketGuard(r?.result?.value) ? r.result.value : void 0;
|
|
1312
|
+
};
|
|
1313
|
+
for (let i = 0; i < 8 && !done; i++) {
|
|
1314
|
+
const g = await read();
|
|
1315
|
+
if (g) return g;
|
|
1316
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1317
|
+
}
|
|
1318
|
+
await tab.send("Page.navigate", { url: `${WEB}/messages` }).catch(() => void 0);
|
|
1319
|
+
for (let i = 0; i < 20 && !done; i++) {
|
|
1320
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1321
|
+
const g = await read();
|
|
1322
|
+
if (g) return g;
|
|
1323
|
+
}
|
|
1324
|
+
return void 0;
|
|
1325
|
+
};
|
|
1230
1326
|
const collectCookies = async () => {
|
|
1231
1327
|
for (let i = 0; i < 30 && !done; i++) {
|
|
1232
1328
|
const { cookies } = await tab.send(
|
|
@@ -1240,7 +1336,8 @@ async function loginQR(opts) {
|
|
|
1240
1336
|
)) {
|
|
1241
1337
|
byName.set(c.name, { name: c.name, value: c.value });
|
|
1242
1338
|
}
|
|
1243
|
-
|
|
1339
|
+
const ticketGuard = await readTicketGuard();
|
|
1340
|
+
return finish(null, { cookies: [...byName.values()], userAgent, ...ticketGuard ? { ticketGuard } : {} });
|
|
1244
1341
|
}
|
|
1245
1342
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
1246
1343
|
}
|