357ac45281
- 登录门:非 signed_in 全屏盖 app,登录/注册/找回三页,只盖 UI
不断机器通道(U-1);status 通道异常落登录页不砖(U-3)
- U-5:登录成功当场校验本机 installation ∈ 会话用户列表,不符即
销毁会话并提示绑定者邮箱;绑定状态损坏 fail-closed
- relay 客户端补注册/找回四通道 + installations/binding-owner 查询;
IPC 预期失败全部结构化 {ok:false, code} 返回
- i18n userAccount 48 键 × 6 语言;electron +11 / ui +7 测试全绿
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
67 lines
1.8 KiB
TypeScript
67 lines
1.8 KiB
TypeScript
/**
|
||
* machine-binding.ts — 本机机器绑定(mercury-relay 插件 state.json)的只读视图。
|
||
*
|
||
* U-5 机器-用户锁的事实来源:插件绑定后把 ``relay_binding`` 写进
|
||
* ``<HERMES_HOME|~/.hermes>/mercury-relay/state.json``。desktop 只取
|
||
* ``site`` 与 ``agent_id``(= relay installation id)两个字段做所有权
|
||
* 校验;``host_refresh_token`` 等凭据字段读完即弃,绝不落日志。
|
||
*
|
||
* 无 electron import:状态文件原文由 main.ts 注入。文件缺失 = 未绑定
|
||
* (返回 null);文件存在但损坏 = fail-closed 抛 MachineBindingError
|
||
* (锁态无法判定时不能默许放行)。
|
||
*/
|
||
|
||
import { canonicalizeRelaySite } from './relay-account'
|
||
|
||
export interface MachineBinding {
|
||
site: string
|
||
installationId: string
|
||
}
|
||
|
||
export class MachineBindingError extends Error {
|
||
readonly code = 'binding_state_invalid'
|
||
|
||
constructor() {
|
||
super('binding_state_invalid')
|
||
}
|
||
}
|
||
|
||
export function parseMachineBinding(stateText: string): MachineBinding | null {
|
||
let parsed: any
|
||
|
||
try {
|
||
parsed = JSON.parse(stateText)
|
||
} catch {
|
||
throw new MachineBindingError()
|
||
}
|
||
|
||
const record = parsed?.relay_binding
|
||
|
||
if (record === null || record === undefined) {return null}
|
||
|
||
try {
|
||
const site = canonicalizeRelaySite(record.site)
|
||
const installationId = typeof record.agent_id === 'string' ? record.agent_id : ''
|
||
|
||
if (!installationId) {throw new MachineBindingError()}
|
||
|
||
return { site, installationId }
|
||
} catch (error) {
|
||
if (error instanceof MachineBindingError) {throw error}
|
||
throw new MachineBindingError()
|
||
}
|
||
}
|
||
|
||
/** 读状态文件;ENOENT 视为未绑定。 */
|
||
export function loadMachineBinding(readStateText: () => string): MachineBinding | null {
|
||
let text: string
|
||
|
||
try {
|
||
text = readStateText()
|
||
} catch {
|
||
return null
|
||
}
|
||
|
||
return parseMachineBinding(text)
|
||
}
|