实现账号绑定、持久登录与跨设备对局恢复
CI / test (ubuntu-latest, node 22) (push) Failing after 33m10s
CI / test (ubuntu-latest, node 24) (push) Failing after 34m41s
CI / tooling (lint, imports, typecheck) (push) Failing after 36m11s
CI / test (windows-latest, node 22) (push) Canceled after 0s
CI / test (windows-latest, node 24) (push) Canceled after 0s
CI / test (ubuntu-latest, node 22) (push) Failing after 33m10s
CI / test (ubuntu-latest, node 24) (push) Failing after 34m41s
CI / tooling (lint, imports, typecheck) (push) Failing after 36m11s
CI / test (windows-latest, node 22) (push) Canceled after 0s
CI / test (windows-latest, node 24) (push) Canceled after 0s
This commit is contained in:
1 parent
488db49f11
commit
7c25c89992
62 files changed
+6855
-1005
No files matched your search
@@ -0,0 +1,69 @@
|
||||
// Interactive Geetest hand-off, following the official HG account SDK. The user completes the provider's widget;
|
||||
// only its proof is returned to the original login request. No phone, password or account token is sent here.
|
||||
const scripts = new Map();
|
||||
const PROVIDERS = {
|
||||
v4: { src: 'https://static.geetest.com/v4/gt4.js', init: 'initGeetest4' },
|
||||
v3: { src: 'https://static.geetest.com/static/tools/gt.js', init: 'initGeetest' },
|
||||
};
|
||||
|
||||
async function loadProvider(version) {
|
||||
const provider = PROVIDERS[version];
|
||||
if (typeof globalThis[provider.init] === 'function') return;
|
||||
if (!scripts.has(version)) {
|
||||
const pending = new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
const fail = () => { clearTimeout(timer); script.remove(); reject(new Error('captcha_load')); };
|
||||
const timer = setTimeout(fail, 20_000);
|
||||
script.src = provider.src;
|
||||
script.async = true;
|
||||
script.onerror = fail;
|
||||
script.onload = () => {
|
||||
clearTimeout(timer);
|
||||
if (typeof globalThis[provider.init] === 'function') resolve();
|
||||
else fail();
|
||||
};
|
||||
document.head.append(script);
|
||||
});
|
||||
scripts.set(version, pending);
|
||||
pending.catch(() => scripts.delete(version));
|
||||
}
|
||||
await scripts.get(version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called only by the user's captcha button. `scope` and `load` allow isolated widget contract tests without a
|
||||
* network call or a real challenge. Resolves null on user cancellation; never completes a real puzzle itself.
|
||||
*/
|
||||
export async function solveCaptcha(challenge, { scope = globalThis, load = loadProvider } = {}) {
|
||||
const version = challenge.geetestId ? 'v4' : challenge.gt && challenge.challenge ? 'v3' : null;
|
||||
if (!version) throw new Error('captcha_type');
|
||||
await load(version);
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
const init = scope[PROVIDERS[version].init];
|
||||
const options = version === 'v4'
|
||||
? { captchaId: challenge.geetestId, riskType: challenge.riskType, product: 'bind', language: 'zho' }
|
||||
: { gt: challenge.gt, challenge: challenge.challenge, new_captcha: challenge.new_captcha,
|
||||
offline: false, product: 'bind', https: true, lang: 'zh-cn' };
|
||||
init(options, (widget) => {
|
||||
const finish = (proof, error) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
widget.destroy?.();
|
||||
if (error) reject(error); else resolve(proof);
|
||||
};
|
||||
widget.onSuccess(() => {
|
||||
const proof = widget.getValidate();
|
||||
if (!proof) { finish(null, new Error('captcha_validate')); return; }
|
||||
finish(version === 'v4' ? { ...proof, challenge: challenge.challenge } : proof);
|
||||
});
|
||||
widget.onError((error) => {
|
||||
if (error?.error_code !== 'error_01') finish(null, new Error('captcha_validate'));
|
||||
});
|
||||
widget.onClose(() => finish(null));
|
||||
widget.onReady(() => {
|
||||
if (version === 'v4') widget.showCaptcha(); else widget.verify();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user