Files
ark-stronghold-protocol/public/js/skland/api.js
T
herryyun 7c25c89992
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
实现账号绑定、持久登录与跨设备对局恢复
2026-10-14 23:22:50 +08:00

71 lines
3.3 KiB
JavaScript

// 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);
}