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