c-capcut 1.0.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/assets/logo.svg +37 -0
- package/config.json +24 -0
- package/data/accounts.json +67 -0
- package/data/email-db.json +7 -0
- package/data/result.txt +5 -0
- package/index.js +42 -0
- package/package.json +13 -0
- package/src/commands/create.js +271 -0
- package/src/commands/export.js +58 -0
- package/src/config.js +36 -0
- package/src/lib/capcut.js +322 -0
- package/src/lib/email-gen.js +120 -0
- package/src/lib/http-client.js +149 -0
- package/src/lib/otp.js +157 -0
- package/src/lib/storage.js +76 -0
- package/src/ui/banner.js +134 -0
- package/src/ui/display.js +175 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/capcut.js
|
|
3
|
+
* Full CapCut account registration via direct HTTP fetch.
|
|
4
|
+
* No browser required - pure API calls based on HAR analysis.
|
|
5
|
+
*
|
|
6
|
+
* CapCut uses the TikTok "passport" account system. Request bodies are
|
|
7
|
+
* obfuscated with "mix_mode" encoding: each character is XOR-ed with 0x05
|
|
8
|
+
* and emitted as 2-digit lowercase hex.
|
|
9
|
+
*
|
|
10
|
+
* Flow (login-row.www.capcut.com / www.capcut.com):
|
|
11
|
+
* 1. GET www.capcut.com/ → bootstrap `ttwid` cookie
|
|
12
|
+
* 2. POST /passport/web/region/ → `passport_csrf_token` + region domain
|
|
13
|
+
* 3. POST /passport/web/user/check_email_registered → ensure email is free
|
|
14
|
+
* 4. POST /passport/web/email/send_code/ (type=34) → trigger OTP email
|
|
15
|
+
* 5. (poll mailbox for the 6-digit code)
|
|
16
|
+
* 6. POST /passport/web/email/register_verify_login → create account + session cookies
|
|
17
|
+
* 7. GET /passport/web/account/info/ → confirm account
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
21
|
+
import {
|
|
22
|
+
CookieJar,
|
|
23
|
+
fetchCookie,
|
|
24
|
+
fetchRedirect,
|
|
25
|
+
USER_AGENT,
|
|
26
|
+
} from "./http-client.js";
|
|
27
|
+
import {
|
|
28
|
+
CAPCUT_AID,
|
|
29
|
+
CAPCUT_SDK_VERSION,
|
|
30
|
+
CAPCUT_LANGUAGE,
|
|
31
|
+
CAPCUT_REGION,
|
|
32
|
+
CAPCUT_LOGIN_DOMAIN,
|
|
33
|
+
} from "../config.js";
|
|
34
|
+
|
|
35
|
+
export const TOTAL_STEPS = 6;
|
|
36
|
+
|
|
37
|
+
// ─── mix_mode Encoding (XOR 0x05 → hex) ─────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** Encode a plaintext value into CapCut/TikTok mix_mode hex. */
|
|
40
|
+
export function mixEncode(str) {
|
|
41
|
+
let out = "";
|
|
42
|
+
for (let i = 0; i < str.length; i++) {
|
|
43
|
+
const x = (str.charCodeAt(i) ^ 0x05) & 0xff;
|
|
44
|
+
out += x.toString(16).padStart(2, "0");
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Decode mix_mode hex back to plaintext (useful for debugging). */
|
|
50
|
+
export function mixDecode(hex) {
|
|
51
|
+
let out = "";
|
|
52
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
53
|
+
out += String.fromCharCode(parseInt(hex.substr(i, 2), 16) ^ 0x05);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Anti-bot fingerprint helpers ───────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/** Generate a TikTok-style `verifyFp` device fingerprint. */
|
|
61
|
+
function genVerifyFp() {
|
|
62
|
+
const chars =
|
|
63
|
+
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
64
|
+
const t = chars.length;
|
|
65
|
+
const r = Date.now().toString(36);
|
|
66
|
+
const o = new Array(36);
|
|
67
|
+
o[8] = o[13] = o[18] = o[23] = "_";
|
|
68
|
+
o[14] = "4";
|
|
69
|
+
for (let i = 0; i < 36; i++) {
|
|
70
|
+
if (!o[i]) {
|
|
71
|
+
const n = Math.floor(Math.random() * t);
|
|
72
|
+
o[i] = chars[i === 19 ? (3 & n) | 8 : n];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return `verify_${r}_${o.join("")}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Generate a random `msToken` (best-effort; real one is produced by the SDK). */
|
|
79
|
+
function genMsToken(len = 107) {
|
|
80
|
+
const chars =
|
|
81
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
82
|
+
let out = "";
|
|
83
|
+
const bytes = randomBytes(len);
|
|
84
|
+
for (let i = 0; i < len; i++) out += chars[bytes[i] % chars.length];
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Random 32-byte hex device id used as `hashed_id` for the region call. */
|
|
89
|
+
function genHashedId() {
|
|
90
|
+
return randomBytes(32).toString("hex");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Date of birth ──────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function randomBirthday() {
|
|
96
|
+
const year = 1996 + Math.floor(Math.random() * 8); // 1996..2003
|
|
97
|
+
const month = Math.floor(Math.random() * 12) + 1;
|
|
98
|
+
const day = Math.floor(Math.random() * 28) + 1;
|
|
99
|
+
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Registration ───────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Register a CapCut account via fetch (no browser).
|
|
106
|
+
* @param {{ email: string, password: string, fullName: string }} account
|
|
107
|
+
* @param {{ askOtpFn: (email:string, opts?:{after?:number})=>Promise<string[]>, onProgress?: (step:number,msg:string)=>void }} opts
|
|
108
|
+
* @returns {Promise<Object>} account data with passport info
|
|
109
|
+
*/
|
|
110
|
+
export async function registerAccount(account, opts = {}) {
|
|
111
|
+
const { email, password } = account;
|
|
112
|
+
const { askOtpFn, onProgress } = opts;
|
|
113
|
+
const progress = (step, msg) => onProgress?.(step, msg);
|
|
114
|
+
|
|
115
|
+
const jar = new CookieJar();
|
|
116
|
+
const verifyFp = genVerifyFp();
|
|
117
|
+
const msToken = genMsToken();
|
|
118
|
+
|
|
119
|
+
let loginDomain = CAPCUT_LOGIN_DOMAIN;
|
|
120
|
+
|
|
121
|
+
// Shared query params for passport endpoints.
|
|
122
|
+
const baseQuery = () =>
|
|
123
|
+
new URLSearchParams({
|
|
124
|
+
aid: CAPCUT_AID,
|
|
125
|
+
account_sdk_source: "web",
|
|
126
|
+
sdk_version: CAPCUT_SDK_VERSION,
|
|
127
|
+
language: CAPCUT_LANGUAGE,
|
|
128
|
+
verifyFp,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const passportHeaders = (csrf) => ({
|
|
132
|
+
Accept: "application/json, text/javascript",
|
|
133
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
134
|
+
Origin: "https://www.capcut.com",
|
|
135
|
+
Referer: "https://www.capcut.com/",
|
|
136
|
+
"x-tt-passport-csrf-token": csrf || "",
|
|
137
|
+
"User-Agent": USER_AGENT,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const post = (path, csrf, bodyParams, extraQuery = {}) => {
|
|
141
|
+
const q = baseQuery();
|
|
142
|
+
for (const [k, v] of Object.entries(extraQuery)) q.set(k, v);
|
|
143
|
+
const url = `${loginDomain}${path}?${q.toString()}`;
|
|
144
|
+
return fetchCookie(jar, url, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: passportHeaders(csrf),
|
|
147
|
+
body: new URLSearchParams(bodyParams).toString(),
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// ── Step 1: Bootstrap device (ttwid) ─────────────────────────────────────
|
|
152
|
+
progress(1, "Bootstrapping device session...");
|
|
153
|
+
try {
|
|
154
|
+
const { response } = await fetchRedirect(jar, "https://www.capcut.com/", {
|
|
155
|
+
headers: {
|
|
156
|
+
Accept:
|
|
157
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
158
|
+
Referer: "https://www.capcut.com/",
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
await response.text().catch(() => {});
|
|
162
|
+
} catch {
|
|
163
|
+
// Non-fatal: ttwid may still be issued by the region call.
|
|
164
|
+
}
|
|
165
|
+
progress(1, "Device session ready ✓");
|
|
166
|
+
|
|
167
|
+
// ── Step 2: Region → CSRF token + regional login domain ──────────────────
|
|
168
|
+
progress(2, "Resolving region & CSRF token...");
|
|
169
|
+
const regionRes = await post("/passport/web/region/", "", {
|
|
170
|
+
type: "2",
|
|
171
|
+
hashed_id: genHashedId(),
|
|
172
|
+
});
|
|
173
|
+
const regionData = await regionRes.json().catch(() => ({}));
|
|
174
|
+
if (regionData?.data?.domain) loginDomain = regionData.data.domain;
|
|
175
|
+
const csrf = jar.getCookie("passport_csrf_token") || "";
|
|
176
|
+
if (!csrf) throw new Error("Failed to obtain passport CSRF token");
|
|
177
|
+
progress(
|
|
178
|
+
2,
|
|
179
|
+
`CSRF acquired ✓ (region: ${regionData?.data?.country_code || CAPCUT_REGION})`,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// ── Step 3: Check email availability ─────────────────────────────────────
|
|
183
|
+
progress(3, "Checking email availability...");
|
|
184
|
+
const checkRes = await post(
|
|
185
|
+
"/passport/web/user/check_email_registered",
|
|
186
|
+
csrf,
|
|
187
|
+
{
|
|
188
|
+
mix_mode: "1",
|
|
189
|
+
email: mixEncode(email),
|
|
190
|
+
fixed_mix_mode: "1",
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
const checkData = await checkRes.json().catch(() => ({}));
|
|
194
|
+
if (checkData?.data?.is_registered === 1) {
|
|
195
|
+
throw new Error("Email already registered");
|
|
196
|
+
}
|
|
197
|
+
progress(3, "Email available ✓");
|
|
198
|
+
|
|
199
|
+
// ── Step 4: Send OTP code ────────────────────────────────────────────────
|
|
200
|
+
// Record timestamp BEFORE triggering OTP (5s skew buffer for the mail server).
|
|
201
|
+
let otpSentAt = Date.now() - 5_000;
|
|
202
|
+
progress(4, "Requesting verification code...");
|
|
203
|
+
const sendRes = await post("/passport/web/email/send_code/", csrf, {
|
|
204
|
+
mix_mode: "1",
|
|
205
|
+
email: mixEncode(email),
|
|
206
|
+
password: mixEncode(password),
|
|
207
|
+
type: "34",
|
|
208
|
+
fixed_mix_mode: "1",
|
|
209
|
+
});
|
|
210
|
+
const sendData = await sendRes.json().catch(() => ({}));
|
|
211
|
+
if (sendData?.message !== "success") {
|
|
212
|
+
const desc =
|
|
213
|
+
sendData?.data?.description || sendData?.data?.error_code || "unknown";
|
|
214
|
+
throw new Error(`send_code failed: ${desc}`);
|
|
215
|
+
}
|
|
216
|
+
progress(4, "Verification code sent ✓");
|
|
217
|
+
|
|
218
|
+
// ── Step 5: Wait for OTP (with resend) ───────────────────────────────────
|
|
219
|
+
const OTP_POLL_MS = 30_000;
|
|
220
|
+
const MAX_RESENDS = 2;
|
|
221
|
+
let otpCodes = null;
|
|
222
|
+
|
|
223
|
+
for (let attempt = 0; attempt <= MAX_RESENDS; attempt++) {
|
|
224
|
+
if (attempt > 0) {
|
|
225
|
+
otpSentAt = Date.now() - 5_000;
|
|
226
|
+
progress(5, `Resending code (${attempt}/${MAX_RESENDS})...`);
|
|
227
|
+
await post("/passport/web/email/send_code/", csrf, {
|
|
228
|
+
mix_mode: "1",
|
|
229
|
+
email: mixEncode(email),
|
|
230
|
+
password: mixEncode(password),
|
|
231
|
+
type: "34",
|
|
232
|
+
fixed_mix_mode: "1",
|
|
233
|
+
}).catch(() => {});
|
|
234
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
progress(
|
|
238
|
+
5,
|
|
239
|
+
attempt > 0
|
|
240
|
+
? `Waiting for code (resend ${attempt})...`
|
|
241
|
+
: "Waiting for verification code...",
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const result = await Promise.race([
|
|
246
|
+
askOtpFn(email, { after: otpSentAt }),
|
|
247
|
+
new Promise((_, reject) =>
|
|
248
|
+
setTimeout(() => reject(new Error("timeout")), OTP_POLL_MS),
|
|
249
|
+
),
|
|
250
|
+
]);
|
|
251
|
+
otpCodes = Array.isArray(result) ? result : [result];
|
|
252
|
+
if (otpCodes.length > 0) break;
|
|
253
|
+
} catch {
|
|
254
|
+
if (attempt === MAX_RESENDS) {
|
|
255
|
+
throw new Error("OTP not received after 2 resends");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
progress(5, `Code received ✓ (${otpCodes.length}: ${otpCodes.join(", ")})`);
|
|
261
|
+
|
|
262
|
+
// ── Step 6: Verify & register (try each code, newest first) ──────────────
|
|
263
|
+
const birthday = randomBirthday();
|
|
264
|
+
let regData = null;
|
|
265
|
+
|
|
266
|
+
for (const code of otpCodes) {
|
|
267
|
+
progress(6, `Verifying code (${code})...`);
|
|
268
|
+
const regRes = await post(
|
|
269
|
+
"/passport/web/email/register_verify_login/",
|
|
270
|
+
csrf,
|
|
271
|
+
{
|
|
272
|
+
mix_mode: "1",
|
|
273
|
+
email: mixEncode(email),
|
|
274
|
+
code: mixEncode(code.trim()),
|
|
275
|
+
password: mixEncode(password),
|
|
276
|
+
type: "34",
|
|
277
|
+
birthday,
|
|
278
|
+
force_user_region: CAPCUT_REGION,
|
|
279
|
+
biz_param: "{}",
|
|
280
|
+
fixed_mix_mode: "1",
|
|
281
|
+
},
|
|
282
|
+
{ msToken },
|
|
283
|
+
);
|
|
284
|
+
regData = await regRes.json().catch(() => ({}));
|
|
285
|
+
|
|
286
|
+
if (regData?.data?.user_id || regData?.message === "success") break;
|
|
287
|
+
|
|
288
|
+
const errCode = regData?.data?.error_code;
|
|
289
|
+
progress(6, `Code ${code} rejected (err ${errCode}), trying next...`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!(regData?.data?.user_id || regData?.message === "success")) {
|
|
293
|
+
const desc = regData?.data?.description || JSON.stringify(regData);
|
|
294
|
+
throw new Error(`Registration failed: ${desc}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const userId = regData.data.user_id_str || String(regData.data.user_id || "");
|
|
298
|
+
const screenName = regData.data.screen_name || regData.data.name || "";
|
|
299
|
+
const sessionId = jar.getCookie("sessionid") || "";
|
|
300
|
+
|
|
301
|
+
// ── Confirm account info (best-effort) ───────────────────────────────────
|
|
302
|
+
try {
|
|
303
|
+
const infoUrl = `https://www.capcut.com/passport/web/account/info/?${baseQuery().toString()}`;
|
|
304
|
+
await fetchCookie(jar, infoUrl, {
|
|
305
|
+
headers: passportHeaders(csrf),
|
|
306
|
+
});
|
|
307
|
+
} catch {
|
|
308
|
+
// optional
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const birthdayDisplay = birthday;
|
|
312
|
+
progress(6, `Account created ✓ (uid: ${userId})`);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
...account,
|
|
316
|
+
userId,
|
|
317
|
+
screenName,
|
|
318
|
+
birthday: birthdayDisplay,
|
|
319
|
+
sessionId,
|
|
320
|
+
status: "verified",
|
|
321
|
+
};
|
|
322
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/email-gen.js
|
|
3
|
+
* Email account generator using GoMail API.
|
|
4
|
+
*
|
|
5
|
+
* Flow:
|
|
6
|
+
* 1. GET /api/v1/domains → list available domains
|
|
7
|
+
* 2. Generate random username + pick random domain
|
|
8
|
+
* 3. POST /api/v1/auth/signup → create email account
|
|
9
|
+
* 4. Return ready-to-use email address
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { faker } from "@faker-js/faker";
|
|
13
|
+
|
|
14
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
function apiHeaders(apiKey) {
|
|
17
|
+
return {
|
|
18
|
+
"X-API-Key": apiKey,
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
Accept: "application/json",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function generateSafeNamePart(generator) {
|
|
25
|
+
let name = "";
|
|
26
|
+
do {
|
|
27
|
+
name = generator().replace(/\s+/g, " ").trim();
|
|
28
|
+
} while (!/^[A-Za-z]+(?: [A-Za-z]+)*$/.test(name));
|
|
29
|
+
return name;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toEmailPart(name) {
|
|
33
|
+
return name
|
|
34
|
+
.replace(/[^A-Za-z\s]/g, " ")
|
|
35
|
+
.replace(/\s+/g, "")
|
|
36
|
+
.toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── API Calls ────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Fetch available domains from email service.
|
|
43
|
+
* @param {string} apiUrl - Base URL, e.g. "https://mail.gopretstudio.com"
|
|
44
|
+
* @param {string} apiKey - X-API-Key
|
|
45
|
+
* @returns {Promise<string[]>} array of domain strings
|
|
46
|
+
*/
|
|
47
|
+
export async function fetchDomains(apiUrl, apiKey) {
|
|
48
|
+
const res = await fetch(`${apiUrl}/api/v1/domains`, {
|
|
49
|
+
headers: apiHeaders(apiKey),
|
|
50
|
+
});
|
|
51
|
+
const data = await res.json();
|
|
52
|
+
if (!res.ok) throw new Error(data.error || "Failed to fetch domains");
|
|
53
|
+
const domains = (data.domains || data.data || [])
|
|
54
|
+
.filter((d) => d.is_active !== false)
|
|
55
|
+
.map((d) => d.domain);
|
|
56
|
+
if (domains.length === 0) throw new Error("No active domains available");
|
|
57
|
+
return domains;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create an email account via the email service API.
|
|
62
|
+
* @param {string} apiUrl
|
|
63
|
+
* @param {string} apiKey
|
|
64
|
+
* @param {string} emailAlias - e.g. "john@domain.com"
|
|
65
|
+
* @param {string} password
|
|
66
|
+
* @param {string} [displayName]
|
|
67
|
+
* @returns {Promise<object>} user data from API
|
|
68
|
+
*/
|
|
69
|
+
export async function createEmailAccount(
|
|
70
|
+
apiUrl,
|
|
71
|
+
apiKey,
|
|
72
|
+
emailAlias,
|
|
73
|
+
password,
|
|
74
|
+
displayName,
|
|
75
|
+
) {
|
|
76
|
+
const res = await fetch(`${apiUrl}/api/v1/auth/signup`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: apiHeaders(apiKey),
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
email_alias: emailAlias,
|
|
81
|
+
password,
|
|
82
|
+
display_name: displayName,
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
const data = await res.json();
|
|
86
|
+
if (!res.ok) throw new Error(data.error || "Failed to create email account");
|
|
87
|
+
return data;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Generator ────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create a configured account generator.
|
|
94
|
+
* @param {{ apiUrl: string, apiKey: string, password: string, suffix?: string }} config
|
|
95
|
+
*/
|
|
96
|
+
export function createAccountGenerator(config) {
|
|
97
|
+
const { apiUrl, apiKey, password, suffix = "" } = config;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate a random account and create it on the email service.
|
|
101
|
+
* @returns {Promise<{ email, password, firstName, lastName, fullName }>}
|
|
102
|
+
*/
|
|
103
|
+
return async function generateAccount() {
|
|
104
|
+
// 1. Fetch available domains
|
|
105
|
+
const domains = await fetchDomains(apiUrl, apiKey);
|
|
106
|
+
const domain = faker.helpers.arrayElement(domains);
|
|
107
|
+
|
|
108
|
+
// 2. Generate name + username
|
|
109
|
+
const firstName = generateSafeNamePart(() => faker.person.firstName());
|
|
110
|
+
const lastName = generateSafeNamePart(() => faker.person.lastName());
|
|
111
|
+
const fullName = `${firstName} ${lastName}`;
|
|
112
|
+
const username = `${toEmailPart(firstName)}${toEmailPart(lastName)}${suffix}`;
|
|
113
|
+
const email = `${username}@${domain}`;
|
|
114
|
+
|
|
115
|
+
// 3. Create email account on the service
|
|
116
|
+
await createEmailAccount(apiUrl, apiKey, email, password, fullName);
|
|
117
|
+
|
|
118
|
+
return { email, password, firstName, lastName, fullName };
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/http-client.js
|
|
3
|
+
* Cookie-aware HTTP client for cross-domain request chains.
|
|
4
|
+
* Handles cookie persistence across capcut.com ↔ login-row.www.capcut.com.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const UA =
|
|
8
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:151.0) Gecko/20100101 Firefox/151.0";
|
|
9
|
+
|
|
10
|
+
export const USER_AGENT = UA;
|
|
11
|
+
|
|
12
|
+
// ─── CookieJar ────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export class CookieJar {
|
|
15
|
+
constructor() {
|
|
16
|
+
/** @type {Record<string, Record<string, {name:string,value:string,expires?:string}>>} */
|
|
17
|
+
this.store = {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Parse Set-Cookie headers from response and store them. */
|
|
21
|
+
parseResponse(requestUrl, response) {
|
|
22
|
+
const url = new URL(requestUrl);
|
|
23
|
+
const headers =
|
|
24
|
+
typeof response.headers.getSetCookie === "function"
|
|
25
|
+
? response.headers.getSetCookie()
|
|
26
|
+
: [];
|
|
27
|
+
|
|
28
|
+
for (const h of headers) this._parseAndStore(h, url);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Build Cookie header string for a given request URL. */
|
|
32
|
+
getCookieString(requestUrl) {
|
|
33
|
+
const hostname = new URL(requestUrl).hostname;
|
|
34
|
+
const pairs = [];
|
|
35
|
+
|
|
36
|
+
for (const [dom, cookies] of Object.entries(this.store)) {
|
|
37
|
+
if (!this._domainMatch(dom, hostname)) continue;
|
|
38
|
+
for (const c of Object.values(cookies)) {
|
|
39
|
+
if (c.expires && new Date(c.expires) < new Date()) continue;
|
|
40
|
+
pairs.push(`${c.name}=${c.value}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return pairs.join("; ");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Read a single cookie value by name (across all domains). */
|
|
48
|
+
getCookie(name) {
|
|
49
|
+
for (const cookies of Object.values(this.store)) {
|
|
50
|
+
if (cookies[name]) return cookies[name].value;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Manually set a cookie for a domain. */
|
|
56
|
+
setCookie(domain, name, value) {
|
|
57
|
+
const dom = domain.startsWith(".") ? domain : "." + domain;
|
|
58
|
+
if (!this.store[dom]) this.store[dom] = {};
|
|
59
|
+
this.store[dom][name] = { name, value };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_domainMatch(cookieDomain, hostname) {
|
|
63
|
+
if (cookieDomain === hostname) return true;
|
|
64
|
+
if (cookieDomain.startsWith(".")) {
|
|
65
|
+
return (
|
|
66
|
+
hostname === cookieDomain.slice(1) || hostname.endsWith(cookieDomain)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_parseAndStore(header, url) {
|
|
73
|
+
const parts = header.split(";").map((s) => s.trim());
|
|
74
|
+
const eqIdx = parts[0].indexOf("=");
|
|
75
|
+
if (eqIdx < 0) return;
|
|
76
|
+
|
|
77
|
+
const name = parts[0].substring(0, eqIdx).trim();
|
|
78
|
+
const value = parts[0].substring(eqIdx + 1).trim();
|
|
79
|
+
const cookie = { name, value };
|
|
80
|
+
let domain = url.hostname;
|
|
81
|
+
|
|
82
|
+
for (let i = 1; i < parts.length; i++) {
|
|
83
|
+
const [k, ...v] = parts[i].split("=");
|
|
84
|
+
const key = k.trim().toLowerCase();
|
|
85
|
+
const val = v.join("=").trim();
|
|
86
|
+
|
|
87
|
+
if (key === "domain") {
|
|
88
|
+
domain = val.startsWith(".") ? val : "." + val;
|
|
89
|
+
} else if (key === "expires") {
|
|
90
|
+
cookie.expires = val;
|
|
91
|
+
} else if (key === "max-age") {
|
|
92
|
+
const ma = parseInt(val);
|
|
93
|
+
cookie.expires =
|
|
94
|
+
ma <= 0
|
|
95
|
+
? new Date(0).toUTCString()
|
|
96
|
+
: new Date(Date.now() + ma * 1000).toUTCString();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Delete expired / null cookies
|
|
101
|
+
if (
|
|
102
|
+
value === "null" ||
|
|
103
|
+
value === "" ||
|
|
104
|
+
(cookie.expires && new Date(cookie.expires) < new Date())
|
|
105
|
+
) {
|
|
106
|
+
if (this.store[domain]) delete this.store[domain][name];
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!this.store[domain]) this.store[domain] = {};
|
|
111
|
+
this.store[domain][name] = cookie;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Fetch Helpers ────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
/** Single fetch with cookie jar (no auto-redirect). */
|
|
118
|
+
export async function fetchCookie(jar, url, opts = {}) {
|
|
119
|
+
const cookies = jar.getCookieString(url);
|
|
120
|
+
const headers = {
|
|
121
|
+
"User-Agent": UA,
|
|
122
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
123
|
+
...opts.headers,
|
|
124
|
+
};
|
|
125
|
+
if (cookies) headers["Cookie"] = cookies;
|
|
126
|
+
|
|
127
|
+
const res = await fetch(url, { ...opts, headers, redirect: "manual" });
|
|
128
|
+
jar.parseResponse(url, res);
|
|
129
|
+
return res;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Fetch following redirects, capturing cookies at each hop. */
|
|
133
|
+
export async function fetchRedirect(jar, url, opts = {}, max = 15) {
|
|
134
|
+
let res = await fetchCookie(jar, url, opts);
|
|
135
|
+
let cur = url;
|
|
136
|
+
let n = 0;
|
|
137
|
+
|
|
138
|
+
while ([301, 302, 303, 307, 308].includes(res.status) && n++ < max) {
|
|
139
|
+
const loc = res.headers.get("location");
|
|
140
|
+
if (!loc) break;
|
|
141
|
+
await res.text().catch(() => {});
|
|
142
|
+
cur = new URL(loc, cur).href;
|
|
143
|
+
const method =
|
|
144
|
+
res.status === 307 || res.status === 308 ? opts.method || "GET" : "GET";
|
|
145
|
+
res = await fetchCookie(jar, cur, { headers: opts.headers, method });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { response: res, url: cur };
|
|
149
|
+
}
|