@lightharu/krouter 1.8.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.
Files changed (61) hide show
  1. package/LICENSE +679 -0
  2. package/README.md +238 -0
  3. package/dist-web/assets/index-CM4-0adf.css +1 -0
  4. package/dist-web/assets/index-DCslvfUR.js +139 -0
  5. package/dist-web/favicon.svg +9 -0
  6. package/dist-web/icon.svg +9 -0
  7. package/dist-web/index.html +19 -0
  8. package/out-server/main/kiroAuthSync.js +249 -0
  9. package/out-server/main/kproxy/certManager.js +262 -0
  10. package/out-server/main/kproxy/index.js +254 -0
  11. package/out-server/main/kproxy/mitmProxy.js +475 -0
  12. package/out-server/main/kproxy/types.js +23 -0
  13. package/out-server/main/proxy/accountPool.js +543 -0
  14. package/out-server/main/proxy/clientConfig.js +596 -0
  15. package/out-server/main/proxy/index.js +25 -0
  16. package/out-server/main/proxy/kiroApi.js +1996 -0
  17. package/out-server/main/proxy/logger.js +407 -0
  18. package/out-server/main/proxy/modelCatalog.js +75 -0
  19. package/out-server/main/proxy/promptCacheTracker.js +301 -0
  20. package/out-server/main/proxy/proxyServer.js +3543 -0
  21. package/out-server/main/proxy/selfSignedCert.js +179 -0
  22. package/out-server/main/proxy/systemProxy.js +250 -0
  23. package/out-server/main/proxy/tokenCounter.js +164 -0
  24. package/out-server/main/proxy/toolNameRegistry.js +57 -0
  25. package/out-server/main/proxy/translator.js +1084 -0
  26. package/out-server/main/proxy/types.js +3 -0
  27. package/out-server/main/registration/browser-identity.js +184 -0
  28. package/out-server/main/registration/chainProxy.js +349 -0
  29. package/out-server/main/registration/config.js +58 -0
  30. package/out-server/main/registration/email-service.js +801 -0
  31. package/out-server/main/registration/fingerprint.js +352 -0
  32. package/out-server/main/registration/http-utils.js +148 -0
  33. package/out-server/main/registration/jwe.js +74 -0
  34. package/out-server/main/registration/names.js +142 -0
  35. package/out-server/main/registration/proton-mail-window.js +339 -0
  36. package/out-server/main/registration/registrar.js +1715 -0
  37. package/out-server/main/registration/tlsClientPool.js +70 -0
  38. package/out-server/main/registration/xxtea.js +161 -0
  39. package/out-server/main/runtimePaths.js +19 -0
  40. package/out-server/main/utils/redact.js +95 -0
  41. package/out-server/server/index.js +1272 -0
  42. package/out-server/server/services/accountExtras.js +105 -0
  43. package/out-server/server/services/accountProfileHydration.js +95 -0
  44. package/out-server/server/services/authFlows.js +509 -0
  45. package/out-server/server/services/dashboardTunnel.js +315 -0
  46. package/out-server/server/services/diagnostics.js +326 -0
  47. package/out-server/server/services/kiroAccounts.js +431 -0
  48. package/out-server/server/services/kiroSettings.js +260 -0
  49. package/out-server/server/services/kproxyRuntime.js +264 -0
  50. package/out-server/server/services/localKiroCredentials.js +320 -0
  51. package/out-server/server/services/machineIdRuntime.js +327 -0
  52. package/out-server/server/services/protonBrowserRuntime.js +724 -0
  53. package/out-server/server/services/proxyRuntime.js +523 -0
  54. package/out-server/server/services/registrationRuntime.js +106 -0
  55. package/out-server/server/store.js +266 -0
  56. package/package.json +113 -0
  57. package/resources/tls-client-xgo-1.14.0-windows-amd64.dll +0 -0
  58. package/scripts/kiro-manager-cli.cjs +3 -0
  59. package/scripts/krouter-cli.cjs +509 -0
  60. package/src/renderer/src/assets/krouter-logo.svg +11 -0
  61. package/src/renderer/src/assets/krouter-mark.svg +9 -0
@@ -0,0 +1,1715 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Registrar = void 0;
4
+ const tlsclientwrapper_1 = require("tlsclientwrapper");
5
+ const tlsClientPool_1 = require("./tlsClientPool");
6
+ const undici_1 = require("undici");
7
+ const browser_identity_1 = require("./browser-identity");
8
+ const chainProxy_1 = require("./chainProxy");
9
+ const fingerprint_1 = require("./fingerprint");
10
+ const jwe_1 = require("./jwe");
11
+ const xxtea_1 = require("./xxtea");
12
+ const http_utils_1 = require("./http-utils");
13
+ const email_service_1 = require("./email-service");
14
+ const systemProxy_1 = require("../proxy/systemProxy");
15
+ const redact_1 = require("../utils/redact");
16
+ const runtimePaths_1 = require("../runtimePaths");
17
+ class Registrar {
18
+ cfg;
19
+ session = null;
20
+ /** 共享的 ModuleClient(来自 tlsClientPool);不在 cleanup 中 terminate,由进程退出时统一释放 */
21
+ moduleClient = null;
22
+ cookies = new Map();
23
+ identity;
24
+ fpCtx;
25
+ vid;
26
+ email = '';
27
+ emailSvc = null;
28
+ clientId = '';
29
+ clientSecret = '';
30
+ deviceCode = '';
31
+ userCode = '';
32
+ workflowHandle = '';
33
+ workflowId = '';
34
+ workflowState = '';
35
+ ubid = '';
36
+ regCode = '';
37
+ signState = '';
38
+ authCode = '';
39
+ ssoState = '';
40
+ wdcCSRFToken = '';
41
+ ssoToken = '';
42
+ outlookMailCount = 0;
43
+ log;
44
+ onStep;
45
+ abortController = new AbortController();
46
+ chainRelay = null;
47
+ chainTargetProxy = '';
48
+ exitIP = '';
49
+ tlsSessionId = (0, http_utils_1.newUUID)(); // 固定:整个 Registrar 生命周期内 DLL 中只注册一个 session
50
+ constructor(cfg, log, onStep) {
51
+ this.cfg = cfg;
52
+ this.identity = (0, browser_identity_1.randomIdentity)();
53
+ this.fpCtx = (0, fingerprint_1.newFPContext)(this.identity);
54
+ this.vid = (0, http_utils_1.visitorId)();
55
+ // 注册日志会推送到 UI / 控制台,统一脱敏代理账密、token 等敏感片段
56
+ const rawLog = log || ((msg) => console.log(msg));
57
+ this.log = (msg) => rawLog((0, redact_1.redactString)(msg));
58
+ this.onStep = onStep || (() => { });
59
+ }
60
+ /** 触发 step 事件:上层(前端 UI)可据此实时展示注册到了哪一步。失败时静默以不影响主流程。 */
61
+ emitStep(name, info) {
62
+ try {
63
+ this.onStep({ name, ts: Date.now(), email: this.email || undefined, exitIp: this.exitIP || undefined, ...info });
64
+ }
65
+ catch { /* ignore */ }
66
+ }
67
+ /** 基于当前 identity 的 sec-ch-ua 头(动态生成,跟 chromeVer 对齐) */
68
+ get secUA() {
69
+ const major = this.identity.chromeVer.split('.')[0];
70
+ return `"Chromium";v="${major}", "Not/A)Brand";v="24", "Google Chrome";v="${major}"`;
71
+ }
72
+ /** 中止当前注册流程 */
73
+ abort() {
74
+ this.abortController.abort();
75
+ }
76
+ /**
77
+ * 启用代理链:若同时配置了 upstreamProxy(上游中转) 与 proxy(目标代理),
78
+ * 在本机起一个中继把链路串成「本机 → 中继 → 上游中转(非大陆) → 目标代理 → 目标站点」,
79
+ * 并把 cfg.proxy 指向本地中继,使后续所有请求自动走链路。
80
+ */
81
+ async setupProxyChain() {
82
+ const target = (this.cfg.proxy || '').trim();
83
+ const upstream = (this.cfg.upstreamProxy || '').trim();
84
+ if (!target || !upstream)
85
+ return;
86
+ try {
87
+ this.chainRelay = new chainProxy_1.ChainProxyRelay(upstream, target, (m) => this.log(m));
88
+ const relayUrl = await this.chainRelay.start();
89
+ this.chainTargetProxy = target;
90
+ this.cfg.proxy = relayUrl;
91
+ this.log('[ProxyChain] Đã bật chuỗi proxy: máy cục bộ → proxy trung chuyển → proxy đích → trang đích');
92
+ }
93
+ catch (err) {
94
+ const msg = err instanceof Error ? err.message : String(err);
95
+ this.chainRelay = null;
96
+ // 严格代理模式下,链路失败必须立刻中止,防止"回退仅用目标代理"时大陆 IP 被目标拒绝
97
+ if (this.cfg.strictProxy) {
98
+ throw new Error(`[ProxyChain] Bật thất bại, chế độ proxy nghiêm ngặt đã dừng: ${msg}`);
99
+ }
100
+ this.log(`[ProxyChain] Bật thất bại, chuyển sang dùng trực tiếp proxy đích: ${msg}`);
101
+ }
102
+ }
103
+ checkAborted() {
104
+ if (this.abortController.signal.aborted)
105
+ throw new Error('Đăng ký đã bị hủy');
106
+ }
107
+ /**
108
+ * 探测当前代理的出口 IP 并写入日志。
109
+ * 如果探测失败且代理 URL 是参数化格式(bestproxy 等),自动换 session 重建代理链重试。
110
+ * 最多重试 maxRetries 次(默认 2),保证拿到可用出口再继续注册。
111
+ */
112
+ async detectExitIP(maxRetries = 2) {
113
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
114
+ const proxyUrl = this.sessionOpts.proxyUrl;
115
+ try {
116
+ const agent = (0, systemProxy_1.safeCreateProxyAgent)(proxyUrl);
117
+ const resp = await (0, undici_1.fetch)('https://api.ipify.org?format=json', {
118
+ method: 'GET',
119
+ dispatcher: agent || undefined,
120
+ signal: AbortSignal.timeout(10000),
121
+ headers: { 'User-Agent': this.identity.ua }
122
+ });
123
+ if (resp.ok) {
124
+ const body = await resp.json();
125
+ const ip = String(body.ip || body.query || body.origin || '').trim();
126
+ if (ip) {
127
+ this.exitIP = ip;
128
+ this.emitStep('exit-ip', { exitIp: ip });
129
+ }
130
+ const via = proxyUrl ? proxyUrl.replace(/:([^:@/]+)@/, ':***@') : undefined;
131
+ this.log(`[✓ IP] IP đầu ra: ${ip || 'không xác định'}${via ? ` (qua ${via})` : ' (kết nối trực tiếp)'}`);
132
+ return; // 成功,退出
133
+ }
134
+ this.log(`[IP] Kiểm tra IP đầu ra thất bại: HTTP ${resp.status}`);
135
+ }
136
+ catch (err) {
137
+ this.log(`[IP] Kiểm tra IP đầu ra thất bại: ${err instanceof Error ? err.message : String(err)}`);
138
+ }
139
+ // 失败后尝试换 session 重建代理链
140
+ if (attempt < maxRetries && this.canRefreshProxySession()) {
141
+ this.log(`[IP] Đổi phiên và thử lại (${attempt + 1}/${maxRetries})...`);
142
+ await this.refreshProxySession();
143
+ }
144
+ }
145
+ if (this.cfg.strictProxy) {
146
+ throw new Error('[NetworkGuard] Không thể xác minh IP đầu ra của route đã khóa; đã dừng đăng ký');
147
+ }
148
+ this.log('[IP] Tất cả lần kiểm tra IP đầu ra đều thất bại, tiếp tục bằng kết nối hệ thống');
149
+ }
150
+ /** 判断当前代理是否支持 session 轮换(参数化格式 + 含 _session- 或含 _area-/_life- 等) */
151
+ canRefreshProxySession() {
152
+ return false;
153
+ }
154
+ /** 重新随机 session 并重建代理链 */
155
+ async refreshProxySession() {
156
+ // 还原到原始目标代理 URL(代理链会把 cfg.proxy 替换为本地中继地址)
157
+ const original = this.chainTargetProxy || this.cfg.proxy || '';
158
+ if (!original)
159
+ return;
160
+ // 替换或追加 _session-随机值
161
+ const session = Array.from({ length: 8 }, () => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'[Math.floor(Math.random() * 62)]).join('');
162
+ let newTarget;
163
+ if (/_session-[^_:@/]*/i.test(original)) {
164
+ // 已有 _session-xxx → 替换
165
+ newTarget = original.replace(/(_session-)[^_:@/]*/i, `$1${session}`);
166
+ }
167
+ else {
168
+ // 没有 _session- → 在 : 或 @ 之前插入
169
+ const atIdx = original.indexOf('@');
170
+ const colonIdx = original.indexOf(':', original.indexOf('://') + 3);
171
+ const insertPos = colonIdx > 0 && colonIdx < atIdx ? colonIdx : atIdx;
172
+ newTarget = original.slice(0, insertPos) + `_session-${session}` + original.slice(insertPos);
173
+ }
174
+ this.log(`[IP] Phiên mới: ${newTarget.replace(/:([^:@/]+)@/, ':***@')}`);
175
+ // 停掉旧代理链
176
+ if (this.chainRelay) {
177
+ await this.chainRelay.stop();
178
+ this.chainRelay = null;
179
+ }
180
+ // 重建
181
+ this.cfg.proxy = newTarget;
182
+ this.chainTargetProxy = '';
183
+ await this.setupProxyChain();
184
+ }
185
+ /** TLS SessionClient 选项 */
186
+ get sessionOpts() {
187
+ const explicit = (this.cfg.proxy && this.cfg.proxy.trim()) || undefined;
188
+ // 严格模式:必须有显式代理,禁止回退到环境变量/系统代理,避免网络路径静默改变。
189
+ if (this.cfg.strictProxy) {
190
+ if (!explicit) {
191
+ throw new Error('Chế độ proxy nghiêm ngặt: cfg.proxy đang trống, đã dừng để tránh tự động đổi cấu hình mạng');
192
+ }
193
+ }
194
+ const proxyUrl = this.cfg.strictProxy
195
+ ? explicit
196
+ : (explicit
197
+ || process.env.HTTPS_PROXY || process.env.https_proxy
198
+ || process.env.HTTP_PROXY || process.env.http_proxy
199
+ || (0, systemProxy_1.getSystemProxy)() || undefined);
200
+ return {
201
+ tlsClientIdentifier: 'chrome_146',
202
+ // 25s:AWS 注册 API 正常响应 1-5s,慢住宅代理 10-15s;超过基本是挂起。
203
+ // 配合 sendRequest 的 3 次重试,单步最坏 ~75s(旧值 60s 会到 ~180s,是批量卡 1-5 分钟主因)
204
+ timeoutSeconds: 25,
205
+ followRedirects: true,
206
+ insecureSkipVerify: true,
207
+ // 多线程隔离:固定 sessionId 隔离 DLL 层面共享的 TLS session cache
208
+ // 整个 Registrar 生命周期内用同一个 ID,避免 rebuildTlsClient 产生僵尸 session
209
+ sessionId: this.tlsSessionId,
210
+ proxyUrl
211
+ };
212
+ }
213
+ /**
214
+ * 初始化 TLS 客户端
215
+ *
216
+ * DLL 存储策略(按优先级,从高到低):
217
+ * 1. userData/tls-client/ — 应用用户数据目录(系统不会清理,**永久复用**)
218
+ * 2. resources/ — 应用安装目录(打包资源,开发版可能不存在)
219
+ * 3. tmpdir → 自动迁移到 userData(老版本兼容)
220
+ * 4. GitHub 下载到 userData(最后兜底,仅首次)
221
+ */
222
+ async initTlsClient() {
223
+ const { existingPath, downloadDir } = this.ensureTlsLib();
224
+ const opts = existingPath
225
+ ? { customLibraryPath: existingPath }
226
+ : { customLibraryDownloadPath: downloadDir };
227
+ // 共享池:首次注册才真正 open(DLL+worker pool),之后所有注册秒级复用
228
+ this.moduleClient = await (0, tlsClientPool_1.acquireModuleClient)(opts);
229
+ this.log('[TLS] using shared ModuleClient, pool stats: ' + JSON.stringify(this.moduleClient.getPoolStats()));
230
+ this.session = new tlsclientwrapper_1.SessionClient(this.moduleClient, this.sessionOpts);
231
+ }
232
+ /**
233
+ * 确保 tls-client 共享库可用
234
+ * @returns existingPath 已经存在的完整 DLL 文件路径(如有,传 customLibraryPath)
235
+ * downloadDir 需要下载到的目录(如未找到,传 customLibraryDownloadPath 让 tlsclientwrapper 自动下载)
236
+ *
237
+ * 优先放到 userData,避免被系统临时目录清理工具误删(之前用 tmpdir 会被清理)
238
+ */
239
+ ensureTlsLib() {
240
+ const os = require('os');
241
+ const path = require('path');
242
+ const fs = require('fs');
243
+ const platform = os.platform();
244
+ const arch = os.arch();
245
+ let filename = 'tls-client-xgo-1.14.0-';
246
+ if (platform === 'win32') {
247
+ filename += (arch.includes('64') ? 'windows-amd64' : 'windows-386') + '.dll';
248
+ }
249
+ else if (platform === 'darwin') {
250
+ filename += (arch === 'arm64' ? 'darwin-arm64' : 'darwin-amd64') + '.dylib';
251
+ }
252
+ else {
253
+ filename += (arch === 'arm64' ? 'linux-arm64' : 'linux-amd64') + '.so';
254
+ }
255
+ // 1. userData 永久目录(首选)
256
+ const userDataDir = (0, runtimePaths_1.getRuntimeUserDataPath)();
257
+ const tlsClientDir = path.join(userDataDir, 'tls-client');
258
+ const finalPath = path.join(tlsClientDir, filename);
259
+ // 确保目录存在
260
+ try {
261
+ fs.mkdirSync(tlsClientDir, { recursive: true });
262
+ }
263
+ catch { /* ignore */ }
264
+ // 已存在 → 直接复用
265
+ if (fs.existsSync(finalPath)) {
266
+ this.log('[TLS] Library reused from userData (persistent): ' + finalPath);
267
+ return { existingPath: finalPath, downloadDir: tlsClientDir };
268
+ }
269
+ // 2. 从打包资源复制(安装包自带)
270
+ const resourceCandidates = [
271
+ path.join(process.resourcesPath || '', filename),
272
+ path.join(__dirname, '..', '..', '..', 'resources', filename)
273
+ ];
274
+ const resourcePath = resourceCandidates.find((candidate) => fs.existsSync(candidate));
275
+ if (resourcePath) {
276
+ this.log('[TLS] Copying library from resources to userData (one-time): ' + resourcePath + ' -> ' + finalPath);
277
+ try {
278
+ fs.copyFileSync(resourcePath, finalPath);
279
+ return { existingPath: finalPath, downloadDir: tlsClientDir };
280
+ }
281
+ catch (err) {
282
+ this.log('[TLS] Failed to copy from resources: ' + err.message);
283
+ }
284
+ }
285
+ // 3. 兼容老版本:检测 tmpdir 副本并迁移到 userData
286
+ const tmpPath = path.join(os.tmpdir(), filename);
287
+ if (fs.existsSync(tmpPath)) {
288
+ this.log('[TLS] Migrating library from tmpdir to userData: ' + tmpPath + ' -> ' + finalPath);
289
+ try {
290
+ fs.copyFileSync(tmpPath, finalPath);
291
+ return { existingPath: finalPath, downloadDir: tlsClientDir };
292
+ }
293
+ catch (err) {
294
+ this.log('[TLS] Migration failed, will use tmpdir as fallback: ' + err.message);
295
+ return { existingPath: tmpPath, downloadDir: tlsClientDir };
296
+ }
297
+ }
298
+ // 4. 都没有 → 返回 downloadDir,让 tlsclientwrapper open() 自动下载到此目录(永久保存)
299
+ this.log('[TLS] Library not found, will download from GitHub to userData (one-time): ' + tlsClientDir);
300
+ return { downloadDir: tlsClientDir };
301
+ }
302
+ async rebuildTlsClient() {
303
+ // 只重建轻量级的 SessionClient(新 TLS 连接),复用重量级的 ModuleClient(worker pool + DLL)
304
+ // 之前的实现会 terminate + 重新 open ModuleClient,导致每次注册创建 2 个 worker pool
305
+ try {
306
+ await this.session?.destroySession();
307
+ }
308
+ catch { /* ignore */ }
309
+ if (!this.moduleClient) {
310
+ await this.initTlsClient();
311
+ return;
312
+ }
313
+ this.session = new tlsclientwrapper_1.SessionClient(this.moduleClient, this.sessionOpts);
314
+ }
315
+ /**
316
+ * 用 undici 直接 fetch 静态资源(如 AWS signin app.js),绕过 tls-client。
317
+ * 原因:tls-client 的 dll 是进程级单例,失败请求会污染其全局状态,
318
+ * 导致后续重建 SessionClient 后仍报 "no tls client for modification check"。
319
+ * 静态资源不需要 TLS 指纹伪装,直接用 Node/undici fetch 即可。
320
+ */
321
+ async fetchAppJS(url, init) {
322
+ const proxyUrl = (this.cfg.proxy && this.cfg.proxy.trim())
323
+ || process.env.HTTPS_PROXY || process.env.https_proxy
324
+ || process.env.HTTP_PROXY || process.env.http_proxy
325
+ || (0, systemProxy_1.getSystemProxy)() || undefined;
326
+ const agent = (0, systemProxy_1.safeCreateProxyAgent)(proxyUrl);
327
+ if (agent) {
328
+ const resp = await (0, undici_1.fetch)(url, { ...init, dispatcher: agent });
329
+ return resp;
330
+ }
331
+ return await fetch(url, init);
332
+ }
333
+ isRecoverableTlsClientError(err) {
334
+ if (!(err instanceof Error))
335
+ return false;
336
+ return err.message.includes('EOF')
337
+ || err.message.includes('no tls client for modification check')
338
+ || err.message.includes('failed to modify existing client');
339
+ }
340
+ /** 清理 TLS 客户端资源:仅销毁 SessionClient;ModuleClient 是进程级共享池,不再每次 terminate */
341
+ async cleanup() {
342
+ if (this.chainRelay) {
343
+ try {
344
+ await this.chainRelay.stop();
345
+ }
346
+ catch { /* ignore */ }
347
+ this.chainRelay = null;
348
+ }
349
+ if (this.session) {
350
+ // destroySession 带 3 秒超时:Go runtime 的 idle connections 可能要等 60 秒才关闭
351
+ const s = this.session;
352
+ this.session = null;
353
+ try {
354
+ await Promise.race([
355
+ s.destroySession(),
356
+ new Promise(resolve => setTimeout(resolve, 3000))
357
+ ]);
358
+ }
359
+ catch { /* ignore */ }
360
+ }
361
+ // moduleClient 是共享引用,不能 terminate(会影响其它正在跑的注册)
362
+ this.moduleClient = null;
363
+ }
364
+ /** 公共销毁方法,供外部调用释放资源。同时 abort 所有进行中的异步操作。 */
365
+ async destroy() {
366
+ this.abortController.abort();
367
+ await this.cleanup();
368
+ }
369
+ // ============ HTTP 工具方法 ============
370
+ cookieString() {
371
+ return Array.from(this.cookies.entries()).map(([k, v]) => `${k}=${v}`).join('; ');
372
+ }
373
+ buildHeaders(referer, origin) {
374
+ const h = {
375
+ 'Accept': 'application/json, text/plain, */*',
376
+ 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
377
+ 'Accept-Encoding': 'gzip, deflate, br',
378
+ 'Content-Type': 'application/json',
379
+ 'User-Agent': this.identity.ua,
380
+ 'sec-ch-ua': this.secUA,
381
+ 'sec-ch-ua-mobile': '?0',
382
+ 'sec-ch-ua-platform': '"Windows"',
383
+ 'sec-fetch-dest': 'empty',
384
+ 'sec-fetch-mode': 'cors',
385
+ 'sec-fetch-site': 'same-origin'
386
+ };
387
+ if (referer)
388
+ h['Referer'] = referer;
389
+ if (origin)
390
+ h['Origin'] = origin;
391
+ if (this.cookies.size > 0)
392
+ h['Cookie'] = this.cookieString();
393
+ return h;
394
+ }
395
+ buildProfileHeaders(referer) {
396
+ const h = {
397
+ 'Accept': '*/*',
398
+ 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
399
+ 'Content-Type': 'application/json;charset=UTF-8',
400
+ 'User-Agent': this.identity.ua,
401
+ 'Origin': this.cfg.profileBase,
402
+ 'Referer': referer,
403
+ 'sec-ch-ua': this.secUA,
404
+ 'sec-ch-ua-mobile': '?0',
405
+ 'sec-ch-ua-platform': '"Windows"',
406
+ 'sec-fetch-dest': 'empty',
407
+ 'sec-fetch-mode': 'cors',
408
+ 'sec-fetch-site': 'same-origin',
409
+ 'priority': 'u=1, i'
410
+ };
411
+ const keys = ['awsccc', 'aws-user-profile-ubid', 'i18next'];
412
+ if (this.cookies.has('awsd2c-token'))
413
+ keys.push('awsd2c-token', 'awsd2c-token-c');
414
+ const parts = keys.filter((k) => this.cookies.has(k)).map((k) => `${k}=${this.cookies.get(k)}`);
415
+ if (parts.length)
416
+ h['Cookie'] = parts.join('; ');
417
+ return h;
418
+ }
419
+ async doGet(url, headers) {
420
+ return this.sendRequest('GET', url, headers);
421
+ }
422
+ async doPost(url, payload, headers) {
423
+ return this.sendRequest('POST', url, headers, JSON.stringify(payload));
424
+ }
425
+ /** 网络层退避时长:指数 + 抖动(约 0.8s / 1.6s / 3.2s,封顶 8s) */
426
+ netBackoffMs(attempt) {
427
+ const base = Math.min(800 * Math.pow(2, attempt - 1), 8000);
428
+ return base + Math.floor(Math.random() * 400);
429
+ }
430
+ /**
431
+ * 判断响应是否为「瞬时失败」需要重试。
432
+ * 关键:tlsclientwrapper 会把连接层失败(EOF / 重置 / 超时)包装成 status=0 + body 错误描述,
433
+ * 并不抛异常;若不在响应层识别,会被上层当成业务失败直接判死号(如 #9 的「未获取到加密公钥」)。
434
+ */
435
+ isTransientResponse(status, body) {
436
+ if (status === 0 || status === 429 || status === 502 || status === 503 || status === 504)
437
+ return true;
438
+ const lower = body.toLowerCase();
439
+ return lower.includes('failed to do request') || lower.includes('eof')
440
+ || lower.includes('connection reset') || lower.includes('timeout');
441
+ }
442
+ /**
443
+ * 判断是否为「超时类」失败(出口链路慢 / 网络抖动 / 隧道挂起)。
444
+ * 这类失败重建 TLS 通常无用,应 refresh proxy session 后重建链路。
445
+ */
446
+ isTimeoutResponse(status, body) {
447
+ if (status === 504)
448
+ return true;
449
+ if (status !== 0)
450
+ return false;
451
+ const lower = body.toLowerCase();
452
+ return lower.includes('timeout') || lower.includes('deadline')
453
+ || lower.includes('client.timeout') || lower.includes('failed to do request');
454
+ }
455
+ /**
456
+ * 统一的 TLS 请求发送:对瞬时网络失败(status=0 / EOF / 5xx / 429)自动「重建 TLS + 指数退避」重试。
457
+ * 连接类失败才重建客户端,限流类仅退避;cookies 存于 this.cookies,不随重建丢失。
458
+ */
459
+ async sendRequest(method, url, headers, body) {
460
+ if (!this.session)
461
+ throw new Error('TLS client chưa được khởi tạo');
462
+ const maxAttempts = 3;
463
+ let lastErr = null;
464
+ let sessionRefreshed = false; // 整个请求最多换 1 次 proxy session,避免频繁停建代理链
465
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
466
+ try {
467
+ const resp = method === 'GET'
468
+ ? await this.session.get(url, { headers })
469
+ : await this.session.post(url, body ?? '', { headers });
470
+ const decoded = this.decodeBody(resp.body);
471
+ const status = resp.status;
472
+ if (attempt < maxAttempts && this.isTransientResponse(status, decoded)) {
473
+ const broken = status === 0 || /eof|reset|failed to do request/i.test(decoded);
474
+ // 超时类(出口链路慢/网络抖动/隧道挂起):refresh proxy session 后重建链路。
475
+ if (this.isTimeoutResponse(status, decoded) && !sessionRefreshed && this.canRefreshProxySession()) {
476
+ this.log(`[Mạng] ${method} hết thời gian chờ (status=${status}), làm mới phiên proxy rồi thử lại ${attempt}/${maxAttempts - 1}`);
477
+ try {
478
+ await this.refreshProxySession();
479
+ await this.rebuildTlsClient();
480
+ sessionRefreshed = true;
481
+ }
482
+ catch (e) {
483
+ this.log(`[Mạng] Đổi phiên thất bại, chuyển sang tạo lại kết nối thông thường: ${e instanceof Error ? e.message : String(e)}`);
484
+ await this.rebuildTlsClient();
485
+ }
486
+ }
487
+ else {
488
+ this.log(`[Mạng] ${method} tạm thời thất bại status=${status}, ${broken ? 'tạo lại TLS + ' : ''}chờ rồi thử lại ${attempt}/${maxAttempts - 1}`);
489
+ if (broken)
490
+ await this.rebuildTlsClient();
491
+ }
492
+ await this.abortableSleep(this.netBackoffMs(attempt));
493
+ continue;
494
+ }
495
+ return { body: decoded, status, headers: (resp.headers || {}) };
496
+ }
497
+ catch (err) {
498
+ lastErr = err;
499
+ if (attempt < maxAttempts && this.isRecoverableTlsClientError(err)) {
500
+ this.log(`[TLS] ${method} gặp lỗi có thể khôi phục: ${err instanceof Error ? err.message : String(err)}, tạo lại TLS rồi thử lại ${attempt}/${maxAttempts - 1}`);
501
+ await this.rebuildTlsClient();
502
+ await this.abortableSleep(this.netBackoffMs(attempt));
503
+ continue;
504
+ }
505
+ throw err;
506
+ }
507
+ }
508
+ if (lastErr)
509
+ throw lastErr;
510
+ throw new Error(`${method} ${url} vẫn thất bại sau ${maxAttempts} lần thử`);
511
+ }
512
+ /** 可被中止打断的 sleep:停止注册时立即结束等待,让 abort 即时生效 */
513
+ abortableSleep(ms) {
514
+ const signal = this.abortController.signal;
515
+ return new Promise((resolve, reject) => {
516
+ if (signal.aborted) {
517
+ reject(new Error('Đăng ký đã bị hủy'));
518
+ return;
519
+ }
520
+ let timer;
521
+ const onAbort = () => { clearTimeout(timer); reject(new Error('Đăng ký đã bị hủy')); };
522
+ timer = setTimeout(() => { signal.removeEventListener('abort', onAbort); resolve(); }, ms);
523
+ signal.addEventListener('abort', onAbort, { once: true });
524
+ });
525
+ }
526
+ /** 拟人随机延迟:步骤之间停顿,降低机械化节奏特征 */
527
+ async humanDelay(min = 280, max = 1200) {
528
+ await this.abortableSleep(min + Math.floor(Math.random() * Math.max(1, max - min)));
529
+ }
530
+ /**
531
+ * 整体超时看门狗:给任意步骤 Promise 加上限,超时后 reject(原 Promise 在后台自生自灭)。
532
+ * 用于批量场景快速释放卡住的线程,避免单个账号占用并发槽 1-5 分钟。支持 abort 即时中断。
533
+ */
534
+ withTimeout(p, ms, label) {
535
+ const signal = this.abortController.signal;
536
+ return new Promise((resolve, reject) => {
537
+ if (signal.aborted) {
538
+ reject(new Error('Đăng ký đã bị hủy'));
539
+ return;
540
+ }
541
+ let done = false;
542
+ const settle = (fn) => {
543
+ if (done)
544
+ return;
545
+ done = true;
546
+ clearTimeout(timer);
547
+ signal.removeEventListener('abort', onAbort);
548
+ fn();
549
+ };
550
+ const timer = setTimeout(() => settle(() => reject(new Error(`${label} hết thời gian chờ tổng thể ${Math.round(ms / 1000)} giây`))), ms);
551
+ const onAbort = () => settle(() => reject(new Error('Đăng ký đã bị hủy')));
552
+ signal.addEventListener('abort', onAbort, { once: true });
553
+ p.then((v) => settle(() => resolve(v)), (e) => settle(() => reject(e)));
554
+ });
555
+ }
556
+ /**
557
+ * 幂等步骤重试:失败后退避重试(仅用于无副作用的前置步骤,如 OIDC / Device / Portal / WorkflowInit)。
558
+ * - timeoutMs:每次尝试加整体超时看门狗,超时即判失败进入下一次(防止单次卡满 3×25s)
559
+ * - refreshSession:失败后若代理支持,refresh proxy session 再退避(处理慢链路或隧道挂起)
560
+ */
561
+ async retryStep(name, fn, attempts, opts) {
562
+ let lastErr = null;
563
+ for (let i = 1; i <= attempts; i++) {
564
+ try {
565
+ if (opts?.timeoutMs)
566
+ await this.withTimeout(fn(), opts.timeoutMs, name);
567
+ else
568
+ await fn();
569
+ return;
570
+ }
571
+ catch (err) {
572
+ lastErr = err;
573
+ if (i < attempts) {
574
+ // 幂等步骤失败:若支持 session refresh,先重建代理会话再退避。
575
+ if (opts?.refreshSession && this.canRefreshProxySession()) {
576
+ try {
577
+ await this.refreshProxySession();
578
+ await this.rebuildTlsClient();
579
+ this.log(`[${name}] Đã làm mới phiên proxy`);
580
+ }
581
+ catch { /* 换 session 失败则继续普通重试 */ }
582
+ }
583
+ const wait = 1500 * i + Math.floor(Math.random() * 800);
584
+ this.log(`[${name}] Lần ${i}/${attempts} thất bại: ${err.message}, thử lại sau ${wait}ms`);
585
+ await this.abortableSleep(wait);
586
+ }
587
+ }
588
+ }
589
+ throw lastErr;
590
+ }
591
+ /**
592
+ * tls-client 返回的 body 是字节透传字符串(latin1);
593
+ * 如果响应实际是 UTF-8 编码(含中文等多字节),需要二次解码。
594
+ * 实现:把 string 当作 latin1 字节读回,再用 UTF-8 解码;
595
+ * 若解码后含 U+FFFD 替换字符比原文多很多,则回退原值(说明原本就是 latin1 / ASCII)。
596
+ */
597
+ decodeBody(body) {
598
+ if (!body)
599
+ return '';
600
+ try {
601
+ // 快速路径:纯 ASCII 直接返回
602
+ // eslint-disable-next-line no-control-regex
603
+ if (/^[\x00-\x7F]*$/.test(body))
604
+ return body;
605
+ const buf = Buffer.from(body, 'latin1');
606
+ const utf8 = buf.toString('utf-8');
607
+ // 检测 mojibake:原文如果在 latin1 解码 UTF-8 字节,会出现大量字符在 \u00a0-\u00ff 区间
608
+ // 重解后如果替换字符数量明显多于原文,说明不是 UTF-8,回退原值
609
+ const replaceInOriginal = (body.match(/\uFFFD/g) || []).length;
610
+ const replaceInUtf8 = (utf8.match(/\uFFFD/g) || []).length;
611
+ if (replaceInUtf8 > replaceInOriginal + 2)
612
+ return body;
613
+ return utf8;
614
+ }
615
+ catch {
616
+ return body;
617
+ }
618
+ }
619
+ parseBody(body) {
620
+ try {
621
+ return JSON.parse(body);
622
+ }
623
+ catch {
624
+ return {};
625
+ }
626
+ }
627
+ /**
628
+ * 识别 AWS 风控触发的错误响应,返回人类可读的标签
629
+ * @returns 风控类型标签(如 'AWS-RISK-CONTROL'),不是风控返回 null
630
+ */
631
+ detectRiskControl(body, status) {
632
+ if (status !== 400)
633
+ return null;
634
+ const lower = body.toLowerCase();
635
+ // 中文消息(已正确解码)
636
+ if (body.includes('请稍后再试') && body.includes('管理员'))
637
+ return 'AWS-RISK-CONTROL';
638
+ if (body.includes('发生意外错误'))
639
+ return 'AWS-RISK-CONTROL';
640
+ // 英文消息
641
+ if (lower.includes('try again later') && lower.includes('administrator'))
642
+ return 'AWS-RISK-CONTROL';
643
+ if (lower.includes('unexpected error') && lower.includes('contact'))
644
+ return 'AWS-RISK-CONTROL';
645
+ return null;
646
+ }
647
+ /** 把响应错误格式化为更友好的消息(含风控识别) */
648
+ formatErrorBody(body, status) {
649
+ const risk = this.detectRiskControl(body, status);
650
+ if (risk) {
651
+ return `${risk} (AWS đã chặn yêu cầu; đề xuất: 1) dừng tác vụ hàng loạt hiện tại; 2) bật giới hạn tốc độ và tự động tạm dừng; 3) tránh đăng ký hàng loạt cùng một tên miền email; 4) nếu tài khoản bị hạn chế, liên hệ Support theo hướng dẫn của AWS/Kiro)`;
652
+ }
653
+ return `status=${status} body=${body.substring(0, 200)}`;
654
+ }
655
+ async fetchD2CToken(origin, referer) {
656
+ const headers = {
657
+ 'Accept': '*/*', 'Content-Type': 'application/json',
658
+ 'User-Agent': this.identity.ua, 'Origin': origin, 'Referer': referer,
659
+ 'sec-ch-ua': this.secUA, 'sec-ch-ua-mobile': '?0',
660
+ 'sec-ch-ua-platform': '"Windows"', 'sec-fetch-dest': 'empty',
661
+ 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'cross-site', 'priority': 'u=1, i'
662
+ };
663
+ const parts = [];
664
+ if (this.cookies.has('awsccc'))
665
+ parts.push('awsccc=' + this.cookies.get('awsccc'));
666
+ if (this.cookies.has('awsd2c-token')) {
667
+ const old = this.cookies.get('awsd2c-token');
668
+ parts.push('awsd2c-token=' + old, 'awsd2c-token-c=' + old);
669
+ }
670
+ if (parts.length)
671
+ headers['Cookie'] = parts.join('; ');
672
+ const payload = {};
673
+ if (this.cookies.has('awsd2c-token'))
674
+ payload.token = this.cookies.get('awsd2c-token');
675
+ const resp = await this.doPost('https://vs.aws.amazon.com/token', payload, headers);
676
+ (0, http_utils_1.saveCookies)(this.cookies, resp.headers);
677
+ const data = this.parseBody(resp.body);
678
+ const tok = data.token;
679
+ if (tok) {
680
+ this.cookies.set('awsd2c-token', tok);
681
+ this.cookies.set('awsd2c-token-c', tok);
682
+ // 从 JWT 中提取 visitor ID
683
+ const jwtParts = tok.split('.');
684
+ if (jwtParts.length >= 2) {
685
+ try {
686
+ const decoded = JSON.parse(Buffer.from(jwtParts[1], 'base64url').toString());
687
+ if (decoded.vid)
688
+ this.vid = decoded.vid;
689
+ }
690
+ catch { /* ignore */ }
691
+ }
692
+ }
693
+ }
694
+ // ============ 指纹生成 ============
695
+ genFP(pageType, eventType, emailLen, emailAddr) {
696
+ return this.genFPWithTime(pageType, eventType, 0, emailLen, emailAddr);
697
+ }
698
+ genFPWithTime(pageType, eventType, timeOnPage, emailLen, emailAddr) {
699
+ const did = this.cfg.directoryId;
700
+ let loc = '', ref = '';
701
+ switch (pageType) {
702
+ case 'signin':
703
+ loc = `${this.cfg.signinBase}/platform/${did}/login?workflowStateHandle=${this.workflowHandle}`;
704
+ break;
705
+ case 'signup':
706
+ loc = `${this.cfg.signinBase}/platform/${did}/signup?workflowStateHandle=${this.workflowHandle}`;
707
+ break;
708
+ default: // profile
709
+ if (eventType === 'PageSubmit') {
710
+ loc = `${this.cfg.profileBase}/?workflowID=${this.workflowId}#/signup/enter-email`;
711
+ }
712
+ else {
713
+ loc = `${this.cfg.profileBase}/?workflowID=${this.workflowId}#/signup/start`;
714
+ }
715
+ if (!this.workflowId)
716
+ loc = this.cfg.profileBase + '/';
717
+ }
718
+ if (pageType === 'profile') {
719
+ ref = `${this.cfg.signinBase}/platform/${did}/signup?workflowStateHandle=${this.workflowHandle}`;
720
+ }
721
+ else {
722
+ ref = this.cfg.viewBase + '/';
723
+ }
724
+ return (0, fingerprint_1.generateFingerprint)(this.identity, loc, ref, this.fpCtx, pageType, eventType, timeOnPage, emailLen, emailAddr);
725
+ }
726
+ // ============ 注册步骤 ============
727
+ async step1OIDC() {
728
+ this.emitStep('oidc');
729
+ this.log('[1] Đăng ký OIDC');
730
+ const payload = {
731
+ clientName: 'Amazon Q Developer for command line',
732
+ clientType: 'public',
733
+ scopes: ['codewhisperer:completions', 'codewhisperer:analysis', 'codewhisperer:conversations', 'codewhisperer:transformations', 'codewhisperer:taskassist']
734
+ };
735
+ const headers = { 'Content-Type': 'application/json' };
736
+ let resp = null;
737
+ for (let attempt = 0; attempt < 3; attempt++) {
738
+ try {
739
+ resp = await this.doPost(this.cfg.oidcBase + '/client/register', payload, headers);
740
+ if (resp.status === 200)
741
+ break;
742
+ }
743
+ catch (err) {
744
+ if (attempt < 2) {
745
+ this.log(`[1] Thử lại OIDC (${attempt + 1}/3)...`);
746
+ await this.abortableSleep(2000 * (attempt + 1));
747
+ await this.rebuildTlsClient();
748
+ continue;
749
+ }
750
+ throw err;
751
+ }
752
+ }
753
+ if (!resp)
754
+ throw new Error('Đăng ký OIDC thất bại: tất cả lần thử đều thất bại');
755
+ const data = this.parseBody(resp.body);
756
+ this.clientId = data.clientId || '';
757
+ this.clientSecret = data.clientSecret || '';
758
+ if (!this.clientId)
759
+ throw new Error(`Đăng ký OIDC thất bại: ${resp.body.slice(0, 200)}`);
760
+ }
761
+ async step2Device() {
762
+ this.emitStep('device');
763
+ this.log('[2] Cấp quyền thiết bị');
764
+ const resp = await this.doPost(this.cfg.oidcBase + '/device_authorization', {
765
+ clientId: this.clientId, clientSecret: this.clientSecret,
766
+ startUrl: this.cfg.startURL
767
+ }, { 'Content-Type': 'application/json' });
768
+ const data = this.parseBody(resp.body);
769
+ this.deviceCode = data.deviceCode || '';
770
+ this.userCode = data.userCode || '';
771
+ this.log(`user_code=${this.userCode}`);
772
+ }
773
+ async step3Email() {
774
+ if (this.cfg.manualMode)
775
+ return; // 手动模式在外部设置
776
+ if (this.cfg.useOutlook && this.cfg.outlookData) {
777
+ this.log('[3] Sử dụng email Outlook');
778
+ const accounts = (0, email_service_1.parseOutlookLines)(this.cfg.outlookData);
779
+ if (accounts.length === 0)
780
+ throw new Error('Không có tài khoản Outlook khả dụng');
781
+ // 单行 → 直接用(批量并发时前端已为每个 task 切一行,避免并发抢占)
782
+ // 多行(单次注册)→ 随机挑一行
783
+ const acc = accounts.length === 1
784
+ ? accounts[0]
785
+ : accounts[Math.floor(Math.random() * accounts.length)];
786
+ this.email = acc.email;
787
+ this.emitStep('email-created');
788
+ this.log(`email=${this.email}`);
789
+ return;
790
+ }
791
+ if (this.cfg.useTempMailPlus) {
792
+ this.log('[3] Sử dụng email tên miền riêng (TempMail.Plus)');
793
+ if (!this.cfg.tempMailPlusEmail || !this.cfg.tempMailPlusEpin || !this.cfg.tempMailPlusDomain) {
794
+ throw new Error('Cấu hình TempMail.Plus chưa đầy đủ');
795
+ }
796
+ this.emailSvc = new email_service_1.TempMailPlusService(this.cfg.tempMailPlusEmail, this.cfg.tempMailPlusEpin, this.cfg.tempMailPlusDomain);
797
+ this.email = await this.emailSvc.create();
798
+ if (!this.email)
799
+ throw new Error('Tạo địa chỉ email thất bại');
800
+ this.emitStep('email-created');
801
+ this.log(`email=${this.email}`);
802
+ return;
803
+ }
804
+ if (this.cfg.useTingamefiMail) {
805
+ this.log('[3] Sử dụng email tạm Tingamefi');
806
+ if (!this.cfg.tingamefiMailApiUrl || !this.cfg.tingamefiMailAdminPassword || !this.cfg.tingamefiMailDomain) {
807
+ throw new Error('Cấu hình email Tingamefi chưa đầy đủ');
808
+ }
809
+ this.emailSvc = new email_service_1.TingamefiMailService(this.cfg.tingamefiMailApiUrl, this.cfg.tingamefiMailAdminPassword, this.cfg.tingamefiMailDomain);
810
+ this.email = await this.emailSvc.create();
811
+ if (!this.email)
812
+ throw new Error('Tạo địa chỉ email Tingamefi thất bại');
813
+ this.emitStep('email-created');
814
+ this.log(`email=${this.email}`);
815
+ return;
816
+ }
817
+ if (this.cfg.useProton) {
818
+ this.log('[3] Sử dụng email Proton (bí danh dấu chấm)');
819
+ if (!this.cfg.protonEmail) {
820
+ throw new Error('Chưa cấu hình địa chỉ email Proton');
821
+ }
822
+ this.emailSvc = new email_service_1.ProtonWebviewService(this.cfg.protonEmail, (m) => this.log(m));
823
+ this.email = await this.emailSvc.create();
824
+ if (!this.email)
825
+ throw new Error('Địa chỉ email Proton đang trống');
826
+ this.emitStep('email-created');
827
+ this.log(`email=${this.email}`);
828
+ return;
829
+ }
830
+ this.log('[3] Tạo email tạm');
831
+ if (!this.cfg.moEmailBaseURL)
832
+ throw new Error('Chưa cấu hình MoEmail');
833
+ this.emailSvc = new email_service_1.MoEmailService(this.cfg.moEmailBaseURL, this.cfg.moEmailAPIKey);
834
+ this.email = await this.emailSvc.create();
835
+ if (!this.email)
836
+ throw new Error('Tạo email tạm thất bại');
837
+ this.emitStep('email-created');
838
+ this.log(`email=${this.email}`);
839
+ }
840
+ async step4Portal() {
841
+ this.emitStep('portal');
842
+ this.log('[4] Khởi tạo Portal');
843
+ this.cookies.set('awsccc', (0, http_utils_1.awsccc)());
844
+ const redirect = `${this.cfg.viewBase}/start/#/device?user_code=${this.userCode}`;
845
+ const url = `${this.cfg.portalBase}/login?directory_id=view&redirect_url=${redirect}`;
846
+ const h = {
847
+ 'Accept': 'application/json, text/plain, */*',
848
+ 'Content-Type': 'application/json',
849
+ 'Origin': this.cfg.viewBase,
850
+ 'Referer': this.cfg.viewBase + '/',
851
+ 'User-Agent': this.identity.ua
852
+ };
853
+ const resp = await this.doGet(url, h);
854
+ (0, http_utils_1.saveCookies)(this.cookies, resp.headers);
855
+ const data = this.parseBody(resp.body);
856
+ const rurl = data.redirectUrl || '';
857
+ if (rurl.includes('workflowStateHandle=')) {
858
+ this.workflowHandle = (0, http_utils_1.splitAfter)(rurl, 'workflowStateHandle=');
859
+ }
860
+ if (data.csrfToken)
861
+ this.cookies.set('loginCsrfToken', data.csrfToken);
862
+ if (!this.workflowHandle)
863
+ throw new Error('Portal không trả về workflow handle');
864
+ const loginURL = `${this.cfg.signinBase}/platform/${this.cfg.directoryId}/login?workflowStateHandle=${this.workflowHandle}`;
865
+ await this.fetchD2CToken(this.cfg.signinBase, loginURL);
866
+ }
867
+ async step5WorkflowInit() {
868
+ this.emitStep('workflow-init');
869
+ this.log('[5] Khởi tạo quy trình');
870
+ const api = `${this.cfg.signinBase}/platform/${this.cfg.directoryId}/api/execute`;
871
+ const ref = `${this.cfg.signinBase}/platform/${this.cfg.directoryId}/login?workflowStateHandle=${this.workflowHandle}`;
872
+ let fp = this.genFP('signin', 'first_load', 0, '');
873
+ let rid = (0, http_utils_1.newUUID)();
874
+ let h = this.buildHeaders(ref, this.cfg.signinBase);
875
+ h['x-amzn-requestid'] = rid;
876
+ h['x-amz-date'] = (0, http_utils_1.gmtDate)();
877
+ h['priority'] = 'u=1, i';
878
+ let resp = await this.doPost(api, {
879
+ stepId: '', workflowStateHandle: this.workflowHandle,
880
+ inputs: [{ input_type: 'FingerPrintRequestInput', fingerPrint: fp }],
881
+ requestId: rid
882
+ }, h);
883
+ (0, http_utils_1.saveCookies)(this.cookies, resp.headers);
884
+ let data = this.parseBody(resp.body);
885
+ if (data.workflowStateHandle)
886
+ this.workflowHandle = data.workflowStateHandle;
887
+ if (data.stepId === 'start') {
888
+ fp = this.genFP('signin', 'PageLoad', 0, '');
889
+ rid = (0, http_utils_1.newUUID)();
890
+ h = this.buildHeaders(ref, this.cfg.signinBase);
891
+ h['x-amzn-requestid'] = rid;
892
+ h['x-amz-date'] = (0, http_utils_1.gmtDate)();
893
+ h['priority'] = 'u=1, i';
894
+ resp = await this.doPost(api, {
895
+ stepId: 'start', workflowStateHandle: this.workflowHandle,
896
+ inputs: [{ input_type: 'FingerPrintRequestInput', fingerPrint: fp }],
897
+ requestId: rid
898
+ }, h);
899
+ (0, http_utils_1.saveCookies)(this.cookies, resp.headers);
900
+ data = this.parseBody(resp.body);
901
+ if (data.workflowStateHandle)
902
+ this.workflowHandle = data.workflowStateHandle;
903
+ }
904
+ }
905
+ async step6SubmitEmail() {
906
+ this.emitStep('submit-email');
907
+ this.log(`[6] Gửi email ${this.email}`);
908
+ const api = `${this.cfg.signinBase}/platform/${this.cfg.directoryId}/api/execute`;
909
+ const ref = `${this.cfg.signinBase}/platform/${this.cfg.directoryId}/login?workflowStateHandle=${this.workflowHandle}`;
910
+ const fp = this.genFP('signin', 'PageSubmit', this.email.length, this.email);
911
+ const rid = (0, http_utils_1.newUUID)();
912
+ const h = this.buildHeaders(ref, this.cfg.signinBase);
913
+ h['x-amzn-requestid'] = rid;
914
+ h['x-amz-date'] = (0, http_utils_1.gmtDate)();
915
+ h['priority'] = 'u=1, i';
916
+ const resp = await this.doPost(api, {
917
+ stepId: 'get-identity-user', workflowStateHandle: this.workflowHandle,
918
+ actionId: 'SUBMIT',
919
+ inputs: [
920
+ { input_type: 'UserRequestInput', username: this.email },
921
+ { input_type: 'ApplicationTypeRequestInput', applicationType: 'SSO_INDIVIDUAL_ID' },
922
+ {
923
+ input_type: 'UserEventRequestInput', directoryId: this.cfg.directoryId,
924
+ userName: this.email,
925
+ userEvents: [{ input_type: 'UserEvent', eventType: 'PAGE_SUBMIT', pageName: 'IDENTIFICATION', timeSpentOnPage: 5000 }]
926
+ },
927
+ { input_type: 'FingerPrintRequestInput', fingerPrint: fp }
928
+ ],
929
+ visitorId: this.vid, requestId: rid
930
+ }, h);
931
+ (0, http_utils_1.saveCookies)(this.cookies, resp.headers);
932
+ const data = this.parseBody(resp.body);
933
+ if (data.workflowStateHandle)
934
+ this.workflowHandle = data.workflowStateHandle;
935
+ if (resp.status === 400)
936
+ return 'signup';
937
+ if (resp.status === 200)
938
+ return 'login';
939
+ throw new Error(`Gửi email thất bại: ${resp.status} - ${resp.body.slice(0, 200)}`);
940
+ }
941
+ async step7Signup() {
942
+ this.emitStep('signup');
943
+ this.log('[7] Đăng ký (SIGNUP)');
944
+ const api = `${this.cfg.signinBase}/platform/${this.cfg.directoryId}/api/execute`;
945
+ const ref = `${this.cfg.signinBase}/platform/${this.cfg.directoryId}/login?workflowStateHandle=${this.workflowHandle}`;
946
+ const fp = this.genFP('signup', 'PageSubmit', 0, '');
947
+ const rid = (0, http_utils_1.newUUID)();
948
+ const h = this.buildHeaders(ref, this.cfg.signinBase);
949
+ h['x-amzn-requestid'] = rid;
950
+ h['x-amz-date'] = (0, http_utils_1.gmtDate)();
951
+ h['priority'] = 'u=1, i';
952
+ const resp = await this.doPost(api, {
953
+ stepId: 'get-identity-user', workflowStateHandle: this.workflowHandle,
954
+ actionId: 'SIGNUP',
955
+ inputs: [
956
+ { input_type: 'UserRequestInput', username: this.email },
957
+ { input_type: 'FingerPrintRequestInput', fingerPrint: fp }
958
+ ],
959
+ visitorId: this.vid, requestId: rid
960
+ }, h);
961
+ (0, http_utils_1.saveCookies)(this.cookies, resp.headers);
962
+ const data = this.parseBody(resp.body);
963
+ const redir = data.redirect;
964
+ const rurl = redir?.url;
965
+ if (rurl?.includes('workflowStateHandle=')) {
966
+ this.workflowHandle = (0, http_utils_1.splitAfter)(rurl, 'workflowStateHandle=');
967
+ }
968
+ }
969
+ async step7_5SignupInit() {
970
+ this.log('[7.5] Khởi tạo API đăng ký');
971
+ const api = `${this.cfg.signinBase}/platform/${this.cfg.directoryId}/signup/api/execute`;
972
+ const ref = `${this.cfg.signinBase}/platform/${this.cfg.directoryId}/signup?workflowStateHandle=${this.workflowHandle}`;
973
+ let fp = this.genFP('signup', 'first_load', 0, '');
974
+ let rid = (0, http_utils_1.newUUID)();
975
+ let h = this.buildHeaders(ref, this.cfg.signinBase);
976
+ h['x-amzn-requestid'] = rid;
977
+ h['x-amz-date'] = (0, http_utils_1.gmtDate)();
978
+ h['priority'] = 'u=1, i';
979
+ let resp = await this.doPost(api, {
980
+ stepId: '', workflowStateHandle: this.workflowHandle,
981
+ inputs: [
982
+ { input_type: 'UserRequestInput', username: this.email },
983
+ { input_type: 'FingerPrintRequestInput', fingerPrint: fp }
984
+ ],
985
+ visitorId: this.vid, requestId: rid
986
+ }, h);
987
+ (0, http_utils_1.saveCookies)(this.cookies, resp.headers);
988
+ let data = this.parseBody(resp.body);
989
+ if (data.workflowStateHandle)
990
+ this.workflowHandle = data.workflowStateHandle;
991
+ if (data.stepId !== 'start')
992
+ throw new Error(`Khởi tạo đăng ký thất bại: ${this.formatErrorBody(resp.body, resp.status)}`);
993
+ fp = this.genFP('signup', 'PageLoad', 0, '');
994
+ rid = (0, http_utils_1.newUUID)();
995
+ h = this.buildHeaders(ref, this.cfg.signinBase);
996
+ h['x-amzn-requestid'] = rid;
997
+ h['x-amz-date'] = (0, http_utils_1.gmtDate)();
998
+ h['priority'] = 'u=1, i';
999
+ resp = await this.doPost(api, {
1000
+ stepId: 'start', workflowStateHandle: this.workflowHandle,
1001
+ inputs: [
1002
+ { input_type: 'UserRequestInput', username: this.email },
1003
+ { input_type: 'FingerPrintRequestInput', fingerPrint: fp }
1004
+ ],
1005
+ visitorId: this.vid, requestId: rid
1006
+ }, h);
1007
+ (0, http_utils_1.saveCookies)(this.cookies, resp.headers);
1008
+ data = this.parseBody(resp.body);
1009
+ if (data.workflowStateHandle)
1010
+ this.workflowHandle = data.workflowStateHandle;
1011
+ const redir = data.redirect;
1012
+ const rurl = redir?.url;
1013
+ if (rurl?.includes('workflowID=')) {
1014
+ let wid = (0, http_utils_1.splitAfter)(rurl, 'workflowID=');
1015
+ const hashIdx = wid.indexOf('#');
1016
+ if (hashIdx >= 0)
1017
+ wid = wid.slice(0, hashIdx);
1018
+ this.workflowId = wid;
1019
+ }
1020
+ if (!this.workflowId)
1021
+ throw new Error('Khởi tạo đăng ký không trả về workflowID');
1022
+ }
1023
+ async step7_8ProfileInit() {
1024
+ this.log('[7.8] Khởi tạo trang hồ sơ');
1025
+ this.ubid = (0, http_utils_1.ubidGen)();
1026
+ this.cookies.set('aws-user-profile-ubid', this.ubid);
1027
+ this.cookies.set('i18next', 'zh-CN');
1028
+ if (!this.cookies.has('awsccc'))
1029
+ this.cookies.set('awsccc', (0, http_utils_1.awsccc)());
1030
+ const url = `${this.cfg.profileBase}/?workflowID=${this.workflowId}`;
1031
+ const resp = await this.doGet(url, {
1032
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
1033
+ 'User-Agent': this.identity.ua,
1034
+ 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate'
1035
+ });
1036
+ (0, http_utils_1.saveCookies)(this.cookies, resp.headers);
1037
+ (0, fingerprint_1.resetPerfTiming)(this.fpCtx);
1038
+ await this.fetchD2CToken(this.cfg.profileBase, url);
1039
+ }
1040
+ async step8ProfileStart() {
1041
+ this.log('[8] Khởi động hồ sơ');
1042
+ const ref = `${this.cfg.profileBase}/?workflowID=${this.workflowId}`;
1043
+ const fp = this.genFP('profile', 'PageLoad', 0, '');
1044
+ const resp = await this.doPost(this.cfg.profileBase + '/api/start', {
1045
+ workflowID: this.workflowId,
1046
+ browserData: {
1047
+ attributes: {
1048
+ fingerprint: fp,
1049
+ eventTimestamp: new Date().toISOString().replace(/\.\d{3}Z$/, '.000Z'),
1050
+ timeSpentOnPage: '38', eventType: 'PageLoad',
1051
+ ubid: this.ubid, visitorId: this.vid
1052
+ },
1053
+ cookies: {}
1054
+ }
1055
+ }, this.buildProfileHeaders(ref));
1056
+ const data = this.parseBody(resp.body);
1057
+ this.workflowState = data.workflowState || '';
1058
+ if (!this.workflowState)
1059
+ throw new Error(`Khởi động hồ sơ không trả về workflowState: ${resp.body.slice(0, 200)}`);
1060
+ }
1061
+ async step9SendOTP() {
1062
+ this.emitStep('send-otp');
1063
+ this.log('[9] Gửi mã xác minh');
1064
+ if (this.cfg.useOutlook && this.cfg.outlookData) {
1065
+ const accounts = (0, email_service_1.parseOutlookLines)(this.cfg.outlookData);
1066
+ const acc = accounts.find((a) => a.email === this.email);
1067
+ if (acc) {
1068
+ try {
1069
+ this.outlookMailCount = await (0, email_service_1.getInboxCount)(acc);
1070
+ this.log(`Số email trước khi gửi: ${this.outlookMailCount}`);
1071
+ }
1072
+ catch (err) {
1073
+ this.log(`Lấy số lượng email thất bại: ${err}, dùng giá trị mặc định 0`);
1074
+ }
1075
+ }
1076
+ }
1077
+ const ref = `${this.cfg.profileBase}/?workflowID=${this.workflowId}`;
1078
+ const timeOnPage = 5000 + Math.floor(Math.random() * 3001);
1079
+ const fp = this.genFPWithTime('profile', 'PageSubmit', timeOnPage, this.email.length, this.email);
1080
+ const tsp = String(timeOnPage);
1081
+ const payload = {
1082
+ workflowState: this.workflowState,
1083
+ email: this.email,
1084
+ browserData: {
1085
+ attributes: {
1086
+ fingerprint: fp,
1087
+ eventTimestamp: new Date().toISOString().replace(/\.\d{3}Z$/, '.000Z'),
1088
+ timeSpentOnPage: tsp, pageName: 'EMAIL_COLLECTION',
1089
+ eventType: 'PageSubmit', ubid: this.ubid, visitorId: this.vid
1090
+ },
1091
+ cookies: {}
1092
+ }
1093
+ };
1094
+ const resp = await this.doPost(this.cfg.profileBase + '/api/send-otp', payload, this.buildProfileHeaders(ref));
1095
+ if (resp.status !== 200)
1096
+ throw new Error(`Gửi mã xác minh thất bại (${resp.status}), body: ${resp.body.substring(0, 300)}`);
1097
+ this.log('Đã gửi mã xác minh');
1098
+ }
1099
+ async step10GetOTP() {
1100
+ if (this.cfg.manualMode)
1101
+ throw new Error('Chế độ thủ công yêu cầu cung cấp mã xác minh từ bên ngoài');
1102
+ this.emitStep('waiting-otp');
1103
+ this.log('[10] Chờ mã xác minh');
1104
+ const signal = this.abortController.signal;
1105
+ if (this.cfg.useOutlook && this.cfg.outlookData) {
1106
+ const accounts = (0, email_service_1.parseOutlookLines)(this.cfg.outlookData);
1107
+ const acc = accounts.find((a) => a.email === this.email);
1108
+ if (!acc)
1109
+ throw new Error('Không tìm thấy tài khoản Outlook tương ứng');
1110
+ return await (0, email_service_1.waitForOTP)(acc, this.outlookMailCount, 120, 5, signal);
1111
+ }
1112
+ if (!this.emailSvc)
1113
+ throw new Error('Dịch vụ email chưa được khởi tạo');
1114
+ return await this.emailSvc.waitForCode(120, 3, signal);
1115
+ }
1116
+ async step11CreateIdentity(otp) {
1117
+ this.emitStep('otp-received');
1118
+ this.emitStep('create-identity');
1119
+ this.log('[11] Tạo danh tính');
1120
+ const ref = `${this.cfg.profileBase}/?workflowID=${this.workflowId}`;
1121
+ const fp = this.genFP('profile', 'EmailVerification', 0, '');
1122
+ const resp = await this.doPost(this.cfg.profileBase + '/api/create-identity', {
1123
+ workflowState: this.workflowState,
1124
+ userData: { email: this.email, fullName: this.cfg.fullName },
1125
+ otpCode: otp,
1126
+ browserData: {
1127
+ attributes: {
1128
+ fingerprint: fp,
1129
+ eventTimestamp: new Date().toISOString().replace(/\.\d{3}Z$/, '.000Z'),
1130
+ timeSpentOnPage: '45000', pageName: 'EMAIL_VERIFICATION',
1131
+ eventType: 'EmailVerification', ubid: this.ubid, visitorId: this.vid
1132
+ },
1133
+ cookies: {}
1134
+ }
1135
+ }, this.buildProfileHeaders(ref));
1136
+ const data = this.parseBody(resp.body);
1137
+ this.regCode = data.registrationCode || '';
1138
+ this.signState = data.signInState || '';
1139
+ if (!this.regCode)
1140
+ throw new Error(`create-identity không trả về registrationCode: ${resp.body.slice(0, 200)}`);
1141
+ }
1142
+ async step12SetPassword() {
1143
+ this.emitStep('set-password');
1144
+ this.log('[12] Đặt mật khẩu');
1145
+ const api = `${this.cfg.signinBase}/platform/${this.cfg.directoryId}/signup/api/execute`;
1146
+ const ref = `${this.cfg.signinBase}/platform/${this.cfg.directoryId}/signup?registrationCode=${this.regCode}&state=${this.signState}`;
1147
+ let fp = this.genFP('signup', 'PageSubmit', 0, '');
1148
+ // 12a: 获取加密公钥
1149
+ let rid = (0, http_utils_1.newUUID)();
1150
+ let h = this.buildHeaders(ref, this.cfg.signinBase);
1151
+ h['x-amzn-requestid'] = rid;
1152
+ h['x-amz-date'] = (0, http_utils_1.gmtDate)();
1153
+ h['priority'] = 'u=1, i';
1154
+ let resp = await this.doPost(api, {
1155
+ stepId: '', state: this.signState,
1156
+ inputs: [
1157
+ { input_type: 'UserRegistrationRequestInput', registrationCode: this.regCode, state: this.signState },
1158
+ { input_type: 'FingerPrintRequestInput', fingerPrint: fp }
1159
+ ],
1160
+ requestId: rid
1161
+ }, h);
1162
+ (0, http_utils_1.saveCookies)(this.cookies, resp.headers);
1163
+ let data = this.parseBody(resp.body);
1164
+ this.workflowHandle = data.workflowStateHandle || '';
1165
+ const encCtx = (0, http_utils_1.getNestedMap)(data, 'workflowResponseData', 'encryptionContextResponse');
1166
+ const pubKeyMap = encCtx ? (0, http_utils_1.getNestedStringMap)(encCtx, 'publicKey') : null;
1167
+ if (!pubKeyMap?.n)
1168
+ throw new Error(`Không lấy được khóa công khai mã hóa: ${this.formatErrorBody(resp.body, resp.status)}`);
1169
+ const issuer = encCtx?.issuer || 'signin';
1170
+ const audience = encCtx?.audience || 'AWSPasswordService';
1171
+ const region = encCtx?.region || 'us-east-1';
1172
+ const encrypted = (0, jwe_1.encryptPassword)(this.cfg.password, pubKeyMap, issuer, audience, region);
1173
+ // 12b: 提交密码
1174
+ fp = this.genFP('signup', 'PageSubmit', 0, '');
1175
+ rid = (0, http_utils_1.newUUID)();
1176
+ h = this.buildHeaders(ref, this.cfg.signinBase);
1177
+ h['x-amzn-requestid'] = rid;
1178
+ h['x-amz-date'] = (0, http_utils_1.gmtDate)();
1179
+ h['priority'] = 'u=1, i';
1180
+ resp = await this.doPost(api, {
1181
+ stepId: 'get-new-password-for-password-creation',
1182
+ workflowStateHandle: this.workflowHandle, actionId: 'SUBMIT',
1183
+ inputs: [
1184
+ { input_type: 'PasswordRequestInput', password: encrypted, successfullyEncrypted: 'SUCCESSFUL' },
1185
+ { input_type: 'UserRequestInput', username: this.email },
1186
+ { input_type: 'FingerPrintRequestInput', fingerPrint: fp }
1187
+ ],
1188
+ visitorId: this.vid, requestId: rid
1189
+ }, h);
1190
+ (0, http_utils_1.saveCookies)(this.cookies, resp.headers);
1191
+ data = this.parseBody(resp.body);
1192
+ const redir = data.redirect;
1193
+ const rurl = redir?.url;
1194
+ if (!rurl)
1195
+ throw new Error(`Bước đặt mật khẩu không trả về redirect: ${resp.body.slice(0, 200)}`);
1196
+ const wh = (0, http_utils_1.extractParam)(rurl, 'workflowStateHandle');
1197
+ const st = (0, http_utils_1.extractParam)(rurl, 'state');
1198
+ const rh = (0, http_utils_1.extractParam)(rurl, 'workflowResultHandle');
1199
+ await this.completeSignup(wh, st, rh);
1200
+ }
1201
+ async completeSignup(wh, state, rh) {
1202
+ this.log('[12.5] Hoàn tất quy trình đăng ký');
1203
+ const api = `${this.cfg.signinBase}/platform/${this.cfg.directoryId}/api/execute`;
1204
+ const ref = `${this.cfg.signinBase}/platform/${this.cfg.directoryId}/login?workflowStateHandle=${wh}&state=${state}&workflowResultHandle=${rh}`;
1205
+ const fp = this.genFP('signin', 'PageLoad', 0, '');
1206
+ const rid = (0, http_utils_1.newUUID)();
1207
+ const h = this.buildHeaders(ref, this.cfg.signinBase);
1208
+ h['x-amzn-requestid'] = rid;
1209
+ h['x-amz-date'] = (0, http_utils_1.gmtDate)();
1210
+ h['priority'] = 'u=1, i';
1211
+ const resp = await this.doPost(api, {
1212
+ stepId: '', workflowStateHandle: wh,
1213
+ workflowResultHandle: rh, state,
1214
+ inputs: [
1215
+ { input_type: 'UserRequestInput', username: this.email },
1216
+ { input_type: 'FingerPrintRequestInput', fingerPrint: fp }
1217
+ ],
1218
+ visitorId: this.vid, requestId: rid
1219
+ }, h);
1220
+ (0, http_utils_1.saveCookies)(this.cookies, resp.headers);
1221
+ const data = this.parseBody(resp.body);
1222
+ if (data.stepId !== 'end-of-workflow-success')
1223
+ throw new Error(`Hoàn tất quy trình thất bại: ${data.stepId || 'undefined'} ${this.formatErrorBody(resp.body, resp.status)}`);
1224
+ const redir = data.redirect;
1225
+ const rurl = redir?.url;
1226
+ if (rurl) {
1227
+ this.authCode = (0, http_utils_1.extractParam)(rurl, 'workflowResultHandle');
1228
+ this.ssoState = (0, http_utils_1.extractParam)(rurl, 'state');
1229
+ this.wdcCSRFToken = (0, http_utils_1.extractParam)(rurl, 'wdc_csrf_token');
1230
+ }
1231
+ }
1232
+ // ============ SSO 授权 (Step12.8-13) ============
1233
+ async step12_8SSOWorkflow() {
1234
+ this.emitStep('sso-workflow');
1235
+ this.log('[12.8] Quy trình SSO');
1236
+ const redirectURL = encodeURIComponent(this.cfg.viewBase + '/start/#/');
1237
+ const loginURL = `${this.cfg.portalBase}/login?directory_id=view&redirect_url=${redirectURL}`;
1238
+ const h = {
1239
+ 'Accept': '*/*', 'User-Agent': this.identity.ua,
1240
+ 'Origin': this.cfg.viewBase, 'Referer': this.cfg.viewBase + '/',
1241
+ 'sec-ch-ua': this.secUA, 'sec-ch-ua-mobile': '?0',
1242
+ 'sec-ch-ua-platform': '"Windows"', 'sec-fetch-dest': 'empty',
1243
+ 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'cross-site', 'priority': 'u=1, i'
1244
+ };
1245
+ if (this.cookies.has('awsccc'))
1246
+ h['Cookie'] = 'awsccc=' + this.cookies.get('awsccc');
1247
+ const resp = await this.doGet(loginURL, h);
1248
+ (0, http_utils_1.saveCookies)(this.cookies, resp.headers);
1249
+ const data = this.parseBody(resp.body);
1250
+ if (data.csrfToken)
1251
+ this.cookies.set('loginCsrfToken', data.csrfToken);
1252
+ const rurl = data.redirectUrl || '';
1253
+ let wh = '';
1254
+ if (rurl.includes('workflowStateHandle=')) {
1255
+ wh = (0, http_utils_1.splitAfter)(rurl, 'workflowStateHandle=');
1256
+ }
1257
+ if (!wh)
1258
+ throw new Error('SSO không lấy được workflowStateHandle');
1259
+ await this.completeSSOWorkflow(wh);
1260
+ }
1261
+ async completeSSOWorkflow(wh) {
1262
+ const api = `${this.cfg.signinBase}/platform/${this.cfg.directoryId}/api/execute`;
1263
+ const ref = `${this.cfg.signinBase}/platform/${this.cfg.directoryId}/login?workflowStateHandle=${wh}`;
1264
+ let fp = this.genFP('signin', 'PageLoad', 0, '');
1265
+ let rid = (0, http_utils_1.newUUID)();
1266
+ let h = this.buildHeaders(ref, this.cfg.signinBase);
1267
+ h['x-amzn-requestid'] = rid;
1268
+ h['x-amz-date'] = (0, http_utils_1.gmtDate)();
1269
+ h['priority'] = 'u=1, i';
1270
+ let resp = await this.doPost(api, {
1271
+ stepId: '', workflowStateHandle: wh,
1272
+ inputs: [{ input_type: 'FingerPrintRequestInput', fingerPrint: fp }],
1273
+ requestId: rid
1274
+ }, h);
1275
+ (0, http_utils_1.saveCookies)(this.cookies, resp.headers);
1276
+ let data = this.parseBody(resp.body);
1277
+ let newWH = data.workflowStateHandle || wh;
1278
+ if (data.stepId === 'start') {
1279
+ fp = this.genFP('signin', 'PageLoad', 0, '');
1280
+ rid = (0, http_utils_1.newUUID)();
1281
+ h = this.buildHeaders(ref, this.cfg.signinBase);
1282
+ h['x-amzn-requestid'] = rid;
1283
+ h['x-amz-date'] = (0, http_utils_1.gmtDate)();
1284
+ h['priority'] = 'u=1, i';
1285
+ resp = await this.doPost(api, {
1286
+ stepId: 'start', workflowStateHandle: newWH,
1287
+ inputs: [{ input_type: 'FingerPrintRequestInput', fingerPrint: fp }],
1288
+ requestId: rid
1289
+ }, h);
1290
+ (0, http_utils_1.saveCookies)(this.cookies, resp.headers);
1291
+ data = this.parseBody(resp.body);
1292
+ }
1293
+ if (data.stepId === 'end-of-workflow-success') {
1294
+ const redir = data.redirect;
1295
+ const rurl = redir?.url;
1296
+ if (rurl) {
1297
+ this.authCode = (0, http_utils_1.extractParam)(rurl, 'workflowResultHandle');
1298
+ this.ssoState = (0, http_utils_1.extractParam)(rurl, 'state');
1299
+ this.wdcCSRFToken = (0, http_utils_1.extractParam)(rurl, 'wdc_csrf_token');
1300
+ }
1301
+ }
1302
+ // 访问 start 页面
1303
+ const params = new URLSearchParams();
1304
+ if (this.ssoState)
1305
+ params.set('state', this.ssoState);
1306
+ params.set('workflowResultHandle', this.authCode);
1307
+ if (this.wdcCSRFToken)
1308
+ params.set('wdc_csrf_token', this.wdcCSRFToken);
1309
+ const startURL = this.cfg.viewBase + '/start/?' + params.toString();
1310
+ const cookieParts = [];
1311
+ if (this.cookies.has('loginCsrfToken'))
1312
+ cookieParts.push('loginCsrfToken=' + this.cookies.get('loginCsrfToken'));
1313
+ if (this.cookies.has('awsccc'))
1314
+ cookieParts.push('awsccc=' + this.cookies.get('awsccc'));
1315
+ await this.doGet(startURL, {
1316
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
1317
+ 'User-Agent': this.identity.ua,
1318
+ 'Referer': this.cfg.signinBase + '/',
1319
+ 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate',
1320
+ ...(cookieParts.length ? { Cookie: cookieParts.join('; ') } : {})
1321
+ });
1322
+ }
1323
+ async step13SSOToken() {
1324
+ this.emitStep('sso-token');
1325
+ this.log('[13] Lấy SSO Token');
1326
+ const csrf = this.cookies.get('loginCsrfToken');
1327
+ if (!csrf)
1328
+ throw new Error('Thiếu loginCsrfToken');
1329
+ const h = {
1330
+ 'Accept': 'application/json, text/plain, */*',
1331
+ 'Content-Type': 'application/x-www-form-urlencoded',
1332
+ 'User-Agent': this.identity.ua, 'Origin': this.cfg.viewBase,
1333
+ 'Referer': this.cfg.viewBase + '/',
1334
+ 'x-amz-sso-csrf-token': csrf,
1335
+ 'sec-ch-ua': this.secUA, 'sec-ch-ua-mobile': '?0',
1336
+ 'sec-ch-ua-platform': '"Windows"', 'sec-fetch-dest': 'empty',
1337
+ 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'cross-site', 'priority': 'u=1, i'
1338
+ };
1339
+ const formData = `authCode=${encodeURIComponent(this.authCode)}&state=${encodeURIComponent(this.ssoState)}&orgId=view`;
1340
+ // 使用新客户端轮询 SSO Token
1341
+ const ssoSession = new tlsclientwrapper_1.SessionClient(this.moduleClient, this.sessionOpts);
1342
+ try {
1343
+ for (let retry = 0; retry < 5; retry++) {
1344
+ const resp = await ssoSession.post(this.cfg.portalBase + '/auth/sso-token', formData, { headers: h });
1345
+ const data = JSON.parse(resp.body || '{}');
1346
+ if (data.token) {
1347
+ this.ssoToken = data.token;
1348
+ break;
1349
+ }
1350
+ const errMsg = (data.errorMessage || '');
1351
+ if (errMsg.toLowerCase().includes('not authorized')) {
1352
+ await this.abortableSleep(3000);
1353
+ continue;
1354
+ }
1355
+ throw new Error(`Lấy SSO Token thất bại: ${resp.body?.slice(0, 200)}`);
1356
+ }
1357
+ }
1358
+ finally {
1359
+ try {
1360
+ await ssoSession.destroySession();
1361
+ }
1362
+ catch { /* ignore */ }
1363
+ }
1364
+ if (!this.ssoToken)
1365
+ throw new Error('Lấy SSO Token vẫn thất bại sau 5 lần thử');
1366
+ // Accept device + Associate token
1367
+ let resp = await this.doPost(this.cfg.oidcBase + '/device_authorization/accept_user_code', {
1368
+ userCode: this.userCode, userSessionId: this.ssoToken
1369
+ }, { 'Content-Type': 'application/json' });
1370
+ const dcData = this.parseBody(resp.body);
1371
+ const dc = dcData.deviceContext;
1372
+ await this.doPost(this.cfg.oidcBase + '/device_authorization/associate_token', {
1373
+ deviceContext: dc, userSessionId: this.ssoToken
1374
+ }, { 'Content-Type': 'application/json' });
1375
+ // 轮询 token
1376
+ for (let i = 0; i < 30; i++) {
1377
+ resp = await this.doPost(this.cfg.oidcBase + '/token', {
1378
+ clientId: this.clientId, clientSecret: this.clientSecret,
1379
+ deviceCode: this.deviceCode,
1380
+ grantType: 'urn:ietf:params:oauth:grant-type:device_code'
1381
+ }, { 'Content-Type': 'application/json' });
1382
+ if (resp.status === 200)
1383
+ return this.parseBody(resp.body);
1384
+ await this.abortableSleep(2000);
1385
+ }
1386
+ throw new Error('Hết thời gian chờ lấy Token');
1387
+ }
1388
+ // ============ 验活 ============
1389
+ async verifyAlive(awsToken) {
1390
+ this.log('[Kiểm tra] Làm mới Token và đọc hạn mức');
1391
+ const refreshToken = awsToken.refreshToken || '';
1392
+ const resp = await this.doPost('https://oidc.us-east-1.amazonaws.com/token', {
1393
+ clientId: this.clientId, clientSecret: this.clientSecret,
1394
+ refreshToken, grantType: 'refresh_token'
1395
+ }, { 'Content-Type': 'application/json' });
1396
+ if (resp.status !== 200) {
1397
+ this.log(`Làm mới Token thất bại: ${resp.status}`);
1398
+ return { alive: false, error: `refresh failed: ${resp.status}` };
1399
+ }
1400
+ const tok = this.parseBody(resp.body);
1401
+ const access = tok.accessToken || '';
1402
+ if (access) {
1403
+ awsToken.accessToken = access;
1404
+ }
1405
+ const usageUA = 'aws-sdk-js/1.0.18 ua/2.1 os/windows lang/js md/nodejs#20.16.0 api/codewhispererstreaming#1.0.18 m/E KiroIDE-0.6.18';
1406
+ const usageErrors = [];
1407
+ for (const baseURL of ['https://q.us-east-1.amazonaws.com/getUsageLimits', 'https://q.eu-central-1.amazonaws.com/getUsageLimits']) {
1408
+ const usageURL = baseURL + '?origin=AI_EDITOR&resourceType=AGENTIC_REQUEST&isEmailRequired=true';
1409
+ const usageResp = await this.doGet(usageURL, {
1410
+ 'Accept': 'application/json',
1411
+ 'Authorization': 'Bearer ' + access,
1412
+ 'User-Agent': usageUA
1413
+ });
1414
+ if (usageResp.status === 403 && usageResp.body.toLowerCase().includes('suspended')) {
1415
+ return { alive: false, suspended: true, error: 'suspended' };
1416
+ }
1417
+ if (usageResp.status === 200) {
1418
+ return this.parseUsage(usageResp.body);
1419
+ }
1420
+ if (usageResp.status === 401 || usageResp.status === 403) {
1421
+ const err = `${baseURL} -> ${usageResp.status}: ${usageResp.body.slice(0, 200)}`;
1422
+ usageErrors.push(err);
1423
+ this.log(`[Kiểm tra] ${err}`);
1424
+ }
1425
+ else {
1426
+ usageErrors.push(`${baseURL} -> ${usageResp.status}`);
1427
+ }
1428
+ }
1429
+ return { alive: false, error: `usage query failed${usageErrors.length ? ` (${usageErrors.join(' | ')})` : ''}` };
1430
+ }
1431
+ parseUsage(body) {
1432
+ const usage = this.parseBody(body);
1433
+ const userInfo = usage.userInfo || {};
1434
+ const emailAddr = userInfo.email || '';
1435
+ const subInfo = usage.subscriptionInfo || {};
1436
+ let sub = subInfo.subscriptionTitle || 'Free';
1437
+ let totalLimit = 0, totalUsed = 0;
1438
+ const breakdown = usage.usageBreakdownList;
1439
+ if (breakdown) {
1440
+ for (const item of breakdown) {
1441
+ const rt = item.resourceType;
1442
+ const dn = item.displayName;
1443
+ if (rt === 'CREDIT' || dn === 'Credits') {
1444
+ totalLimit = item.usageLimitWithPrecision || item.usageLimit || 0;
1445
+ totalUsed = item.currentUsageWithPrecision || item.currentUsage || 0;
1446
+ const ft = item.freeTrialInfo;
1447
+ if (ft?.freeTrialStatus === 'ACTIVE') {
1448
+ totalLimit += ft.usageLimitWithPrecision || 0;
1449
+ totalUsed += ft.currentUsageWithPrecision || 0;
1450
+ }
1451
+ break;
1452
+ }
1453
+ }
1454
+ }
1455
+ this.log(`Kiểm tra thành công! Email=${emailAddr} Gói=${sub} Credit=${totalUsed}/${totalLimit}`);
1456
+ return { alive: true, email: emailAddr, subscription: sub, credit_used: totalUsed, credit_limit: totalLimit };
1457
+ }
1458
+ // ============ 主流程 ============
1459
+ /** 执行完整注册流程(自动模式) */
1460
+ async run() {
1461
+ this.emitStep('init');
1462
+ try {
1463
+ await this.setupProxyChain();
1464
+ if (this.chainRelay)
1465
+ this.emitStep('proxy-chain-ready');
1466
+ await this.initTlsClient();
1467
+ this.emitStep('tls-ready');
1468
+ await this.detectExitIP();
1469
+ await (0, xxtea_1.refreshAppJSConfig)((url, init) => this.fetchAppJS(url, init));
1470
+ await this.rebuildTlsClient();
1471
+ // 幂等只读步骤:retry 次数 + 整体超时看门狗 + session refresh。
1472
+ // OIDC 为首步(失败即废号)保留自带 3 次重试不快速超时;Email 创建有副作用不重试。
1473
+ const initSteps = [
1474
+ { name: 'OIDC', fn: () => this.step1OIDC() },
1475
+ { name: 'Device', fn: () => this.step2Device(), retry: 2, timeoutMs: 30000, refreshSession: true },
1476
+ { name: 'Email', fn: () => this.step3Email() },
1477
+ { name: 'Portal', fn: () => this.step4Portal(), retry: 3, timeoutMs: 35000, refreshSession: true },
1478
+ { name: 'WorkflowInit', fn: () => this.step5WorkflowInit(), retry: 2, timeoutMs: 35000, refreshSession: true }
1479
+ ];
1480
+ for (const s of initSteps) {
1481
+ this.checkAborted();
1482
+ try {
1483
+ if (s.retry)
1484
+ await this.retryStep(s.name, s.fn, s.retry, { timeoutMs: s.timeoutMs, refreshSession: s.refreshSession });
1485
+ else
1486
+ await s.fn();
1487
+ }
1488
+ catch (err) {
1489
+ return { status: 'failed', email: this.email, error: `[${s.name}] ${err.message}` };
1490
+ }
1491
+ await this.humanDelay();
1492
+ }
1493
+ this.checkAborted();
1494
+ // 非幂等步骤统一加整体超时看门狗(默认 55s):卡住时快速失败释放并发槽,不死等 3×25s
1495
+ const STEP_TIMEOUT = 55000;
1496
+ const emailStatus = await this.withTimeout(this.step6SubmitEmail(), STEP_TIMEOUT, 'SubmitEmail');
1497
+ if (emailStatus === 'signup') {
1498
+ const signupSteps = [
1499
+ { name: 'Signup', fn: () => this.step7Signup() },
1500
+ { name: 'SignupInit', fn: () => this.step7_5SignupInit() },
1501
+ { name: 'ProfileInit', fn: () => this.step7_8ProfileInit() },
1502
+ { name: 'ProfileStart', fn: () => this.step8ProfileStart() },
1503
+ { name: 'SendOTP', fn: () => this.step9SendOTP() }
1504
+ ];
1505
+ for (const s of signupSteps) {
1506
+ this.checkAborted();
1507
+ try {
1508
+ await this.withTimeout(s.fn(), STEP_TIMEOUT, s.name);
1509
+ }
1510
+ catch (err) {
1511
+ return { status: 'failed', email: this.email, error: `[${s.name}] ${err.message}` };
1512
+ }
1513
+ await this.humanDelay();
1514
+ }
1515
+ this.checkAborted();
1516
+ let otp;
1517
+ try {
1518
+ otp = await this.step10GetOTP();
1519
+ }
1520
+ catch (err) {
1521
+ return { status: 'failed', email: this.email, error: `[GetOTP] ${err.message}` };
1522
+ }
1523
+ for (const s of [
1524
+ { name: 'CreateIdentity', fn: () => this.step11CreateIdentity(otp) },
1525
+ { name: 'SetPassword', fn: () => this.step12SetPassword() }
1526
+ ]) {
1527
+ this.checkAborted();
1528
+ try {
1529
+ await this.withTimeout(s.fn(), STEP_TIMEOUT, s.name);
1530
+ }
1531
+ catch (err) {
1532
+ return { status: 'failed', email: this.email, error: `[${s.name}] ${err.message}` };
1533
+ }
1534
+ await this.humanDelay();
1535
+ }
1536
+ }
1537
+ else {
1538
+ return { status: 'failed', email: this.email, error: 'Email này đã được đăng ký' };
1539
+ }
1540
+ // ====== 后期步骤(SSO + Token)======
1541
+ // 到这里账号已创建(Step 11-12 成功),后续只是获取登录凭证。
1542
+ // 如果因网络波动失败,在同一个 Registrar 内重试(复用已有注册状态),
1543
+ // 避免让外层从头开始白白浪费已完成的注册流程。
1544
+ this.checkAborted();
1545
+ let awsToken = null;
1546
+ const SSO_MAX_RETRIES = 2;
1547
+ for (let ssoAttempt = 0; ssoAttempt <= SSO_MAX_RETRIES; ssoAttempt++) {
1548
+ try {
1549
+ // SSO 含 token 轮询,单次尝试加整体超时(卡死时切断进入下一次重试)
1550
+ await this.withTimeout(this.step12_8SSOWorkflow(), 60000, 'SSOWorkflow');
1551
+ await this.abortableSleep(2000);
1552
+ this.checkAborted();
1553
+ awsToken = await this.withTimeout(this.step13SSOToken(), 90000, 'SSOToken');
1554
+ break; // SSO 成功
1555
+ }
1556
+ catch (err) {
1557
+ const errMsg = err.message;
1558
+ if (ssoAttempt < SSO_MAX_RETRIES) {
1559
+ this.log(`[SSO] Bước cuối thất bại, thử lại nội bộ (${ssoAttempt + 1}/${SSO_MAX_RETRIES}): ${errMsg}`);
1560
+ await this.abortableSleep(3000 + Math.floor(Math.random() * 2000));
1561
+ }
1562
+ else {
1563
+ // 最终失败:账号已创建但拿不到 Token
1564
+ return { status: 'failed', email: this.email, error: `[SSOToken] ${errMsg} (tài khoản đã được tạo, có thể nhập thủ công để làm mới)` };
1565
+ }
1566
+ }
1567
+ }
1568
+ const token = awsToken;
1569
+ this.emitStep('verify-alive');
1570
+ const verify = await this.withTimeout(this.verifyAlive(token), 60000, 'VerifyAlive');
1571
+ if (verify.suspended) {
1572
+ return { status: 'failed', email: this.email, error: 'suspended' };
1573
+ }
1574
+ this.emitStep('done');
1575
+ return {
1576
+ status: 'success',
1577
+ email: this.email,
1578
+ password: this.cfg.password,
1579
+ clientId: this.clientId,
1580
+ clientSecret: this.clientSecret,
1581
+ refreshToken: token.refreshToken || '',
1582
+ accessToken: token.accessToken || '',
1583
+ region: 'us-east-1',
1584
+ provider: 'BuilderId',
1585
+ verify,
1586
+ fingerprint: this.fingerprintSnapshot()
1587
+ };
1588
+ }
1589
+ finally {
1590
+ await this.cleanup();
1591
+ }
1592
+ }
1593
+ /**
1594
+ * 返回本次注册实际生效的代理 URL(按 sessionOpts 同样的优先级解析),
1595
+ * 用于在指纹摘要里准确显示是直连还是走代理。
1596
+ */
1597
+ resolvedProxyUrl() {
1598
+ // 代理链启用时 cfg.proxy 是本地中继地址,审计应显示真正的目标代理
1599
+ return (this.chainTargetProxy && this.chainTargetProxy.trim())
1600
+ || (this.cfg.proxy && this.cfg.proxy.trim())
1601
+ || process.env.HTTPS_PROXY || process.env.https_proxy
1602
+ || process.env.HTTP_PROXY || process.env.http_proxy
1603
+ || (0, systemProxy_1.getSystemProxy)() || undefined;
1604
+ }
1605
+ /** 输出本次注册使用的指纹摘要(用于审计与后续复用) */
1606
+ fingerprintSnapshot() {
1607
+ const resolved = this.resolvedProxyUrl();
1608
+ return {
1609
+ chromeVer: this.identity.chromeVer,
1610
+ ua: this.identity.ua,
1611
+ gpuVendor: this.identity.gpuVendor,
1612
+ gpuModel: this.identity.gpuModel,
1613
+ canvasHash: this.identity.canvasHash,
1614
+ screen: { width: this.identity.screen.width, height: this.identity.screen.height },
1615
+ // 脱敏后保存(隐藏密码部分),同时确保系统/环境变量代理也被捕获
1616
+ proxyUrl: resolved ? resolved.replace(/:([^:@/]+)@/, ':***@') : undefined,
1617
+ exitIP: this.exitIP || undefined
1618
+ };
1619
+ }
1620
+ /** 手动模式注册 - Step1-2 自动,Step3 等待外部设置邮箱,Step4-9 自动,Step10 等待外部 OTP */
1621
+ async runManualPhase1() {
1622
+ try {
1623
+ await this.setupProxyChain();
1624
+ await this.initTlsClient();
1625
+ await this.detectExitIP();
1626
+ await (0, xxtea_1.refreshAppJSConfig)((url, init) => this.fetchAppJS(url, init));
1627
+ await this.rebuildTlsClient();
1628
+ await this.step1OIDC();
1629
+ await this.withTimeout(this.step2Device(), 30000, 'Device');
1630
+ return { success: true };
1631
+ }
1632
+ catch (err) {
1633
+ return { success: false, error: err.message };
1634
+ }
1635
+ }
1636
+ /** 手动模式 - 设置邮箱后继续注册流程到发送 OTP */
1637
+ async runManualPhase2(email, fullName) {
1638
+ this.email = email;
1639
+ if (fullName)
1640
+ this.cfg.fullName = fullName;
1641
+ try {
1642
+ // 幂等只读步骤:retry + 超时看门狗 + session refresh;后续非幂等步骤仅加超时快速失败
1643
+ const STEP_TIMEOUT = 55000;
1644
+ await this.retryStep('Portal', () => this.step4Portal(), 3, { timeoutMs: 35000, refreshSession: true });
1645
+ await this.retryStep('WorkflowInit', () => this.step5WorkflowInit(), 2, { timeoutMs: 35000, refreshSession: true });
1646
+ const status = await this.withTimeout(this.step6SubmitEmail(), STEP_TIMEOUT, 'SubmitEmail');
1647
+ if (status !== 'signup')
1648
+ return { success: false, error: 'Email này đã được đăng ký' };
1649
+ await this.withTimeout(this.step7Signup(), STEP_TIMEOUT, 'Signup');
1650
+ await this.withTimeout(this.step7_5SignupInit(), STEP_TIMEOUT, 'SignupInit');
1651
+ await this.withTimeout(this.step7_8ProfileInit(), STEP_TIMEOUT, 'ProfileInit');
1652
+ await this.withTimeout(this.step8ProfileStart(), STEP_TIMEOUT, 'ProfileStart');
1653
+ await this.withTimeout(this.step9SendOTP(), STEP_TIMEOUT, 'SendOTP');
1654
+ return { success: true };
1655
+ }
1656
+ catch (err) {
1657
+ return { success: false, error: err.message };
1658
+ }
1659
+ }
1660
+ /** 手动模式 - 输入 OTP 后完成注册 */
1661
+ async runManualPhase3(otp) {
1662
+ try {
1663
+ // 非幂等步骤加整体超时看门狗,卡住时快速失败
1664
+ await this.withTimeout(this.step11CreateIdentity(otp), 55000, 'CreateIdentity');
1665
+ await this.withTimeout(this.step12SetPassword(), 55000, 'SetPassword');
1666
+ // SSO + Token:账号已创建,网络波动时在同一 Registrar 内重试(复用已有注册状态),避免白费已完成的注册
1667
+ let awsToken = null;
1668
+ const SSO_MAX_RETRIES = 2;
1669
+ for (let ssoAttempt = 0; ssoAttempt <= SSO_MAX_RETRIES; ssoAttempt++) {
1670
+ try {
1671
+ await this.withTimeout(this.step12_8SSOWorkflow(), 60000, 'SSOWorkflow');
1672
+ await this.abortableSleep(2000);
1673
+ this.checkAborted();
1674
+ awsToken = await this.withTimeout(this.step13SSOToken(), 90000, 'SSOToken');
1675
+ break;
1676
+ }
1677
+ catch (err) {
1678
+ const errMsg = err.message;
1679
+ if (ssoAttempt < SSO_MAX_RETRIES) {
1680
+ this.log(`[SSO] Bước cuối thất bại, thử lại nội bộ (${ssoAttempt + 1}/${SSO_MAX_RETRIES}): ${errMsg}`);
1681
+ await this.abortableSleep(3000 + Math.floor(Math.random() * 2000));
1682
+ }
1683
+ else {
1684
+ return { status: 'failed', email: this.email, error: `[SSOToken] ${errMsg} (tài khoản đã được tạo, có thể nhập thủ công để làm mới)` };
1685
+ }
1686
+ }
1687
+ }
1688
+ const token = awsToken;
1689
+ const verify = await this.withTimeout(this.verifyAlive(token), 60000, 'VerifyAlive');
1690
+ if (verify.suspended) {
1691
+ return { status: 'failed', email: this.email, error: 'suspended' };
1692
+ }
1693
+ return {
1694
+ status: 'success',
1695
+ email: this.email,
1696
+ password: this.cfg.password,
1697
+ clientId: this.clientId,
1698
+ clientSecret: this.clientSecret,
1699
+ refreshToken: token.refreshToken || '',
1700
+ accessToken: token.accessToken || '',
1701
+ region: 'us-east-1',
1702
+ provider: 'BuilderId',
1703
+ verify,
1704
+ fingerprint: this.fingerprintSnapshot()
1705
+ };
1706
+ }
1707
+ catch (err) {
1708
+ return { status: 'failed', email: this.email, error: err.message };
1709
+ }
1710
+ finally {
1711
+ await this.cleanup();
1712
+ }
1713
+ }
1714
+ }
1715
+ exports.Registrar = Registrar;