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/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}&timestamp=${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 — the IM server only checks cookies (tested 09/11/2026).
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: { "Content-Type": "application/x-protobuf", Accept: "application/x-protobuf" },
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 resp = Msg.tryParse(Buffer.from(await res.arrayBuffer()));
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
- return finish(null, { cookies: [...byName.values()], userAgent });
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
  }