Files
ark-stronghold-protocol/public/js/skland/gate.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

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