emusks 2.3.6 → 2.3.8

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/src/index.js CHANGED
@@ -54,6 +54,8 @@ export default class Emusks {
54
54
  }
55
55
 
56
56
  async login(p) {
57
+ let loginCookies = {};
58
+ let loginClientProfile;
57
59
  if (typeof p === "string") {
58
60
  if (p.length > 50 || p.length < 20) {
59
61
  throw new Error("invalid auth token length!");
@@ -73,14 +75,21 @@ export default class Emusks {
73
75
  phone: p.phone,
74
76
  onRequest: p.onRequest,
75
77
  getCastleToken: p.getCastleToken,
78
+ castleProfile: p.castleProfile,
76
79
  proxy: p.proxy,
77
80
  debug: p.debug,
81
+ timeout: p.timeout,
78
82
  });
83
+ loginCookies = { ...flowResult.cookies };
84
+ loginClientProfile = flowResult.clientProfile;
85
+ if (flowResult.csrfToken) loginCookies.ct0 = flowResult.csrfToken;
79
86
 
80
87
  p = {
81
88
  auth_token: flowResult.authToken,
82
89
  client: p.client,
83
90
  proxy: p.proxy,
91
+ endpoint: p.endpoint,
92
+ transactionIds: p.transactionIds,
84
93
  };
85
94
  }
86
95
 
@@ -88,6 +97,7 @@ export default class Emusks {
88
97
  if (!p.auth_token) throw new Error("auth_token is required!");
89
98
  if (typeof p.client === "string") p.client = clients[p.client];
90
99
  if (!p.client) throw new Error("invalid client!");
100
+ p.client = { ...p.client, fingerprints: p.client.fingerprints ? { ...p.client.fingerprints } : undefined };
91
101
  if (p.proxy) this.proxy = p.proxy;
92
102
 
93
103
  if (p.endpoint) {
@@ -127,44 +137,58 @@ export default class Emusks {
127
137
  "sec-fetch-mode": "cors",
128
138
  "sec-fetch-site": "same-site",
129
139
  "sec-gpc": "1",
140
+ ...loginClientProfile?.clientHints,
130
141
  };
131
142
 
143
+ if (loginClientProfile) {
144
+ p.client.userAgent = loginClientProfile.userAgent;
145
+ p.client.fingerprints.userAgent = loginClientProfile.userAgent;
146
+ p.client.fingerprints.ja3 = p.client.fingerprints.ja3.replace(/^771,/, "772,").replace("4588-", "");
147
+ delete p.client.fingerprints.ja4r;
148
+ }
132
149
  const cycleTLS = await getCycleTLS();
150
+ loginCookies.auth_token = p.auth_token;
133
151
  const res = await cycleTLS("https://x.com/", {
134
152
  headers: {
135
153
  accept:
136
154
  "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
137
- cookie: `auth_token=${p.auth_token};`,
155
+ cookie: Object.entries(loginCookies).map(([name, value]) => `${name}=${value}`).join("; "),
138
156
  ...p.client.headers,
139
157
  },
140
- userAgent: p.client.fingerprints.userAgent,
158
+ userAgent: p.client.fingerprints.userAgent || p.client.fingerprints["user-agent"],
141
159
  ja3: p.client.fingerprints.ja3,
142
160
  ja4r: p.client.fingerprints.ja4r,
143
161
  proxy: p.proxy || undefined,
144
162
  referrer: "https://x.com/",
145
163
  });
146
164
 
147
- const setCookies = res.headers["Set-Cookie"] || [];
148
- const csrfToken = setCookies
149
- .find((c) => c?.startsWith?.("ct0="))
150
- ?.split?.(";")?.[0]
151
- ?.split?.("=")?.[1];
165
+ if (res.status !== 200) throw new Error(`[emusks] session bootstrap failed: ${res.status}`);
166
+ const setCookie = res.headers?.["Set-Cookie"] ?? res.headers?.["set-cookie"] ?? [];
167
+ const setCookies = Array.isArray(setCookie) ? setCookie : [setCookie];
168
+ for (const cookie of setCookies) {
169
+ const [pair, ...attributes] = cookie.split(";");
170
+ const separator = pair.indexOf("=");
171
+ if (separator < 1) continue;
172
+ const name = pair.slice(0, separator).trim();
173
+ const attrs = Object.fromEntries(attributes.map((attribute) => {
174
+ const [key, ...parts] = attribute.trim().split("=");
175
+ return [key.toLowerCase(), parts.join("=")];
176
+ }));
177
+ const maxAge = /^-?\d+$/.test(attrs["max-age"] ?? "") ? Number(attrs["max-age"]) : null;
178
+ const expired = maxAge !== null ? maxAge <= 0 : Date.parse(attrs.expires) <= Date.now();
179
+ if (expired) delete loginCookies[name];
180
+ else loginCookies[name] = pair.slice(separator + 1).trim();
181
+ }
182
+ const csrfToken = loginCookies.ct0;
152
183
 
153
- if (!csrfToken) {
184
+ if (!csrfToken || !loginCookies.auth_token) {
154
185
  throw new Error("[emusks] failed to log in");
155
186
  }
156
187
 
157
188
  this.auth = p;
158
189
  this.auth.csrfToken = csrfToken;
159
-
160
- const cookieParts = [`auth_token=${p.auth_token}`];
161
- for (const setCookie of setCookies) {
162
- const cookiePair = setCookie.split(";")[0];
163
- if (cookiePair && !cookiePair.startsWith("auth_token=")) {
164
- cookieParts.push(cookiePair);
165
- }
166
- }
167
- this.auth.client.headers.cookie = cookieParts.join("; ");
190
+ this.auth.auth_token = loginCookies.auth_token;
191
+ this.auth.client.headers.cookie = Object.entries(loginCookies).map(([name, value]) => `${name}=${value}`).join("; ");
168
192
 
169
193
  const needsTransactionIds =
170
194
  this.transactionIds !== undefined
@@ -0,0 +1,121 @@
1
+ export function decodeJetfuelMessages(input) {
2
+ const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
3
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
4
+ const strings = [];
5
+ const messages = [];
6
+ let offset = 0;
7
+ let end = bytes.length;
8
+ const take = (length) => {
9
+ if (offset + length > end) throw new Error("truncated jetfuel message");
10
+ const start = offset;
11
+ offset += length;
12
+ return start;
13
+ };
14
+ const u8 = () => view.getUint8(take(1));
15
+ const uint = () => {
16
+ let value = 0;
17
+ for (let shift = 0; shift < 49; shift += 7) {
18
+ const byte = u8();
19
+ value += (byte & 127) * 2 ** shift;
20
+ if (!(byte & 128)) return value;
21
+ }
22
+ throw new Error("invalid jetfuel integer");
23
+ };
24
+ const i16 = () => view.getInt16(take(2), true);
25
+ const i32 = () => view.getInt32(take(4), true);
26
+ const i64 = () => view.getBigInt64(take(8), true);
27
+ const f64 = () => view.getFloat64(take(8), true);
28
+ const bool = () => Boolean(u8());
29
+ const str = () => {
30
+ const length = uint();
31
+ const start = take(length);
32
+ const value = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(start, start + length));
33
+ strings.push(value);
34
+ return value;
35
+ };
36
+ const tuple = (...readers) => () => readers.map((read) => read());
37
+ const list = (read) => () => {
38
+ const count = uint();
39
+ if (count > end - offset) throw new Error("invalid jetfuel list length");
40
+ return Array.from({ length: count }, read);
41
+ };
42
+ const dict = (key, value) => () => new Map(list(tuple(key, value))());
43
+ const optional = (read) => () => bool() ? read() : null;
44
+ const record = (fields) => () => Object.fromEntries(Object.entries(fields).map(([key, read]) => [key, read()]));
45
+ const constant = (value) => () => value;
46
+ const variant = (schema) => {
47
+ let readers;
48
+ const read = () => {
49
+ const tag = u8();
50
+ if (!readers[tag]) throw new Error(`unknown jetfuel tag ${tag}`);
51
+ return [tag, readers[tag]()];
52
+ };
53
+ readers = typeof schema === "function" ? schema(read) : schema;
54
+ return read;
55
+ };
56
+ const element = record({ type: i16, props: dict(i16, uint), children: list(uint), id: optional(i64), extend: optional(uint) });
57
+ const ref = variant({
58
+ 0: record({ id: i64 }), 4: record({ id: i64, root: uint }),
59
+ 1: record({ key: i16, root: uint }),
60
+ ...Object.fromEntries([2, 3, 7, 8].map((tag) => [tag, record({ key: str, root: uint })])),
61
+ ...Object.fromEntries([5, 6, 9, 10, 11].map((tag) => [tag, record({ root: uint })])),
62
+ });
63
+ const mutation = variant({
64
+ 0: ref, 1: tuple(ref, uint), 2: tuple(ref, i16), 3: tuple(ref, str),
65
+ 4: tuple(ref, uint, optional(i16)), 5: tuple(ref, uint),
66
+ 6: tuple(ref, optional(tuple(uint, uint))), 7: tuple(ref, uint), 8: tuple(ref, uint),
67
+ });
68
+ const navigation = variant({
69
+ 0: record({ url: uint, preview: optional(uint), replace: bool }),
70
+ 9: record({ url: uint, preview: optional(uint), replace: bool }),
71
+ 1: record({ url: uint, body: optional(uint), preview: optional(uint), replace: bool }),
72
+ 8: record({ url: uint }), 7: record({ id: uint }),
73
+ ...Object.fromEntries([2, 3, 4, 5, 6].map((tag) => [tag, constant(null)])),
74
+ });
75
+ const action = variant((self) => ({
76
+ 0: mutation, 1: record({ ref: uint, action: self, cancel: optional(self) }),
77
+ 2: list(self), 3: record({ url: uint, body: uint, complete: optional(self), error: optional(self), optimistic: optional(self) }),
78
+ 4: record({ action: self, intensity: i16 }), 5: record({ ref: uint, type: u8 }),
79
+ 21: record({ ref: uint, duration: i16, animation: bool }),
80
+ 19: record({ ref: uint, type: u8, allowsRotation: bool }),
81
+ 20: record({ ref: uint, overlay: uint, mode: str }), 6: navigation,
82
+ 7: record({ type: u8, id: optional(i64) }), 8: str,
83
+ 9: record({ urls: list(str), priority: u8 }), 10: record({ action: str, ref: uint }),
84
+ 11: record({ type: u8, ref: uint }), 12: record({ action: self, delaySeconds: i16 }),
85
+ 13: record({ data: str, secret: str, knownDeviceToken: str }),
86
+ 14: record({ text: str, dismissText: optional(str) }), 15: record({ ref: uint, to: uint }),
87
+ 16: record({ ref: uint, fields: list(str) }), 17: record({ ref: uint, using: uint }),
88
+ 18: record({ ref: uint, field: str }),
89
+ }));
90
+ const condition = variant((self) => ({
91
+ 0: record({ ref }), 15: record({ ref }),
92
+ ...Object.fromEntries([1, 2, 5, 6, 7, 8].map((tag) => [tag, record({ ref, value: uint })])),
93
+ 3: record({ ref, value: list(uint) }), 4: record({ ref, value: list(uint) }),
94
+ ...Object.fromEntries([9, 10, 11].map((tag) => [tag, record({ ref, value: str })])),
95
+ 12: tuple(self, self), 13: tuple(self, self), 14: self,
96
+ }));
97
+ const matrix = list(list(i32));
98
+ const property = variant({
99
+ 0: str, 1: i32, 3: matrix, 25: list(tuple(matrix, condition)),
100
+ 4: i64, 5: f64, 6: bool, 7: uint, 8: list(uint), 10: uint, 11: str,
101
+ 12: list(tuple(u8, str, optional(str))), 14: i64, 15: uint,
102
+ 16: dict(i16, uint), 17: dict(str, str),
103
+ 18: record({ ref, prop_ref: uint, is_default: bool }), 31: record({ ref }),
104
+ 19: action, 21: list(uint), 22: condition, 24: list(uint),
105
+ 26: list(str), 27: list(i32), 28: list(f64), 29: list(bool),
106
+ 30: tuple(str, dict(str, str), dict(str, str), str, u8), 13: constant(undefined),
107
+ });
108
+ const message = variant({
109
+ 0: record({ els: list(element), props: list(property), ts: i32 }),
110
+ 1: record({ ref: uint, t: optional(i32) }), 2: action,
111
+ });
112
+ while (offset < bytes.length) {
113
+ end = bytes.length;
114
+ const length = view.getUint32(take(4), true);
115
+ end = offset + length;
116
+ if (end > bytes.length) throw new Error("truncated jetfuel frame");
117
+ messages.push(message());
118
+ if (offset !== end) throw new Error("unexpected jetfuel frame data");
119
+ }
120
+ return { messages, strings };
121
+ }