emusks 2.3.5 → 2.3.7
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/package.json +1 -1
- package/src/castle.js +261 -0
- package/src/flow-jetfuel.js +288 -0
- package/src/flow.js +2 -2
- package/src/helpers/media.js +2 -2
- package/src/index.js +5 -1
- package/src/scripts/castle-mint-test.js +8 -0
- package/src/scripts/login-test.js +28 -0
- package/src/scripts/verify-session.js +11 -0
- package/src/vendor/castle-sdk.min.js +3 -0
package/package.json
CHANGED
package/src/castle.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import vm from "node:vm";
|
|
5
|
+
import { parseHTML } from "linkedom";
|
|
6
|
+
|
|
7
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_CASTLE_PK = "pk_AvRa79bHyJSYSQHnRpcVtzyxetSvFerx";
|
|
10
|
+
export const CASTLE_MODULE_ID = "84197";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_UA =
|
|
13
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36";
|
|
14
|
+
|
|
15
|
+
function pluginArray() {
|
|
16
|
+
const names = [
|
|
17
|
+
"PDF Viewer",
|
|
18
|
+
"Chrome PDF Viewer",
|
|
19
|
+
"Chromium PDF Viewer",
|
|
20
|
+
"Microsoft Edge PDF Viewer",
|
|
21
|
+
"WebKit built-in PDF",
|
|
22
|
+
];
|
|
23
|
+
const arr = names.map((name) => ({
|
|
24
|
+
name,
|
|
25
|
+
description: "Portable Document Format",
|
|
26
|
+
filename: "internal-pdf-viewer",
|
|
27
|
+
length: 2,
|
|
28
|
+
}));
|
|
29
|
+
arr.item = (i) => arr[i] || null;
|
|
30
|
+
arr.namedItem = (n) => arr.find((p) => p.name === n) || null;
|
|
31
|
+
arr.refresh = () => {};
|
|
32
|
+
return arr;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mimeTypeArray() {
|
|
36
|
+
const arr = [
|
|
37
|
+
{ type: "application/pdf", suffixes: "pdf", description: "Portable Document Format" },
|
|
38
|
+
{ type: "text/pdf", suffixes: "pdf", description: "Portable Document Format" },
|
|
39
|
+
];
|
|
40
|
+
arr.item = (i) => arr[i] || null;
|
|
41
|
+
arr.namedItem = (n) => arr.find((m) => m.type === n) || null;
|
|
42
|
+
return arr;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildSandbox(userAgent) {
|
|
46
|
+
const { document } = parseHTML(
|
|
47
|
+
"<!DOCTYPE html><html><head><title>X</title></head><body></body></html>",
|
|
48
|
+
);
|
|
49
|
+
const define = (prop, value) => {
|
|
50
|
+
try {
|
|
51
|
+
Object.defineProperty(document, prop, { get: () => value, configurable: true });
|
|
52
|
+
} catch {}
|
|
53
|
+
};
|
|
54
|
+
define("cookie", "");
|
|
55
|
+
define("readyState", "complete");
|
|
56
|
+
define("referrer", "https://x.com/");
|
|
57
|
+
define("visibilityState", "visible");
|
|
58
|
+
define("hidden", false);
|
|
59
|
+
document.hasFocus = () => true;
|
|
60
|
+
|
|
61
|
+
const s = {};
|
|
62
|
+
s.globalThis = s;
|
|
63
|
+
s.self = s;
|
|
64
|
+
s.window = s;
|
|
65
|
+
s.global = s;
|
|
66
|
+
s.top = s;
|
|
67
|
+
s.parent = s;
|
|
68
|
+
s.frames = s;
|
|
69
|
+
s.name = "";
|
|
70
|
+
s.closed = false;
|
|
71
|
+
s.origin = "https://x.com";
|
|
72
|
+
s.isSecureContext = true;
|
|
73
|
+
|
|
74
|
+
s.document = document;
|
|
75
|
+
s.navigator = {
|
|
76
|
+
userAgent,
|
|
77
|
+
appVersion: userAgent.replace("Mozilla/", ""),
|
|
78
|
+
appName: "Netscape",
|
|
79
|
+
appCodeName: "Mozilla",
|
|
80
|
+
platform: "MacIntel",
|
|
81
|
+
vendor: "Google Inc.",
|
|
82
|
+
vendorSub: "",
|
|
83
|
+
product: "Gecko",
|
|
84
|
+
productSub: "20030107",
|
|
85
|
+
language: "en-US",
|
|
86
|
+
languages: ["en-US", "en"],
|
|
87
|
+
hardwareConcurrency: 10,
|
|
88
|
+
deviceMemory: 8,
|
|
89
|
+
maxTouchPoints: 0,
|
|
90
|
+
cookieEnabled: true,
|
|
91
|
+
onLine: true,
|
|
92
|
+
doNotTrack: null,
|
|
93
|
+
webdriver: false,
|
|
94
|
+
pdfViewerEnabled: true,
|
|
95
|
+
plugins: pluginArray(),
|
|
96
|
+
mimeTypes: mimeTypeArray(),
|
|
97
|
+
userAgentData: {
|
|
98
|
+
brands: [
|
|
99
|
+
{ brand: "Not(A:Brand", version: "8" },
|
|
100
|
+
{ brand: "Chromium", version: "144" },
|
|
101
|
+
{ brand: "Google Chrome", version: "144" },
|
|
102
|
+
],
|
|
103
|
+
mobile: false,
|
|
104
|
+
platform: "macOS",
|
|
105
|
+
getHighEntropyValues: async () => ({
|
|
106
|
+
platform: "macOS",
|
|
107
|
+
platformVersion: "15.0.0",
|
|
108
|
+
architecture: "arm",
|
|
109
|
+
model: "",
|
|
110
|
+
uaFullVersion: "144.0.0.0",
|
|
111
|
+
bitness: "64",
|
|
112
|
+
}),
|
|
113
|
+
},
|
|
114
|
+
permissions: { query: async () => ({ state: "prompt" }) },
|
|
115
|
+
mediaDevices: { enumerateDevices: async () => [] },
|
|
116
|
+
connection: { effectiveType: "4g", rtt: 50, downlink: 10, saveData: false },
|
|
117
|
+
sendBeacon: () => true,
|
|
118
|
+
javaEnabled: () => false,
|
|
119
|
+
};
|
|
120
|
+
s.screen = {
|
|
121
|
+
width: 1512,
|
|
122
|
+
height: 982,
|
|
123
|
+
availWidth: 1512,
|
|
124
|
+
availHeight: 944,
|
|
125
|
+
colorDepth: 30,
|
|
126
|
+
pixelDepth: 30,
|
|
127
|
+
availLeft: 0,
|
|
128
|
+
availTop: 0,
|
|
129
|
+
orientation: { type: "landscape-primary", angle: 0 },
|
|
130
|
+
};
|
|
131
|
+
s.location = {
|
|
132
|
+
href: "https://x.com/i/flow/login",
|
|
133
|
+
protocol: "https:",
|
|
134
|
+
host: "x.com",
|
|
135
|
+
hostname: "x.com",
|
|
136
|
+
origin: "https://x.com",
|
|
137
|
+
pathname: "/i/flow/login",
|
|
138
|
+
search: "",
|
|
139
|
+
hash: "",
|
|
140
|
+
};
|
|
141
|
+
s.history = { length: 2, state: null, pushState: () => {}, replaceState: () => {} };
|
|
142
|
+
|
|
143
|
+
const store = {};
|
|
144
|
+
s.localStorage = {
|
|
145
|
+
getItem: (k) => (k in store ? store[k] : null),
|
|
146
|
+
setItem: (k, v) => {
|
|
147
|
+
store[k] = String(v);
|
|
148
|
+
},
|
|
149
|
+
removeItem: (k) => {
|
|
150
|
+
delete store[k];
|
|
151
|
+
},
|
|
152
|
+
clear: () => {
|
|
153
|
+
for (const k in store) delete store[k];
|
|
154
|
+
},
|
|
155
|
+
key: (i) => Object.keys(store)[i] ?? null,
|
|
156
|
+
get length() {
|
|
157
|
+
return Object.keys(store).length;
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
s.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {}, clear: () => {} };
|
|
161
|
+
|
|
162
|
+
s.innerWidth = 1512;
|
|
163
|
+
s.innerHeight = 850;
|
|
164
|
+
s.outerWidth = 1512;
|
|
165
|
+
s.outerHeight = 944;
|
|
166
|
+
s.screenX = 0;
|
|
167
|
+
s.screenY = 0;
|
|
168
|
+
s.pageXOffset = 0;
|
|
169
|
+
s.pageYOffset = 0;
|
|
170
|
+
s.devicePixelRatio = 2;
|
|
171
|
+
s.chrome = { app: { isInstalled: false }, runtime: {}, csi: () => ({}), loadTimes: () => ({}) };
|
|
172
|
+
s.Intl = Intl;
|
|
173
|
+
s.matchMedia = (q) => ({
|
|
174
|
+
matches: false,
|
|
175
|
+
media: q,
|
|
176
|
+
addEventListener: () => {},
|
|
177
|
+
removeEventListener: () => {},
|
|
178
|
+
addListener: () => {},
|
|
179
|
+
removeListener: () => {},
|
|
180
|
+
});
|
|
181
|
+
s.addEventListener = () => {};
|
|
182
|
+
s.removeEventListener = () => {};
|
|
183
|
+
s.dispatchEvent = () => true;
|
|
184
|
+
s.requestAnimationFrame = (cb) => setTimeout(() => cb(performance.now()), 0);
|
|
185
|
+
s.cancelAnimationFrame = (id) => clearTimeout(id);
|
|
186
|
+
s.requestIdleCallback = (cb) => setTimeout(() => cb({ timeRemaining: () => 50, didTimeout: false }), 0);
|
|
187
|
+
s.cancelIdleCallback = (id) => clearTimeout(id);
|
|
188
|
+
s.setTimeout = setTimeout;
|
|
189
|
+
s.clearTimeout = clearTimeout;
|
|
190
|
+
s.setInterval = setInterval;
|
|
191
|
+
s.clearInterval = clearInterval;
|
|
192
|
+
s.setImmediate = setImmediate;
|
|
193
|
+
s.queueMicrotask = queueMicrotask;
|
|
194
|
+
s.performance = performance;
|
|
195
|
+
s.crypto = globalThis.crypto;
|
|
196
|
+
s.btoa = (str) => Buffer.from(str, "binary").toString("base64");
|
|
197
|
+
s.atob = (str) => Buffer.from(str, "base64").toString("binary");
|
|
198
|
+
s.TextEncoder = TextEncoder;
|
|
199
|
+
s.TextDecoder = TextDecoder;
|
|
200
|
+
s.console = { log: () => {}, warn: () => {}, error: () => {}, info: () => {}, debug: () => {} };
|
|
201
|
+
s.Image = class {
|
|
202
|
+
set src(_v) {}
|
|
203
|
+
};
|
|
204
|
+
s.XMLHttpRequest = class {
|
|
205
|
+
open() {}
|
|
206
|
+
setRequestHeader() {}
|
|
207
|
+
send() {}
|
|
208
|
+
addEventListener() {}
|
|
209
|
+
};
|
|
210
|
+
s.getComputedStyle = () => ({ getPropertyValue: () => "" });
|
|
211
|
+
return s;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function createCastleMinter(opts = {}) {
|
|
215
|
+
const {
|
|
216
|
+
pk = DEFAULT_CASTLE_PK,
|
|
217
|
+
userAgent = DEFAULT_UA,
|
|
218
|
+
sdkPath = join(HERE, "vendor", "castle-sdk.min.js"),
|
|
219
|
+
sdkSource,
|
|
220
|
+
} = opts;
|
|
221
|
+
|
|
222
|
+
const src = sdkSource ?? readFileSync(sdkPath, "utf8");
|
|
223
|
+
const sandbox = buildSandbox(userAgent);
|
|
224
|
+
|
|
225
|
+
let factory = null;
|
|
226
|
+
sandbox.webpackChunk_twitter_responsive_web = {
|
|
227
|
+
push: (entry) => {
|
|
228
|
+
const modules = entry?.[1];
|
|
229
|
+
if (modules && modules[CASTLE_MODULE_ID]) factory = modules[CASTLE_MODULE_ID];
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const ctx = vm.createContext(sandbox);
|
|
234
|
+
vm.runInContext(src, ctx, { filename: "castle-sdk.min.js" });
|
|
235
|
+
if (!factory) throw new Error(`castle sdk module ${CASTLE_MODULE_ID} not found (X may have rotated the chunk)`);
|
|
236
|
+
|
|
237
|
+
const moduleObj = { exports: {} };
|
|
238
|
+
factory(moduleObj, moduleObj.exports);
|
|
239
|
+
const api = moduleObj.exports;
|
|
240
|
+
if (typeof api.configure !== "function" || typeof api.createRequestToken !== "function") {
|
|
241
|
+
throw new Error("castle sdk did not expose configure/createRequestToken (chunk changed)");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const configured = api.configure({ pk });
|
|
245
|
+
|
|
246
|
+
return async function mint() {
|
|
247
|
+
const instance = await Promise.resolve(configured);
|
|
248
|
+
const token = instance?.createRequestToken
|
|
249
|
+
? await instance.createRequestToken()
|
|
250
|
+
: await api.createRequestToken();
|
|
251
|
+
if (!token) throw new Error("castle createRequestToken returned empty");
|
|
252
|
+
return token;
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let sharedMinter = null;
|
|
257
|
+
|
|
258
|
+
export async function mintCastleToken(opts = {}) {
|
|
259
|
+
if (!sharedMinter || opts.fresh) sharedMinter = createCastleMinter(opts);
|
|
260
|
+
return sharedMinter();
|
|
261
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { ClientTransaction, handleXMigration } from "x-client-transaction-id";
|
|
2
|
+
import { mintCastleToken } from "./castle.js";
|
|
3
|
+
import clients from "./clients.js";
|
|
4
|
+
import getCycleTLS from "./cycletls.js";
|
|
5
|
+
import { decodeJetfuel } from "./parsers/jetfuel.js";
|
|
6
|
+
|
|
7
|
+
const GUEST_ACTIVATE_URL = "https://api.x.com/1.1/guest/activate.json";
|
|
8
|
+
const JFAPI_BASE = "https://x.com/i/jfapi";
|
|
9
|
+
const SURFACE = "onboarding/web";
|
|
10
|
+
const UA =
|
|
11
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36";
|
|
12
|
+
|
|
13
|
+
const MAX_STEPS = 15;
|
|
14
|
+
|
|
15
|
+
class CookieSession {
|
|
16
|
+
constructor(cycleTLS, proxy, timeout) {
|
|
17
|
+
this.cycleTLS = cycleTLS;
|
|
18
|
+
this.cookies = {};
|
|
19
|
+
this.proxy = proxy;
|
|
20
|
+
this.timeout = timeout;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
cookieString() {
|
|
24
|
+
return Object.entries(this.cookies)
|
|
25
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
26
|
+
.join("; ");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
eat(res) {
|
|
30
|
+
const setCookie = res.headers?.["set-cookie"] || res.headers?.["Set-Cookie"];
|
|
31
|
+
if (!setCookie) return;
|
|
32
|
+
for (const cookie of Array.isArray(setCookie) ? setCookie : [setCookie]) {
|
|
33
|
+
const [key, ...rest] = cookie.split(";")[0].split("=");
|
|
34
|
+
if (rest.length) this.cookies[key.trim()] = rest.join("=").trim();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async request(url, method, { headers = {}, body, responseType } = {}) {
|
|
39
|
+
const h = { ...headers };
|
|
40
|
+
const cookieStr = this.cookieString();
|
|
41
|
+
if (cookieStr) h.Cookie = cookieStr;
|
|
42
|
+
|
|
43
|
+
const res = await this.cycleTLS(
|
|
44
|
+
url,
|
|
45
|
+
{
|
|
46
|
+
body,
|
|
47
|
+
ja3: clients.web.fingerprints.ja3,
|
|
48
|
+
ja4r: clients.web.fingerprints.ja4r,
|
|
49
|
+
userAgent: UA,
|
|
50
|
+
headers: h,
|
|
51
|
+
proxy: this.proxy || undefined,
|
|
52
|
+
responseType,
|
|
53
|
+
timeout: this.timeout,
|
|
54
|
+
},
|
|
55
|
+
method,
|
|
56
|
+
);
|
|
57
|
+
this.eat(res);
|
|
58
|
+
return res;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function jfHeaders(guestToken, tid, theme) {
|
|
63
|
+
const headers = {
|
|
64
|
+
authorization: `Bearer ${clients.web.bearer}`,
|
|
65
|
+
"x-guest-token": guestToken,
|
|
66
|
+
"x-jf-v": "JP-5",
|
|
67
|
+
"x-jf-client-theme": theme,
|
|
68
|
+
"x-twitter-active-user": "yes",
|
|
69
|
+
"x-twitter-client-language": "en",
|
|
70
|
+
"accept-language": "en",
|
|
71
|
+
accept: "*/*",
|
|
72
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
|
73
|
+
Origin: "https://x.com",
|
|
74
|
+
Referer: "https://x.com/i/flow/login",
|
|
75
|
+
};
|
|
76
|
+
if (tid) headers["x-client-transaction-id"] = tid;
|
|
77
|
+
return headers;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function getGuestToken(session) {
|
|
81
|
+
const res = await session.request(GUEST_ACTIVATE_URL, "post", {
|
|
82
|
+
headers: { Authorization: `Bearer ${clients.web.bearer}` },
|
|
83
|
+
});
|
|
84
|
+
let body = res.body ?? res.data;
|
|
85
|
+
if (typeof body === "string") body = JSON.parse(body);
|
|
86
|
+
if (!body?.guest_token) throw new Error("failed to obtain guest token");
|
|
87
|
+
return body.guest_token;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function toBuffer(res) {
|
|
91
|
+
if (typeof res.arrayBuffer === "function") return Buffer.from(await res.arrayBuffer());
|
|
92
|
+
const body = res.body ?? res.data;
|
|
93
|
+
if (Buffer.isBuffer(body)) return body;
|
|
94
|
+
if (body && typeof body === "object" && body.data) return Buffer.from(body.data);
|
|
95
|
+
return Buffer.from(String(body ?? ""), "utf8");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function decodeStrings(buffer) {
|
|
99
|
+
try {
|
|
100
|
+
return decodeJetfuel(buffer).strings || [];
|
|
101
|
+
} catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function extractActionIds(strings) {
|
|
107
|
+
const ids = new Set();
|
|
108
|
+
for (const s of strings) {
|
|
109
|
+
const m = String(s).match(/onboarding\/web\/actions\/([a-z0-9_]+)/i);
|
|
110
|
+
if (m) ids.add(m[1]);
|
|
111
|
+
}
|
|
112
|
+
return [...ids];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function extractServerError(strings) {
|
|
116
|
+
const idx = strings.findIndex((s) => /^errors?$/i.test(String(s).trim()));
|
|
117
|
+
if (idx === -1) return null;
|
|
118
|
+
for (let i = idx + 1; i < strings.length; i++) {
|
|
119
|
+
const s = String(strings[i]).trim();
|
|
120
|
+
if (s.length > 8 && /[a-z]\s[a-z]/i.test(s) && !/^error$/i.test(s)) return s;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function extractUserId(cookies) {
|
|
126
|
+
const twid = (cookies.twid || "").replace(/"/g, "");
|
|
127
|
+
for (const prefix of ["u=", "u%3D"]) {
|
|
128
|
+
if (twid.includes(prefix)) return twid.split(prefix)[1].split("&")[0].replace(/"/g, "");
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function resolve(value, onRequest, type) {
|
|
134
|
+
if (value != null && value !== "") return value;
|
|
135
|
+
if (onRequest) {
|
|
136
|
+
const v = await onRequest(type);
|
|
137
|
+
if (v != null && v !== "") return v;
|
|
138
|
+
}
|
|
139
|
+
throw new Error(
|
|
140
|
+
`the login flow needs "${type}" but none was provided. pass it directly or handle it in onRequest.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export default async function flowLoginJetfuel(opts) {
|
|
145
|
+
const {
|
|
146
|
+
username,
|
|
147
|
+
password,
|
|
148
|
+
email,
|
|
149
|
+
phone,
|
|
150
|
+
onRequest,
|
|
151
|
+
getCastleToken = () => mintCastleToken(),
|
|
152
|
+
proxy,
|
|
153
|
+
timeout,
|
|
154
|
+
theme = "light",
|
|
155
|
+
debug,
|
|
156
|
+
} = opts;
|
|
157
|
+
const log = debug ? (...a) => console.error("[jf-flow]", ...a) : () => {};
|
|
158
|
+
|
|
159
|
+
const identifier = username || email;
|
|
160
|
+
if (!identifier) throw new Error("username or email is required for login");
|
|
161
|
+
if (!password) throw new Error("password is required for login");
|
|
162
|
+
|
|
163
|
+
const cycleTLS = await getCycleTLS();
|
|
164
|
+
const session = new CookieSession(cycleTLS, proxy, timeout);
|
|
165
|
+
|
|
166
|
+
const migrationDoc = await handleXMigration();
|
|
167
|
+
const transaction = new ClientTransaction(migrationDoc);
|
|
168
|
+
await transaction.initialize();
|
|
169
|
+
const tid = (method, path) => transaction.generateTransactionId(method, path);
|
|
170
|
+
|
|
171
|
+
const guestToken = await getGuestToken(session);
|
|
172
|
+
log("guest token", guestToken);
|
|
173
|
+
|
|
174
|
+
const landingPath = `/i/jfapi/${SURFACE}?mode=login`;
|
|
175
|
+
const landing = await session.request(`${JFAPI_BASE}/${SURFACE}?mode=login`, "get", {
|
|
176
|
+
headers: jfHeaders(guestToken, await tid("GET", landingPath), theme),
|
|
177
|
+
responseType: "arraybuffer",
|
|
178
|
+
});
|
|
179
|
+
if (landing.status !== 200) throw new Error(`landing failed: ${landing.status}`);
|
|
180
|
+
let strings = decodeStrings(await toBuffer(landing));
|
|
181
|
+
log("landing actions", extractActionIds(strings));
|
|
182
|
+
|
|
183
|
+
const post = async (action, fields) => {
|
|
184
|
+
const path = `/i/jfapi/${SURFACE}/actions/${action}`;
|
|
185
|
+
const castle = await getCastleToken(action);
|
|
186
|
+
const form = new URLSearchParams({ ...fields, $castle_token: castle }).toString();
|
|
187
|
+
const headers = jfHeaders(guestToken, await tid("POST", path), theme);
|
|
188
|
+
headers["content-type"] = "application/x-www-form-urlencoded";
|
|
189
|
+
if (session.cookies.ct0) headers["x-csrf-token"] = session.cookies.ct0;
|
|
190
|
+
const res = await session.request(`${JFAPI_BASE}/${SURFACE}/actions/${action}`, "post", {
|
|
191
|
+
headers,
|
|
192
|
+
body: form,
|
|
193
|
+
responseType: "arraybuffer",
|
|
194
|
+
});
|
|
195
|
+
const buffer = await toBuffer(res);
|
|
196
|
+
const responseStrings = decodeStrings(buffer);
|
|
197
|
+
log(`POST ${action} -> ${res.status}`, extractActionIds(responseStrings));
|
|
198
|
+
if (res.status !== 200) {
|
|
199
|
+
throw new Error(`action ${action} failed: ${res.status} - ${buffer.toString("utf8").slice(0, 200)}`);
|
|
200
|
+
}
|
|
201
|
+
const genericError = responseStrings.find((s) => /something went wrong/i.test(s));
|
|
202
|
+
if (genericError && responseStrings.length < 6) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`action ${action} rejected ("${genericError}") - usually an invalid or missing $castle_token.`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const serverError = extractServerError(responseStrings);
|
|
208
|
+
if (serverError) {
|
|
209
|
+
if (/temporarily limited|rate.?limit|try again later/i.test(serverError)) {
|
|
210
|
+
throw new Error(`login rate-limited by X: "${serverError.trim()}" (back off this account/IP)`);
|
|
211
|
+
}
|
|
212
|
+
throw new Error(`action ${action} error: "${serverError.trim()}"`);
|
|
213
|
+
}
|
|
214
|
+
return responseStrings;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const sessionTokenFrom = (arr) => {
|
|
218
|
+
const i = arr.findIndex((s) => /^session_token$/i.test(String(s).trim()));
|
|
219
|
+
if (i === -1) return null;
|
|
220
|
+
const v = String(arr[i + 1] || "").trim();
|
|
221
|
+
return /^[0-9a-f-]{20,}$/i.test(v) ? v : null;
|
|
222
|
+
};
|
|
223
|
+
let sessionToken = null;
|
|
224
|
+
const withSession = (fields) => (sessionToken ? { session_token: sessionToken, ...fields } : fields);
|
|
225
|
+
|
|
226
|
+
strings = await post("begin_login", { username_or_email: identifier });
|
|
227
|
+
sessionToken = sessionTokenFrom(strings) || sessionToken;
|
|
228
|
+
|
|
229
|
+
let step = 0;
|
|
230
|
+
while (!session.cookies.auth_token && step < MAX_STEPS) {
|
|
231
|
+
step++;
|
|
232
|
+
const lower = strings.map((s) => String(s).toLowerCase());
|
|
233
|
+
const has = (needle) => lower.some((s) => s.includes(needle));
|
|
234
|
+
|
|
235
|
+
if (has("incorrect") || has("wrong password")) {
|
|
236
|
+
throw new Error("login denied: the password was incorrect");
|
|
237
|
+
}
|
|
238
|
+
if (has("suspend") || has("locked") || has("denied")) {
|
|
239
|
+
throw new Error("login denied: account locked or suspended");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const wantsPasswordEntry = has("enter your password") || has("login_enter_password");
|
|
243
|
+
const wantsTwoFactor = has("two_factor") || has("2fa") || has("authentication code");
|
|
244
|
+
const wantsEmailCode = has("acid") || has("verifycode") || has("verification code") ||
|
|
245
|
+
(has("verification") && has("code"));
|
|
246
|
+
const wantsAlternate = has("alternate") || has("login_enter_alternate_identifier");
|
|
247
|
+
|
|
248
|
+
if (wantsTwoFactor) {
|
|
249
|
+
const code = await resolve(null, onRequest, "two_factor_code");
|
|
250
|
+
strings = await post("login_enter_two_factor", withSession({ text: code }));
|
|
251
|
+
sessionToken = sessionTokenFrom(strings) || sessionToken;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (wantsEmailCode) {
|
|
255
|
+
const code = await resolve(null, onRequest, "email_code");
|
|
256
|
+
strings = await post("login_acid", withSession({ text: code }));
|
|
257
|
+
sessionToken = sessionTokenFrom(strings) || sessionToken;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (wantsAlternate) {
|
|
261
|
+
const value = await resolve(email || phone, onRequest, "alternate_identifier");
|
|
262
|
+
strings = await post("login_enter_alternate_identifier", withSession({ text: value }));
|
|
263
|
+
sessionToken = sessionTokenFrom(strings) || sessionToken;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (wantsPasswordEntry || has("password")) {
|
|
267
|
+
strings = await post("login_enter_password", withSession({ password }));
|
|
268
|
+
sessionToken = sessionTokenFrom(strings) || sessionToken;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
throw new Error(
|
|
273
|
+
`unhandled jetfuel login step. decoded actions: [${extractActionIds(strings).join(", ")}]. ` +
|
|
274
|
+
`strings sample: ${JSON.stringify(strings.slice(0, 20))}`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!session.cookies.auth_token) {
|
|
279
|
+
throw new Error(`login did not complete after ${step} steps`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
authToken: session.cookies.auth_token,
|
|
284
|
+
csrfToken: session.cookies.ct0 || null,
|
|
285
|
+
userId: extractUserId(session.cookies),
|
|
286
|
+
cookies: { ...session.cookies },
|
|
287
|
+
};
|
|
288
|
+
}
|
package/src/flow.js
CHANGED
|
@@ -135,7 +135,7 @@ class CookieSession {
|
|
|
135
135
|
|
|
136
136
|
function getFlowHeaders(guestToken) {
|
|
137
137
|
const headers = {
|
|
138
|
-
Authorization: `Bearer ${clients.
|
|
138
|
+
Authorization: `Bearer ${clients.web.bearer}`,
|
|
139
139
|
"Content-Type": "application/json",
|
|
140
140
|
Accept: "*/*",
|
|
141
141
|
"Accept-Language": "en-US",
|
|
@@ -178,7 +178,7 @@ async function makeRequest(session, headers, flowToken, subtaskData) {
|
|
|
178
178
|
|
|
179
179
|
async function getGuestToken(session) {
|
|
180
180
|
const response = await session.post(GUEST_ACTIVATE_URL, {
|
|
181
|
-
headers: { Authorization: `Bearer ${clients.
|
|
181
|
+
headers: { Authorization: `Bearer ${clients.web.bearer}` },
|
|
182
182
|
});
|
|
183
183
|
|
|
184
184
|
if (response.status !== 200) {
|
package/src/helpers/media.js
CHANGED
|
@@ -219,7 +219,7 @@ export async function create(source, opts = {}) {
|
|
|
219
219
|
});
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
return { media_id: mediaId
|
|
222
|
+
return { ...finalizeData, media_id: mediaId };
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
export async function createFromUrl(url, opts = {}) {
|
|
@@ -274,7 +274,7 @@ export async function createFromUrl(url, opts = {}) {
|
|
|
274
274
|
});
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
return { media_id: mediaId
|
|
277
|
+
return { ...initData, media_id: mediaId };
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
export async function createMetadata(mediaId, altText, opts = {}) {
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { ClientTransaction, handleXMigration } from "x-client-transaction-id";
|
|
|
2
2
|
import clients from "./clients.js";
|
|
3
3
|
import getCycleTLS from "./cycletls.js";
|
|
4
4
|
import flowLogin from "./flow.js";
|
|
5
|
+
import flowLoginJetfuel from "./flow-jetfuel.js";
|
|
5
6
|
import graphql, { GRAPHQL_ENDPOINTS } from "./graphql.js";
|
|
6
7
|
import grokApi from "./grok.js";
|
|
7
8
|
import initHelpers from "./helpers/index.js";
|
|
@@ -64,13 +65,16 @@ export default class Emusks {
|
|
|
64
65
|
if (!p.username) throw new Error("username is required for password login");
|
|
65
66
|
if (!p.password) throw new Error("password is required for password login");
|
|
66
67
|
|
|
67
|
-
const
|
|
68
|
+
const runFlow = p.flow === "classic" ? flowLogin : flowLoginJetfuel;
|
|
69
|
+
const flowResult = await runFlow({
|
|
68
70
|
username: p.username,
|
|
69
71
|
password: p.password,
|
|
70
72
|
email: p.email,
|
|
71
73
|
phone: p.phone,
|
|
72
74
|
onRequest: p.onRequest,
|
|
75
|
+
getCastleToken: p.getCastleToken,
|
|
73
76
|
proxy: p.proxy,
|
|
77
|
+
debug: p.debug,
|
|
74
78
|
});
|
|
75
79
|
|
|
76
80
|
p = {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { mintCastleToken } from "../castle.js";
|
|
2
|
+
|
|
3
|
+
const t0 = Date.now();
|
|
4
|
+
const token = await mintCastleToken();
|
|
5
|
+
console.error(`[castle] minted in ${Date.now() - t0}ms`);
|
|
6
|
+
console.error(`[castle] len=${token.length} format=${/^[A-Za-z0-9]+\|/.test(token) ? "ok (<id>|<payload>)" : "unexpected"}`);
|
|
7
|
+
console.error(`[castle] head=${token.slice(0, 24)}`);
|
|
8
|
+
process.exit(0);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import flowLoginJetfuel from "../flow-jetfuel.js";
|
|
3
|
+
|
|
4
|
+
const CODE_FILE = "/tmp/x-code.txt";
|
|
5
|
+
const pollCode = async (type) => {
|
|
6
|
+
console.error(`[test] NEED ${type} -> write it to ${CODE_FILE}`);
|
|
7
|
+
for (let i = 0; i < 90; i++) {
|
|
8
|
+
if (existsSync(CODE_FILE)) { const c = readFileSync(CODE_FILE, "utf8").trim(); unlinkSync(CODE_FILE); if (c) { console.error(`[test] got ${type}`); return c; } }
|
|
9
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
10
|
+
}
|
|
11
|
+
throw new Error(`${type} not provided within timeout`);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const r = await flowLoginJetfuel({
|
|
16
|
+
username: process.env.TEST_USER,
|
|
17
|
+
password: process.env.TEST_PW,
|
|
18
|
+
proxy: process.env.PROXY,
|
|
19
|
+
timeout: 30,
|
|
20
|
+
debug: true,
|
|
21
|
+
onRequest: pollCode,
|
|
22
|
+
});
|
|
23
|
+
console.error("[test] LOGIN OK userId=", r.userId, "auth_token?", Boolean(r.authToken), "ct0?", Boolean(r.csrfToken));
|
|
24
|
+
if (process.env.SESSION_OUT) { writeFileSync(process.env.SESSION_OUT, JSON.stringify(r, null, 2), { mode: 0o600 }); console.error("[test] session ->", process.env.SESSION_OUT); }
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.error("[test] FAILED:", e.message);
|
|
27
|
+
}
|
|
28
|
+
process.exit(0);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import Emusks from "../index.js";
|
|
3
|
+
|
|
4
|
+
const s = JSON.parse(readFileSync(process.env.SESSION_IN || "/tmp/emusks-session.json", "utf8"));
|
|
5
|
+
const client = new Emusks();
|
|
6
|
+
await client.login({ auth_token: s.authToken || s.auth_token, endpoint: "web", transactionIds: true });
|
|
7
|
+
|
|
8
|
+
const res = await client.v1_1("get:account/settings", {});
|
|
9
|
+
const body = await res.json();
|
|
10
|
+
console.error("[verify] status", res.status, "| @" + body.screen_name, "| country", body.country_code);
|
|
11
|
+
process.exit(0);
|