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/README.md +9 -1
- package/package.json +4 -3
- package/src/castle-profile.js +18 -0
- package/src/castle.js +72 -17
- package/src/flow-jetfuel.js +122 -48
- package/src/helpers/media.js +2 -2
- package/src/index.js +41 -17
- package/src/parsers/jetfuel-messages.js +121 -0
- package/src/vendor/castle-sdk.min.js +6 -3
- package/src/scripts/castle-mint-test.js +0 -8
- package/src/scripts/login-test.js +0 -28
- package/src/scripts/verify-session.js +0 -11
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:
|
|
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
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
+
}
|