返回 DeepSeek-Reasonix
grants.ts
根目录 / desktop / electron / src / main / browser / grants.ts
1 import { noGrant } from "./errors.js";
2
3 export interface BrowserGrant {
4 grantId: string;
5 taskId: string;
6 sessionId: string;
7 diagnosticScope?: string;
8 generation: string;
9 createdAt: number;
10 }
11
12 export interface GrantRegistryDeps {
13 generation(): string;
14 now?(): number;
15 onRevoked?(grant: BrowserGrant): void;
16 }
17
18 // Grants are minted by Go per task (its "tabId") and die with the service
19 // generation that minted them, so a restarted service can never act on a
20 // grant it does not remember.
21 export class GrantRegistry {
22 private readonly grants = new Map<string, BrowserGrant>();
23 private generation = "";
24 private readonly revokedListeners = new Set<(grant: BrowserGrant) => void>();
25
26 constructor(private readonly deps: GrantRegistryDeps) {}
27
28 onRevoke(listener: (grant: BrowserGrant) => void): () => void {
29 this.revokedListeners.add(listener);
30 return () => { this.revokedListeners.delete(listener); };
31 }
32
33 install(input: { grantId: string; taskId: string; sessionId: string; diagnosticScope?: string }): BrowserGrant {
34 if (input.grantId === "" || input.taskId === "") throw noGrant("grantId and tabId are required");
35 const generation = this.deps.generation();
36 if (generation === "") throw noGrant("desktop service is not running");
37 this.generation = generation;
38 const grant: BrowserGrant = { ...input, generation, createdAt: (this.deps.now ?? Date.now)() };
39 this.grants.set(input.grantId, grant);
40 return grant;
41 }
42
43 revoke(grantId: string): BrowserGrant | null {
44 const grant = this.grants.get(grantId);
45 if (!grant) return null;
46 this.grants.delete(grantId);
47 this.deps.onRevoked?.(grant);
48 for (const listener of this.revokedListeners) listener(grant);
49 return grant;
50 }
51
52 // A generation change (service restart) retires every grant at once.
53 observeGeneration(generation: string): void {
54 if (generation === this.generation) return;
55 this.generation = generation;
56 for (const grantId of [...this.grants.keys()]) this.revoke(grantId);
57 }
58
59 verify(grantId: string): BrowserGrant {
60 const grant = this.grants.get(grantId);
61 if (!grant) throw noGrant(`unknown grant ${grantId || "(empty)"}`);
62 if (grant.generation !== this.deps.generation()) {
63 this.revoke(grantId);
64 throw noGrant("grant belongs to an earlier service generation");
65 }
66 return grant;
67 }
68
69 // The tab must belong to the grant's task; a grant never reaches across.
70 verifyTab(grantId: string, tabTaskId: string | undefined, tabSessionId?: string): BrowserGrant {
71 const grant = this.verify(grantId);
72 if (tabTaskId === undefined) throw noGrant("unknown browser tab");
73 if (tabTaskId !== grant.taskId) throw noGrant("browser tab belongs to another task");
74 if ((tabSessionId ?? "") !== grant.sessionId) throw noGrant("browser tab belongs to another session");
75 return grant;
76 }
77
78 get size(): number {
79 return this.grants.size;
80 }
81
82 // Diagnostic attribution follows live grants, not a separately evicted
83 // cache. This read does not authorize access or revoke stale generations.
84 diagnosticScopeForTab(taskId: string, sessionId: string): string | undefined {
85 let scope: string | undefined;
86 const generation = this.deps.generation();
87 for (const grant of this.grants.values()) {
88 if (grant.generation !== generation || grant.taskId !== taskId || grant.sessionId !== sessionId || !grant.diagnosticScope || !/^[a-f0-9]{64}$/.test(grant.diagnosticScope)) continue;
89 if (scope && scope !== grant.diagnosticScope) return undefined;
90 scope = grant.diagnosticScope;
91 }
92 return scope;
93 }
94 }
95
95 lines TYPESCRIPT