tempmail-sdk 1.1.3 → 1.1.4

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.
@@ -7,56 +7,90 @@ const BASE_URL = 'https://mail.chatgpt.org.uk/api';
7
7
  const HOME_URL = 'https://mail.chatgpt.org.uk/';
8
8
 
9
9
  const DEFAULT_HEADERS = {
10
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
11
- 'Accept': '*/*',
12
- 'Referer': 'https://mail.chatgpt.org.uk/',
13
- 'Origin': 'https://mail.chatgpt.org.uk',
14
- 'DNT': '1',
10
+ 'User-Agent':
11
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
12
+ Accept: '*/*',
13
+ Referer: 'https://mail.chatgpt.org.uk/',
14
+ Origin: 'https://mail.chatgpt.org.uk',
15
+ DNT: '1',
15
16
  };
16
17
 
18
+ /** 从首页 HTML 解析 window.__BROWSER_AUTH(服务端注入的会话 JWT,供 X-Inbox-Token) */
19
+ function extractBrowserAuthToken(html: string): string {
20
+ const m = html.match(/__BROWSER_AUTH\s*=\s*(\{[\s\S]*?\})\s*;/);
21
+ if (!m) {
22
+ return '';
23
+ }
24
+ try {
25
+ const o = JSON.parse(m[1]) as { token?: string };
26
+ return typeof o.token === 'string' ? o.token : '';
27
+ } catch {
28
+ return '';
29
+ }
30
+ }
31
+
17
32
  function extractGmSid(response: Response): string {
33
+ const h = response.headers as Headers & { getSetCookie?: () => string[] };
34
+ if (typeof h.getSetCookie === 'function') {
35
+ for (const line of h.getSetCookie()) {
36
+ const match = line.match(/^gm_sid=([^;]+)/);
37
+ if (match) {
38
+ return match[1];
39
+ }
40
+ }
41
+ }
18
42
  const setCookie = response.headers.get('set-cookie') || '';
19
43
  const match = setCookie.match(/gm_sid=([^;]+)/);
20
44
  return match ? match[1] : '';
21
45
  }
22
46
 
23
- async function fetchGmSid(): Promise<string> {
47
+ async function fetchHomeSession(): Promise<{ gmSid: string; browserToken: string }> {
24
48
  const response = await fetchWithTimeout(HOME_URL, {
25
49
  method: 'GET',
26
50
  headers: DEFAULT_HEADERS,
27
51
  });
28
52
 
29
53
  if (!response.ok) {
30
- throw new Error(`Failed to fetch gm_sid: ${response.status}`);
54
+ throw new Error(`Failed to load mail.chatgpt.org.uk: ${response.status}`);
31
55
  }
32
56
 
57
+ const html = await response.text();
33
58
  const gmSid = extractGmSid(response);
59
+ const browserToken = extractBrowserAuthToken(html);
60
+
34
61
  if (!gmSid) {
35
62
  throw new Error('Failed to extract gm_sid cookie');
36
63
  }
64
+ if (!browserToken) {
65
+ throw new Error('Failed to extract __BROWSER_AUTH from homepage (API now requires browser session)');
66
+ }
37
67
 
38
- return gmSid;
68
+ return { gmSid, browserToken };
39
69
  }
40
70
 
41
- async function fetchGmSidWithRetry(): Promise<string> {
71
+ async function fetchHomeSessionWithRetry(): Promise<{ gmSid: string; browserToken: string }> {
42
72
  try {
43
- return await fetchGmSid();
73
+ return await fetchHomeSession();
44
74
  } catch (error: any) {
45
75
  const message = String(error?.message || error || '').toLowerCase();
46
- if (message.includes('401') || message.includes('extract gm_sid')) {
47
- return await fetchGmSid();
76
+ if (message.includes('401') || message.includes('429') || message.includes('extract')) {
77
+ return await fetchHomeSession();
48
78
  }
49
79
  throw error;
50
80
  }
51
81
  }
52
82
 
83
+ function sleepMs(ms: number): Promise<void> {
84
+ return new Promise((resolve) => setTimeout(resolve, ms));
85
+ }
86
+
53
87
  async function fetchInboxToken(email: string, gmSid: string): Promise<string> {
54
88
  const response = await fetchWithTimeout(`${BASE_URL}/inbox-token`, {
55
89
  method: 'POST',
56
90
  headers: {
57
91
  ...DEFAULT_HEADERS,
58
92
  'Content-Type': 'application/json',
59
- 'Cookie': `gm_sid=${gmSid}`,
93
+ Cookie: `gm_sid=${gmSid}`,
60
94
  },
61
95
  body: JSON.stringify({ email }),
62
96
  });
@@ -74,30 +108,53 @@ async function fetchInboxToken(email: string, gmSid: string): Promise<string> {
74
108
  return token;
75
109
  }
76
110
 
77
- async function fetchInboxTokenWithRetry(email: string): Promise<string> {
78
- const gmSid = await fetchGmSidWithRetry();
111
+ async function fetchInboxTokenWithRetry(email: string, gmSid: string): Promise<string> {
79
112
  try {
80
113
  return await fetchInboxToken(email, gmSid);
81
114
  } catch (error: any) {
82
115
  const message = String(error?.message || error || '').toLowerCase();
83
116
  if (message.includes('401')) {
84
- const refreshedGmSid = await fetchGmSidWithRetry();
85
- return await fetchInboxToken(email, refreshedGmSid);
117
+ const { gmSid: sid } = await fetchHomeSessionWithRetry();
118
+ return await fetchInboxToken(email, sid);
86
119
  }
87
120
  throw error;
88
121
  }
89
122
  }
90
123
 
91
- async function fetchEmails(token: string, email: string): Promise<Email[]> {
92
- if (!token) {
124
+ /** 列表接口需同时带 Cookie gm_sid x-inbox-token,否则返回 401 */
125
+ function parseChatgptPackedToken(packed: string): { gmSid: string; inbox: string } {
126
+ const t = packed.trim();
127
+ if (t.startsWith('{')) {
128
+ try {
129
+ const o = JSON.parse(t) as { gmSid?: string; inbox?: string };
130
+ if (typeof o.gmSid === 'string' && typeof o.inbox === 'string') {
131
+ return { gmSid: o.gmSid, inbox: o.inbox };
132
+ }
133
+ } catch {
134
+ /* ignore */
135
+ }
136
+ }
137
+ return { gmSid: '', inbox: packed };
138
+ }
139
+
140
+ async function fetchEmails(
141
+ inboxToken: string,
142
+ email: string,
143
+ gmSid: string,
144
+ ): Promise<Email[]> {
145
+ if (!inboxToken) {
93
146
  throw new Error('internal error: token missing for chatgpt-org-uk');
94
147
  }
148
+ if (!gmSid) {
149
+ throw new Error('internal error: gm_sid missing for chatgpt-org-uk');
150
+ }
95
151
  const encodedEmail = encodeURIComponent(email);
96
152
  const response = await fetchWithTimeout(`${BASE_URL}/emails?email=${encodedEmail}`, {
97
153
  method: 'GET',
98
154
  headers: {
99
155
  ...DEFAULT_HEADERS,
100
- 'x-inbox-token': token,
156
+ Cookie: `gm_sid=${gmSid}`,
157
+ 'x-inbox-token': inboxToken,
101
158
  },
102
159
  });
103
160
 
@@ -115,44 +172,58 @@ async function fetchEmails(token: string, email: string): Promise<Email[]> {
115
172
  return rawEmails.map((raw: any) => normalizeEmail(raw, email));
116
173
  }
117
174
 
118
- async function fetchEmailsWithRetry(email: string): Promise<Email[]> {
119
- const token = await fetchInboxTokenWithRetry(email);
120
- try {
121
- return await fetchEmails(token, email);
122
- } catch (error: any) {
123
- const message = String(error?.message || error || '').toLowerCase();
124
- if (message.includes('401')) {
125
- const refreshedToken = await fetchInboxTokenWithRetry(email);
126
- return await fetchEmails(refreshedToken, email);
175
+ export async function generateEmail(): Promise<InternalEmailInfo> {
176
+ /*
177
+ * generate-email 常返回 429;每次重试前重新拉首页以换新 gm_sid / __BROWSER_AUTH,并做指数退避。
178
+ */
179
+ const maxAttempts = 6;
180
+ let lastStatus = 0;
181
+
182
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
183
+ const { gmSid, browserToken } = await fetchHomeSessionWithRetry();
184
+
185
+ const response = await fetchWithTimeout(`${BASE_URL}/generate-email`, {
186
+ method: 'GET',
187
+ headers: {
188
+ ...DEFAULT_HEADERS,
189
+ Cookie: `gm_sid=${gmSid}`,
190
+ 'X-Inbox-Token': browserToken,
191
+ },
192
+ });
193
+
194
+ if (response.status === 429) {
195
+ lastStatus = 429;
196
+ if (attempt < maxAttempts - 1) {
197
+ const wait = Math.min(3000 * 2 ** attempt, 60_000);
198
+ await sleepMs(wait);
199
+ continue;
200
+ }
201
+ throw new Error('Failed to generate email: 429');
127
202
  }
128
- throw error;
129
- }
130
- }
131
203
 
132
- export async function generateEmail(): Promise<InternalEmailInfo> {
133
- const response = await fetchWithTimeout(`${BASE_URL}/generate-email`, {
134
- method: 'GET',
135
- headers: DEFAULT_HEADERS,
136
- });
204
+ if (!response.ok) {
205
+ lastStatus = response.status;
206
+ throw new Error(`Failed to generate email: ${response.status}`);
207
+ }
137
208
 
138
- if (!response.ok) {
139
- throw new Error(`Failed to generate email: ${response.status}`);
140
- }
209
+ const data = await response.json();
141
210
 
142
- const data = await response.json();
211
+ if (!data.success) {
212
+ throw new Error('Failed to generate email');
213
+ }
143
214
 
144
- if (!data.success) {
145
- throw new Error('Failed to generate email');
146
- }
215
+ const email = data.data.email as string;
216
+ const tokenFromAuth = data.auth?.token as string | undefined;
217
+ const inboxJwt = tokenFromAuth || (await fetchInboxTokenWithRetry(email, gmSid));
147
218
 
148
- const email = data.data.email;
149
- const token = await fetchInboxTokenWithRetry(email);
219
+ return {
220
+ channel: CHANNEL,
221
+ email,
222
+ token: JSON.stringify({ gmSid, inbox: inboxJwt }),
223
+ };
224
+ }
150
225
 
151
- return {
152
- channel: CHANNEL,
153
- email,
154
- token,
155
- };
226
+ throw new Error(`Failed to generate email: ${lastStatus || 'unknown'}`);
156
227
  }
157
228
 
158
229
  export async function getEmails(token: string, email: string): Promise<Email[]> {
@@ -160,12 +231,19 @@ export async function getEmails(token: string, email: string): Promise<Email[]>
160
231
  throw new Error('internal error: token missing for chatgpt-org-uk');
161
232
  }
162
233
 
234
+ let { gmSid, inbox } = parseChatgptPackedToken(token);
235
+ if (!gmSid) {
236
+ gmSid = (await fetchHomeSessionWithRetry()).gmSid;
237
+ }
238
+
163
239
  try {
164
- return await fetchEmails(token, email);
240
+ return await fetchEmails(inbox, email, gmSid);
165
241
  } catch (error: any) {
166
242
  const message = String(error?.message || error || '').toLowerCase();
167
243
  if (message.includes('401')) {
168
- return await fetchEmailsWithRetry(email);
244
+ const sess = await fetchHomeSessionWithRetry();
245
+ const newInbox = await fetchInboxTokenWithRetry(email, sess.gmSid);
246
+ return await fetchEmails(newInbox, email, sess.gmSid);
169
247
  }
170
248
  throw error;
171
249
  }
@@ -1,9 +1,168 @@
1
1
  import { InternalEmailInfo, Email, Channel } from '../types';
2
2
  import { normalizeEmail } from '../normalize';
3
3
  import { fetchWithTimeout } from '../retry';
4
+ import { getConfig } from '../config';
4
5
 
5
6
  const CHANNEL: Channel = 'dropmail';
6
- const BASE_URL = 'https://dropmail.me/api/graphql/MY_TOKEN';
7
+
8
+ const TOKEN_GENERATE_URL = 'https://dropmail.me/api/token/generate';
9
+ const TOKEN_RENEW_URL = 'https://dropmail.me/api/token/renew';
10
+ /** 申请 1h 令牌,缓存略短于 1h,避免边界过期 */
11
+ const AUTO_TOKEN_CACHE_MS = 50 * 60 * 1000;
12
+ /** 距缓存过期前多久触发 renew(毫秒) */
13
+ const RENEW_BEFORE_EXPIRY_MS = 10 * 60 * 1000;
14
+
15
+ function cacheMsForLifetime(lifetime: string): number {
16
+ const s = lifetime.trim().toLowerCase();
17
+ if (s === '1h') return 50 * 60 * 1000;
18
+ if (s === '1d') return 23 * 60 * 60 * 1000;
19
+ if (s === '7d' || s === '30d' || s === '90d') {
20
+ const days = parseInt(s, 10);
21
+ return Math.max(0, days - 1) * 24 * 60 * 60 * 1000;
22
+ }
23
+ return AUTO_TOKEN_CACHE_MS;
24
+ }
25
+
26
+ function dropmailRenewLifetime(): string {
27
+ const c = getConfig().dropmailRenewLifetime?.trim();
28
+ if (c) return c;
29
+ const e =
30
+ typeof process !== 'undefined' && process.env?.DROPMAIL_RENEW_LIFETIME?.trim();
31
+ return e || '1d';
32
+ }
33
+
34
+ const TOKEN_HEADERS: Record<string, string> = {
35
+ Accept: 'application/json',
36
+ 'Content-Type': 'application/json',
37
+ Origin: 'https://dropmail.me',
38
+ Referer: 'https://dropmail.me/api/',
39
+ };
40
+
41
+ let cachedAfToken: { value: string; expiresAt: number } | null = null;
42
+ let tokenFetchInFlight: Promise<string> | null = null;
43
+
44
+ function explicitDropmailAuthToken(): string | undefined {
45
+ const fromConfig = getConfig().dropmailAuthToken?.trim();
46
+ const fromEnv =
47
+ typeof process !== 'undefined' && process.env
48
+ ? process.env.DROPMAIL_AUTH_TOKEN?.trim() || process.env.DROPMAIL_API_TOKEN?.trim()
49
+ : undefined;
50
+ return fromConfig || fromEnv;
51
+ }
52
+
53
+ function dropmailAutoTokenDisabled(): boolean {
54
+ if (getConfig().dropmailDisableAutoToken) {
55
+ return true;
56
+ }
57
+ const v = typeof process !== 'undefined' && process.env?.DROPMAIL_NO_AUTO_TOKEN?.trim().toLowerCase();
58
+ return v === '1' || v === 'true' || v === 'yes';
59
+ }
60
+
61
+ async function fetchAfTokenFromApi(): Promise<string> {
62
+ const response = await fetchWithTimeout(TOKEN_GENERATE_URL, {
63
+ method: 'POST',
64
+ headers: TOKEN_HEADERS,
65
+ body: JSON.stringify({ type: 'af', lifetime: '1h' }),
66
+ });
67
+
68
+ if (!response.ok) {
69
+ throw new Error(`DropMail token/generate HTTP ${response.status}`);
70
+ }
71
+
72
+ const body = (await response.json()) as { token?: string; error?: string };
73
+ const token = typeof body.token === 'string' ? body.token.trim() : '';
74
+ if (!token || !token.startsWith('af_')) {
75
+ throw new Error(
76
+ body.error || 'DropMail token/generate 未返回有效 af_ 令牌',
77
+ );
78
+ }
79
+ return token;
80
+ }
81
+
82
+ async function renewAfTokenFromApi(currentToken: string, lifetime: string): Promise<string> {
83
+ const response = await fetchWithTimeout(TOKEN_RENEW_URL, {
84
+ method: 'POST',
85
+ headers: TOKEN_HEADERS,
86
+ body: JSON.stringify({ token: currentToken, lifetime }),
87
+ });
88
+
89
+ if (!response.ok) {
90
+ throw new Error(`DropMail token/renew HTTP ${response.status}`);
91
+ }
92
+
93
+ const body = (await response.json()) as { token?: string; error?: string };
94
+ const token = typeof body.token === 'string' ? body.token.trim() : '';
95
+ if (!token || !token.startsWith('af_')) {
96
+ throw new Error(body.error || 'DropMail token/renew 未返回有效 af_ 令牌');
97
+ }
98
+ return token;
99
+ }
100
+
101
+ /**
102
+ * 解析 GraphQL 用的 af_ 令牌:优先配置/环境变量,否则 generate + 缓存,将过期时 renew。
103
+ */
104
+ async function resolveDropmailAuthToken(): Promise<string> {
105
+ const explicit = explicitDropmailAuthToken();
106
+ if (explicit) {
107
+ return explicit;
108
+ }
109
+
110
+ if (dropmailAutoTokenDisabled()) {
111
+ throw new Error(
112
+ 'DropMail 已禁用自动令牌:请设置 DROPMAIL_AUTH_TOKEN,或 setConfig({ dropmailAuthToken: "af_..." }),见 https://dropmail.me/api/',
113
+ );
114
+ }
115
+
116
+ const now = Date.now();
117
+ if (
118
+ cachedAfToken &&
119
+ now < cachedAfToken.expiresAt - RENEW_BEFORE_EXPIRY_MS
120
+ ) {
121
+ return cachedAfToken.value;
122
+ }
123
+
124
+ if (tokenFetchInFlight) {
125
+ return tokenFetchInFlight;
126
+ }
127
+
128
+ const renewLifetime = dropmailRenewLifetime();
129
+
130
+ tokenFetchInFlight = (async () => {
131
+ try {
132
+ if (cachedAfToken?.value) {
133
+ try {
134
+ const renewed = await renewAfTokenFromApi(
135
+ cachedAfToken.value,
136
+ renewLifetime,
137
+ );
138
+ cachedAfToken = {
139
+ value: renewed,
140
+ expiresAt: Date.now() + cacheMsForLifetime(renewLifetime),
141
+ };
142
+ return renewed;
143
+ } catch {
144
+ /* 续期失败则重新申请 */
145
+ }
146
+ }
147
+
148
+ const token = await fetchAfTokenFromApi();
149
+ cachedAfToken = {
150
+ value: token,
151
+ expiresAt: Date.now() + AUTO_TOKEN_CACHE_MS,
152
+ };
153
+ return token;
154
+ } finally {
155
+ tokenFetchInFlight = null;
156
+ }
157
+ })();
158
+
159
+ return tokenFetchInFlight;
160
+ }
161
+
162
+ async function dropmailGraphqlUrl(): Promise<string> {
163
+ const token = await resolveDropmailAuthToken();
164
+ return `https://dropmail.me/api/graphql/${encodeURIComponent(token)}`;
165
+ }
7
166
 
8
167
  const DEFAULT_HEADERS: Record<string, string> = {
9
168
  'Content-Type': 'application/x-www-form-urlencoded',
@@ -30,7 +189,7 @@ async function graphqlRequest(query: string, variables?: Record<string, any>): P
30
189
  params.set('variables', JSON.stringify(variables));
31
190
  }
32
191
 
33
- const response = await fetchWithTimeout(BASE_URL, {
192
+ const response = await fetchWithTimeout(await dropmailGraphqlUrl(), {
34
193
  method: 'POST',
35
194
  headers: DEFAULT_HEADERS,
36
195
  body: params.toString(),
@@ -1,10 +1,10 @@
1
1
  import { InternalEmailInfo, Email, Channel } from '../types';
2
2
  import { normalizeEmail } from '../normalize';
3
3
  import { fetchWithTimeout } from '../retry';
4
+ import { randomSyntheticLinshiKey } from './linshi-token';
4
5
 
5
6
  const CHANNEL: Channel = 'linshi-email';
6
7
  const BASE_URL = 'https://www.linshi-email.com/api/v1';
7
- const API_KEY = '552562b8524879814776e52bc8de5c9f';
8
8
 
9
9
  const DEFAULT_HEADERS = {
10
10
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
@@ -18,7 +18,8 @@ const DEFAULT_HEADERS = {
18
18
  };
19
19
 
20
20
  export async function generateEmail(): Promise<InternalEmailInfo> {
21
- const response = await fetchWithTimeout(`${BASE_URL}/email/${API_KEY}`, {
21
+ const { apiPathKey } = randomSyntheticLinshiKey();
22
+ const response = await fetchWithTimeout(`${BASE_URL}/email/${apiPathKey}`, {
22
23
  method: 'POST',
23
24
  headers: DEFAULT_HEADERS,
24
25
  body: JSON.stringify({}),
@@ -29,22 +30,37 @@ export async function generateEmail(): Promise<InternalEmailInfo> {
29
30
  }
30
31
 
31
32
  const data = await response.json();
32
-
33
+
33
34
  if (data.status !== 'ok') {
34
35
  throw new Error('Failed to generate email');
35
36
  }
36
37
 
38
+ const d = data.data;
39
+ const raw =
40
+ (typeof d?.email === 'string' && d.email) ||
41
+ (typeof d?.mail === 'string' && d.mail) ||
42
+ (typeof d?.address === 'string' && d.address) ||
43
+ '';
44
+ const email = String(raw).trim();
45
+
46
+ if (!email || !email.includes('@')) {
47
+ throw new Error(
48
+ 'linshi-email: API 未返回有效邮箱地址(data 为空或缺 email 字段,常见于频率限制:每小时约 10 个 / 每天约 20 个)',
49
+ );
50
+ }
51
+
37
52
  return {
38
53
  channel: CHANNEL,
39
- email: data.data.email,
40
- expiresAt: data.data.expired,
54
+ email,
55
+ expiresAt: d?.expired,
56
+ token: apiPathKey,
41
57
  };
42
58
  }
43
59
 
44
- export async function getEmails(email: string): Promise<Email[]> {
60
+ export async function getEmails(email: string, apiPathKey: string): Promise<Email[]> {
45
61
  const encodedEmail = encodeURIComponent(email);
46
62
  const timestamp = Date.now();
47
- const response = await fetchWithTimeout(`${BASE_URL}/refreshmessage/${API_KEY}/${encodedEmail}?t=${timestamp}`, {
63
+ const response = await fetchWithTimeout(`${BASE_URL}/refreshmessage/${apiPathKey}/${encodedEmail}?t=${timestamp}`, {
48
64
  method: 'GET',
49
65
  headers: DEFAULT_HEADERS,
50
66
  });
@@ -0,0 +1,86 @@
1
+ import { createHash, randomBytes, randomInt } from 'crypto';
2
+ export function deriveLinshiApiPathKey(visitorId: string): string {
3
+ if (visitorId.length < 4) {
4
+ throw new Error('visitorId 过短,无法套用 u()');
5
+ }
6
+ let e = visitorId.substring(0, visitorId.length - 2);
7
+ let t = 0;
8
+ for (let i = 0; i < e.length; i++) {
9
+ t += e.charCodeAt(i) % 5;
10
+ }
11
+ if (t < 10) {
12
+ t = 10;
13
+ }
14
+ if (t >= 100) {
15
+ t = 99;
16
+ }
17
+ const ts = t.toString();
18
+ e = e.slice(0, 3) + ts[0] + e.slice(3);
19
+ e = e.slice(0, 12) + ts[1] + e.slice(12);
20
+ return e;
21
+ }
22
+ export interface SyntheticBrowserProfile {
23
+ userAgent: string;
24
+ platform: string;
25
+ language: string;
26
+ languages: string;
27
+ hardwareConcurrency: number;
28
+ deviceMemory: number;
29
+ timezone: string;
30
+ colorDepth: number;
31
+ pixelRatio: number;
32
+ screen: { width: number; height: number; availWidth: number; availHeight: number };
33
+ }
34
+
35
+ const UA_POOL = [
36
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
37
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0',
38
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
39
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
40
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
41
+ ];
42
+
43
+ function pick<T>(arr: T[]): T {
44
+ return arr[randomInt(arr.length)];
45
+ }
46
+
47
+ export function randomBrowserLikeProfile(): SyntheticBrowserProfile {
48
+ const w = pick([1920, 2560, 1680, 1536, 1366]) + randomInt(0, 80);
49
+ const h = pick([1080, 1440, 1050, 864, 768]) + randomInt(0, 40);
50
+ return {
51
+ userAgent: pick(UA_POOL),
52
+ platform: pick(['Win32', 'MacIntel', 'Linux x86_64']),
53
+ language: pick(['zh-CN', 'en-US', 'en-GB', 'ja-JP']),
54
+ languages: pick(['zh-CN,zh,en', 'en-US,en', 'ja-JP,ja,en']),
55
+ hardwareConcurrency: randomInt(4, 33),
56
+ deviceMemory: pick([4, 8, 16]),
57
+ timezone: pick(['Asia/Shanghai', 'America/New_York', 'Europe/London', 'UTC']),
58
+ colorDepth: pick([24, 30]),
59
+ pixelRatio: pick([1, 1.25, 1.5, 2]),
60
+ screen: {
61
+ width: w,
62
+ height: h,
63
+ availWidth: w,
64
+ availHeight: h - randomInt(24, 120),
65
+ },
66
+ };
67
+ }
68
+
69
+ export function syntheticVisitorIdFromProfile(
70
+ profile: SyntheticBrowserProfile,
71
+ salt: Buffer = randomBytes(16),
72
+ ): string {
73
+ const payload = JSON.stringify(profile, Object.keys(profile).sort());
74
+ return createHash('sha256').update(payload).update(salt).digest('hex').slice(0, 32);
75
+ }
76
+
77
+ export function randomSyntheticLinshiKey(): {
78
+ profile: SyntheticBrowserProfile;
79
+ visitorId: string;
80
+ apiPathKey: string;
81
+ } {
82
+ const profile = randomBrowserLikeProfile();
83
+ const visitorId = syntheticVisitorIdFromProfile(profile);
84
+ const apiPathKey = deriveLinshiApiPathKey(visitorId);
85
+ return { profile, visitorId, apiPathKey };
86
+ }