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/dist/index.mjs
CHANGED
|
@@ -750,8 +750,9 @@ var API = class {
|
|
|
750
750
|
getCookies() {
|
|
751
751
|
return [...this.ctx.cookies].map(([name, value]) => ({ name, value }));
|
|
752
752
|
}
|
|
753
|
+
/** Non-cookie session state to save alongside the cookies (pass back into `login`). */
|
|
753
754
|
getContext() {
|
|
754
|
-
return { deviceId: this.ctx.deviceId, userAgent: this.ctx.userAgent };
|
|
755
|
+
return { deviceId: this.ctx.deviceId, userAgent: this.ctx.userAgent, ticketGuard: this.ctx.ticketGuard };
|
|
755
756
|
}
|
|
756
757
|
/** Whether the session is still alive — call periodically to detect expiry early. */
|
|
757
758
|
async isAlive() {
|
|
@@ -881,6 +882,56 @@ var API = class {
|
|
|
881
882
|
}
|
|
882
883
|
};
|
|
883
884
|
|
|
885
|
+
// src/protocol/ticket-guard.ts
|
|
886
|
+
import { createPrivateKey, createPublicKey, sign } from "crypto";
|
|
887
|
+
var SIGNED_PATHS = /* @__PURE__ */ new Set(["/v1/message/send"]);
|
|
888
|
+
function ticketGuardHeaders(guard, path, now = Date.now()) {
|
|
889
|
+
const timestamp = Math.floor(now / 1e3);
|
|
890
|
+
const message = `ticket=${guard.ticket}&path=${path}×tamp=${timestamp}`;
|
|
891
|
+
const reqSign = sign("sha256", Buffer.from(message), createPrivateKey(guard.privateKey)).toString("base64");
|
|
892
|
+
const clientData = { ts_sign: guard.tsSign, req_content: "ticket,path,timestamp", req_sign: reqSign, timestamp };
|
|
893
|
+
return {
|
|
894
|
+
"tt-ticket-guard-client-data": Buffer.from(JSON.stringify(clientData)).toString("base64"),
|
|
895
|
+
"tt-ticket-guard-public-key": publicPoint(guard.publicKey),
|
|
896
|
+
"tt-ticket-guard-version": "2",
|
|
897
|
+
"tt-ticket-guard-web-version": "1",
|
|
898
|
+
"tt-ticket-guard-iteration-version": "0"
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function publicPoint(pem) {
|
|
902
|
+
const jwk = createPublicKey(pem).export({ format: "jwk" });
|
|
903
|
+
return Buffer.concat([
|
|
904
|
+
Buffer.from([4]),
|
|
905
|
+
Buffer.from(jwk.x ?? "", "base64url"),
|
|
906
|
+
Buffer.from(jwk.y ?? "", "base64url")
|
|
907
|
+
]).toString("base64");
|
|
908
|
+
}
|
|
909
|
+
var READ_TICKET_GUARD_JS = `(async () => {
|
|
910
|
+
try {
|
|
911
|
+
const db = await new Promise((ok, ko) => { const r = indexedDB.open('secure-store'); r.onsuccess = () => ok(r.result); r.onerror = ko; });
|
|
912
|
+
if (!db.objectStoreNames.contains('cryptvalues')) return null;
|
|
913
|
+
const get = (k) => new Promise((ok) => { const q = db.transaction('cryptvalues').objectStore('cryptvalues').get(k); q.onsuccess = () => ok(q.result); q.onerror = () => ok(null); });
|
|
914
|
+
const keysRaw = await get('security-sdk/s_sdk_crypt_sdk');
|
|
915
|
+
const signRaw = await get('security-sdk/s_sdk_sign_data_key/tt_fetch');
|
|
916
|
+
if (!keysRaw || !signRaw) return null;
|
|
917
|
+
const keys = JSON.parse(JSON.parse(keysRaw).data);
|
|
918
|
+
const signData = JSON.parse(JSON.parse(signRaw).data);
|
|
919
|
+
if (!keys.ec_privateKey || !keys.ec_publicKey || !signData.ts_sign || !signData.encrypt_ticket) return null;
|
|
920
|
+
const te = new TextEncoder();
|
|
921
|
+
const base = await crypto.subtle.importKey('raw', te.encode('tt-ticket-guard-iv'), 'PBKDF2', false, ['deriveKey']);
|
|
922
|
+
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']);
|
|
923
|
+
const enc = Uint8Array.from(atob(signData.encrypt_ticket), (c) => c.charCodeAt(0));
|
|
924
|
+
const ticket = new TextDecoder().decode(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: enc.slice(0, 12) }, aes, enc.slice(12)));
|
|
925
|
+
return { privateKey: keys.ec_privateKey, publicKey: keys.ec_publicKey, tsSign: signData.ts_sign, ticket };
|
|
926
|
+
} catch (e) {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
})()`;
|
|
930
|
+
function isTicketGuard(v) {
|
|
931
|
+
const g = v;
|
|
932
|
+
return !!g && typeof g.privateKey === "string" && typeof g.publicKey === "string" && typeof g.tsSign === "string" && typeof g.ticket === "string" && g.ticket.length > 0;
|
|
933
|
+
}
|
|
934
|
+
|
|
884
935
|
// src/protocol/context.ts
|
|
885
936
|
function normalizeCookies(input) {
|
|
886
937
|
const jar = /* @__PURE__ */ new Map();
|
|
@@ -902,6 +953,7 @@ var Context = class {
|
|
|
902
953
|
this.cookies = normalizeCookies(creds.cookies);
|
|
903
954
|
this.deviceId = creds.deviceId ?? "";
|
|
904
955
|
this.userAgent = creds.userAgent || DEFAULT_USER_AGENT;
|
|
956
|
+
if (isTicketGuard(creds.ticketGuard)) this.ticketGuard = creds.ticketGuard;
|
|
905
957
|
}
|
|
906
958
|
options;
|
|
907
959
|
cookies;
|
|
@@ -911,6 +963,8 @@ var Context = class {
|
|
|
911
963
|
*/
|
|
912
964
|
deviceId;
|
|
913
965
|
userAgent;
|
|
966
|
+
/** Signing material for `SIGNED_PATHS` — absent for sessions saved before 0.5.0 (sends then go unsigned). */
|
|
967
|
+
ticketGuard;
|
|
914
968
|
ownId = "";
|
|
915
969
|
seq = 1e4;
|
|
916
970
|
cookieHeader() {
|
|
@@ -956,17 +1010,24 @@ var Context = class {
|
|
|
956
1010
|
}
|
|
957
1011
|
/**
|
|
958
1012
|
* Call an IM command over HTTP. Returns the unwrapped body (`ResponseBody[cmd]`).
|
|
959
|
-
* NO X-Gnarly/X-Bogus/msToken signature needed
|
|
1013
|
+
* NO X-Gnarly/X-Bogus/msToken signature needed. `message/send` additionally needs the ticket-guard headers
|
|
1014
|
+
* (enforced 12/11/2026) — added from `ticketGuard` when present.
|
|
960
1015
|
*/
|
|
961
1016
|
async im(path, cmd, body, bodyField = cmd) {
|
|
962
1017
|
const url = `${IM_API}${path}?aid=${AID_WEB}&version_code=1.0.0&app_name=tiktok_web&device_platform=web_pc`;
|
|
963
1018
|
const res = await this.fetch(url, {
|
|
964
1019
|
method: "POST",
|
|
965
|
-
headers: {
|
|
1020
|
+
headers: {
|
|
1021
|
+
"Content-Type": "application/x-protobuf",
|
|
1022
|
+
Accept: "application/x-protobuf",
|
|
1023
|
+
...this.ticketGuard && SIGNED_PATHS.has(path) ? ticketGuardHeaders(this.ticketGuard, path) : {}
|
|
1024
|
+
},
|
|
966
1025
|
body: new Uint8Array(this.buildRequest(cmd, bodyField, body))
|
|
967
1026
|
});
|
|
968
1027
|
if (!res.ok) throw new TikTokError("HTTP_ERROR", `${path}: HTTP ${res.status}`, res.status);
|
|
969
|
-
const
|
|
1028
|
+
const raw = Buffer.from(await res.arrayBuffer());
|
|
1029
|
+
if (/json/i.test(res.headers.get("content-type") ?? "")) throw gatewayError(path, raw);
|
|
1030
|
+
const resp = Msg.tryParse(raw);
|
|
970
1031
|
if (!resp) throw new TikTokError("BAD_RESPONSE", `${path}: response is not protobuf`);
|
|
971
1032
|
const status = resp.num(Resp.status);
|
|
972
1033
|
if (status !== 0) {
|
|
@@ -977,6 +1038,19 @@ var Context = class {
|
|
|
977
1038
|
return resp.msg(Resp.body)?.msg(cmd) ?? resp.msg(Resp.body)?.msg(bodyField) ?? Msg.parse(Buffer.alloc(0));
|
|
978
1039
|
}
|
|
979
1040
|
};
|
|
1041
|
+
function gatewayError(path, raw) {
|
|
1042
|
+
let status;
|
|
1043
|
+
try {
|
|
1044
|
+
status = JSON.parse(raw.toString("utf8")).status_code;
|
|
1045
|
+
} catch {
|
|
1046
|
+
}
|
|
1047
|
+
if (status === 7) return new TikTokError("SESSION_BLOCKED", `${path}: gateway status_code 7 \u2014 session blocked`, 7);
|
|
1048
|
+
return new TikTokError(
|
|
1049
|
+
"BAD_RESPONSE",
|
|
1050
|
+
`${path}: gateway JSON status_code ${String(status)}`,
|
|
1051
|
+
typeof status === "number" ? status : void 0
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
980
1054
|
|
|
981
1055
|
// src/client.ts
|
|
982
1056
|
var TikTok = class {
|
|
@@ -1184,6 +1258,28 @@ async function loginQR(opts) {
|
|
|
1184
1258
|
return null;
|
|
1185
1259
|
}
|
|
1186
1260
|
};
|
|
1261
|
+
const readTicketGuard = async () => {
|
|
1262
|
+
const read = async () => {
|
|
1263
|
+
const r = await tab.send("Runtime.evaluate", {
|
|
1264
|
+
expression: READ_TICKET_GUARD_JS,
|
|
1265
|
+
awaitPromise: true,
|
|
1266
|
+
returnByValue: true
|
|
1267
|
+
}).catch(() => null);
|
|
1268
|
+
return isTicketGuard(r?.result?.value) ? r.result.value : void 0;
|
|
1269
|
+
};
|
|
1270
|
+
for (let i = 0; i < 8 && !done; i++) {
|
|
1271
|
+
const g = await read();
|
|
1272
|
+
if (g) return g;
|
|
1273
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1274
|
+
}
|
|
1275
|
+
await tab.send("Page.navigate", { url: `${WEB}/messages` }).catch(() => void 0);
|
|
1276
|
+
for (let i = 0; i < 20 && !done; i++) {
|
|
1277
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1278
|
+
const g = await read();
|
|
1279
|
+
if (g) return g;
|
|
1280
|
+
}
|
|
1281
|
+
return void 0;
|
|
1282
|
+
};
|
|
1187
1283
|
const collectCookies = async () => {
|
|
1188
1284
|
for (let i = 0; i < 30 && !done; i++) {
|
|
1189
1285
|
const { cookies } = await tab.send(
|
|
@@ -1197,7 +1293,8 @@ async function loginQR(opts) {
|
|
|
1197
1293
|
)) {
|
|
1198
1294
|
byName.set(c.name, { name: c.name, value: c.value });
|
|
1199
1295
|
}
|
|
1200
|
-
|
|
1296
|
+
const ticketGuard = await readTicketGuard();
|
|
1297
|
+
return finish(null, { cookies: [...byName.values()], userAgent, ...ticketGuard ? { ticketGuard } : {} });
|
|
1201
1298
|
}
|
|
1202
1299
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
1203
1300
|
}
|