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
71 lines
3.3 KiB
JavaScript
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);
|
|
}
|