81 lines
1.9 KiB
TypeScript
81 lines
1.9 KiB
TypeScript
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<string, SessionBinding>();
|
|
|
|
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;
|
|
}
|
|
}
|