import crypto from "node:crypto"; export type SessionBinding = { clientSessionId: string; sessionId: string; lastUsedAt: number; }; export type SessionContext = { clientSessionId: string; accessToken?: string; projectId?: string; userId?: string; }; export class SessionRegistry { private readonly ttlMs: number; private readonly bindings = new Map(); constructor(ttlSeconds: number) { this.ttlMs = ttlSeconds * 1000; } upsert(context: SessionContext, sessionId: string): SessionBinding { const binding: SessionBinding = { clientSessionId: context.clientSessionId, sessionId, lastUsedAt: Date.now(), }; this.bindings.set(this.makeKey(context), binding); return binding; } get(context: SessionContext): SessionBinding | null { const key = this.makeKey(context); const binding = this.bindings.get(key); if (!binding) { return null; } if (Date.now() - binding.lastUsedAt > this.ttlMs) { this.bindings.delete(key); return null; } binding.lastUsedAt = Date.now(); return binding; } count(): number { this.evictExpired(); return this.bindings.size; } evictExpired(): string[] { const expired: string[] = []; const now = Date.now(); for (const [key, binding] of this.bindings.entries()) { if (now - binding.lastUsedAt > this.ttlMs) { expired.push(binding.sessionId); this.bindings.delete(key); } } return expired; } private makeKey(context: SessionContext): string { // 会话隔离不能只看前端 session_id;同一浏览器会话切换用户或项目时必须映射到不同 opencode session。 const digest = crypto .createHash("sha256") .update( [ context.clientSessionId, context.userId?.trim() ?? "", context.projectId ?? "", ].join("|"), ) .digest("hex"); return digest; } }