实现账号绑定、持久登录与跨设备对局恢复
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,70 @@
|
||||
// Independent title adapter: the game reconnect token stays in a header, never a URL, QR code or provider request.
|
||||
export class BindingError extends Error {
|
||||
constructor(code, message = code) { super(message); this.code = code; }
|
||||
}
|
||||
|
||||
export function createBindingApi({ token, fetchImpl = globalThis.fetch, signal } = {}) {
|
||||
return async (path, body) => {
|
||||
const credential = typeof token === 'function' ? token() : token;
|
||||
if (!credential) throw new BindingError('session');
|
||||
let response;
|
||||
try {
|
||||
response = await fetchImpl(`/skland/session/${path}`, {
|
||||
method: body === undefined ? 'GET' : 'POST', cache: 'no-store', signal,
|
||||
credentials: 'same-origin',
|
||||
headers: { authorization: `Bearer ${credential}`, ...(body === undefined ? {} : { 'content-type': 'application/json' }) },
|
||||
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
||||
});
|
||||
} catch (error) {
|
||||
if (signal?.aborted) throw error;
|
||||
throw new BindingError('network');
|
||||
}
|
||||
if (response.status === 404) throw new BindingError('off');
|
||||
if (response.status === 401) throw new BindingError('session');
|
||||
let result;
|
||||
try { result = await response.json(); } catch { throw new BindingError('response'); }
|
||||
if (!response.ok) throw new BindingError(result.code === 'resume_expired' ? 'resume_expired' : 'upstream', result.error);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
/** Establish hello without pressing Start or changing the router's entered flag. */
|
||||
export async function ensureBindingSession({ net, identity, name, timeoutMs = 10_000 }) {
|
||||
await identity.init();
|
||||
if (net.status === 'online' && identity.getToken()) return identity.getToken();
|
||||
return new Promise((resolve, reject) => {
|
||||
const clean = () => { clearTimeout(timer); offWelcome(); offError(); };
|
||||
const offWelcome = net.on('welcome', (message) => { clean(); resolve(message.token); });
|
||||
const offError = net.on('helloError', () => { clean(); reject(new BindingError('session')); });
|
||||
const timer = setTimeout(() => { clean(); reject(new BindingError('network')); }, timeoutMs);
|
||||
net.setName(name);
|
||||
net.connect();
|
||||
});
|
||||
}
|
||||
|
||||
export { roleKey } from '../../../shared/accountIdentity.js';
|
||||
import { roleKey, hasRoleIdentity } from '../../../shared/accountIdentity.js';
|
||||
|
||||
export function preferredRole(roles, selected) {
|
||||
const named = roles?.filter((role) => hasRoleIdentity(role) && typeof role?.nickName === 'string' && role.nickName.trim()) || [];
|
||||
return named.find((role) => roleKey(role) === selected) || named.find((role) => role.isDefault) || named[0] || null;
|
||||
}
|
||||
|
||||
let qrLoader;
|
||||
export async function qrDataUrl(value) {
|
||||
if (typeof globalThis.qrcode !== 'function') {
|
||||
qrLoader ||= new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
const fail = () => { clearTimeout(timer); script.remove(); qrLoader = null; reject(new BindingError('qr')); };
|
||||
const timer = setTimeout(fail, 10_000);
|
||||
script.src = '/vendor/qrcode.js';
|
||||
script.onload = () => { clearTimeout(timer); if (typeof globalThis.qrcode === 'function') resolve(); else fail(); };
|
||||
script.onerror = fail;
|
||||
document.head.append(script);
|
||||
});
|
||||
await qrLoader;
|
||||
}
|
||||
const qr = globalThis.qrcode(0, 'M');
|
||||
qr.addData(value); qr.make();
|
||||
return qr.createDataURL(6, 4);
|
||||
}
|
||||
Reference in new issue
Block a user