Files
Agent-System-Frontend/src/auth.ts
T
weichen 55a64f40e8 Respect canonical project bindings and isolate account state
Why:
Project members need RDM commands to address the registered domain project.
Failed project states and responses from a previous account must not create
work, report a rejected mutation as successful, or restore private content.

Evidence:
SELF npm test: 32 files and 251 tests passed, including project command,
mutation rejection, archived history, and delayed account response coverage.
SELF npm run build and git diff --check passed. Original regressions were
observed before implementation; root owns composition and independent replay.

Impact:
Keep host IDs for conversation and base settings APIs; send the decorated
domain ID to RDM project commands. Reuse existing sidebar, settings and toast
states. Reset identity projections and invalidate old asynchronous responses.
No deployment, database change, push, merge, or workspace cleanup.

Refs:
Dev feature-project-member-access task card and implementation plan Task 4
2026-10-13 08:20:44 +00:00

59 lines
2.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// API key 凭证模块:登录门存取(localStorage 优先,无 DOM 环境回退内存)。
// 服务端开启认证时(PG 档默认),所有请求带 Bearer 头;SSE/静态资源链接走 ?key=。
const KEY = 'agent-system.apiKey'
interface KV { getItem(k: string): string | null; setItem(k: string, v: string): void; removeItem(k: string): void }
function pickStorage(): KV {
try {
if (typeof localStorage !== 'undefined') return localStorage
} catch { /* 隐私模式等 */ }
const mem = new Map<string, string>()
return {
getItem: k => mem.get(k) ?? null,
setItem: (k, v) => void mem.set(k, v),
removeItem: k => void mem.delete(k),
}
}
const storage = pickStorage()
const identityListeners = new Set<() => void>()
function identityChanged() { identityListeners.forEach(listener => listener()) }
export const auth = {
apiKey(): string { return storage.getItem(KEY) ?? '' },
setKey(key: string) {
const next = key.trim()
if (next === this.apiKey()) return
storage.setItem(KEY, next)
identityChanged()
},
clear() { storage.removeItem(KEY); identityChanged() },
onIdentityChange(listener: () => void) {
identityListeners.add(listener)
return () => { identityListeners.delete(listener) }
},
// 密码登录成功后切换到 HttpOnly 会话 cookie:必须清掉残留 Bearer——服务端
// 认证中间件 Bearer 优先于会话 cookie,失效残留 key 会把有效会话也判成 401。
beginSession() { this.clear() },
// 带凭证的 fetch:未设置密钥时与原生 fetch 等价(嵌入式档无认证)。
fetch(input: string, init?: RequestInit): Promise<Response> {
const key = this.apiKey()
if (!key) return fetch(input, init)
const headers = new Headers(init?.headers ?? {})
headers.set('Authorization', `Bearer ${key}`)
return fetch(input, { ...init, headers })
},
// 退出登录:立即清客户端投影并回登录门;调用者先经 api.logout 注销服务端会话。
logout() {
this.clear()
try { location.reload() } catch { /* 非 DOM 环境(测试)忽略 */ }
},
// 无法带头的资源 URL(EventSource / <img> / 下载):追加 ?key=。
withKey(url: string): string {
const key = this.apiKey()
if (!key) return url
return url + (url.includes('?') ? '&' : '?') + 'key=' + encodeURIComponent(key)
},
}