实现账号绑定、持久登录与跨设备对局恢复
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
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
This commit is contained in:
1 parent
488db49f11
commit
7c25c89992
62 files changed
+6855
-1005
No files matched your search
@@ -0,0 +1,78 @@
|
||||
// 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; }
|
||||
}
|
||||
Reference in new issue
Block a user