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
79 lines
3.5 KiB
JavaScript
79 lines
3.5 KiB
JavaScript
// Fork-local entry policy: saved callsigns/entered flags are hints, never proof of a role binding.
|
|
import { net, identity } from '../net.js';
|
|
import { store } from '../store.js';
|
|
import { BindingError, createBindingApi, ensureBindingSession, preferredRole } from './api.js';
|
|
|
|
export function selectedBoundRole(state) {
|
|
if (!state?.bound) return null;
|
|
let selected = '';
|
|
try { selected = sessionStorage.getItem('sp.binding.role.' + state.playerId) || ''; } catch { /* optional preference */ }
|
|
return preferredRole(state.roles, state.selectedRoleKey || selected);
|
|
}
|
|
|
|
/** Losing the authenticated game transport must never leave an anonymous socket inside the game shell. */
|
|
export function acceptAccountWelcome(message) {
|
|
if (typeof message.playerId === 'string' && message.playerId.startsWith('a_')) return true;
|
|
identity.setEntered(false);
|
|
store.patch('session', { entered: false });
|
|
return false;
|
|
}
|
|
|
|
/** A title lookup restores the login only. Changing roles releases an old, roomless game transport. */
|
|
export async function syncAccountSession(state) {
|
|
const current = store.get().me.playerId;
|
|
if (current?.startsWith('a_') && current !== state?.gamePlayerId) await resetAccountSession();
|
|
}
|
|
|
|
export async function resetAccountSession() {
|
|
identity.clearToken(); identity.setEntered(false);
|
|
store.patch('session', { entered: false });
|
|
net.reconnectNow('Doctor');
|
|
await ensureBindingSession({ net, identity, name: 'Doctor' });
|
|
}
|
|
|
|
export async function accountRecovery({ signal } = {}) {
|
|
await ensureBindingSession({ net, identity, name: identity.loadName() || 'Doctor' });
|
|
const state = await createBindingApi({ token: () => identity.getToken(), signal })('recovery');
|
|
if (!state.ok) throw new BindingError('response');
|
|
return state.recovery;
|
|
}
|
|
|
|
/** Explicit entry/recovery: ask for the selected role's token, then verify the WebSocket actually attached it. */
|
|
export async function enterBoundAccount(normalizeName, { recoveryId, automatic = false } = {}) {
|
|
identity.setEntered(false);
|
|
store.patch('session', { entered: false });
|
|
await ensureBindingSession({ net, identity, name: identity.loadName() || 'Doctor' });
|
|
const api = createBindingApi({ token: () => identity.getToken() });
|
|
const state = await api('status');
|
|
const name = normalizeName(selectedBoundRole(state)?.nickName || '');
|
|
if (!name || !state.gamePlayerId) throw new BindingError('session');
|
|
if (automatic) {
|
|
// A copied/stale local token must not silently take the role back from the device that now owns it.
|
|
if (net.playerId !== state.gamePlayerId) return false;
|
|
} else {
|
|
const entry = await api(recoveryId ? 'resume' : 'enter', recoveryId ? { id: recoveryId } : {});
|
|
if (!entry.ok || entry.gamePlayerId !== state.gamePlayerId || typeof entry.token !== 'string') throw new BindingError('response');
|
|
net.close();
|
|
if (!identity.stageToken(entry.token)) throw new BindingError('response');
|
|
try {
|
|
net.reconnectNow(name);
|
|
await ensureBindingSession({ net, identity, name });
|
|
if (net.playerId !== state.gamePlayerId) throw new BindingError('session');
|
|
} catch (error) {
|
|
net.close(); identity.clearToken();
|
|
throw error;
|
|
}
|
|
}
|
|
identity.saveName(name);
|
|
store.patch('me', { name });
|
|
net.setName(name);
|
|
identity.setEntered(true);
|
|
store.patch('session', { entered: true });
|
|
return true;
|
|
}
|
|
|
|
/** Boot/Start keep the title visible when the server cannot verify the selected role. */
|
|
export async function restoreBoundEntry(normalizeName, options) {
|
|
try { return await enterBoundAccount(normalizeName, options); } catch { return false; }
|
|
}
|