75 lines
2.1 KiB
TypeScript
75 lines
2.1 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
|
|
import type { ExecApprovalDecision } from "../infra/exec-approvals.js";
|
|
|
|
export type ExecApprovalRequestPayload = {
|
|
command: string;
|
|
cwd?: string | null;
|
|
host?: string | null;
|
|
security?: string | null;
|
|
ask?: string | null;
|
|
agentId?: string | null;
|
|
resolvedPath?: string | null;
|
|
sessionKey?: string | null;
|
|
};
|
|
|
|
export type ExecApprovalRecord = {
|
|
id: string;
|
|
request: ExecApprovalRequestPayload;
|
|
createdAtMs: number;
|
|
expiresAtMs: number;
|
|
resolvedAtMs?: number;
|
|
decision?: ExecApprovalDecision;
|
|
resolvedBy?: string | null;
|
|
};
|
|
|
|
type PendingEntry = {
|
|
record: ExecApprovalRecord;
|
|
resolve: (decision: ExecApprovalDecision) => void;
|
|
reject: (err: Error) => void;
|
|
timer: ReturnType<typeof setTimeout>;
|
|
};
|
|
|
|
export class ExecApprovalManager {
|
|
private pending = new Map<string, PendingEntry>();
|
|
|
|
create(request: ExecApprovalRequestPayload, timeoutMs: number): ExecApprovalRecord {
|
|
const now = Date.now();
|
|
const id = randomUUID();
|
|
const record: ExecApprovalRecord = {
|
|
id,
|
|
request,
|
|
createdAtMs: now,
|
|
expiresAtMs: now + timeoutMs,
|
|
};
|
|
return record;
|
|
}
|
|
|
|
async waitForDecision(record: ExecApprovalRecord, timeoutMs: number): Promise<ExecApprovalDecision> {
|
|
return await new Promise<ExecApprovalDecision>((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
this.pending.delete(record.id);
|
|
resolve("deny");
|
|
}, timeoutMs);
|
|
this.pending.set(record.id, { record, resolve, reject, timer });
|
|
});
|
|
}
|
|
|
|
resolve(recordId: string, decision: ExecApprovalDecision, resolvedBy?: string | null): boolean {
|
|
const pending = this.pending.get(recordId);
|
|
if (!pending) return false;
|
|
clearTimeout(pending.timer);
|
|
pending.record.resolvedAtMs = Date.now();
|
|
pending.record.decision = decision;
|
|
pending.record.resolvedBy = resolvedBy ?? null;
|
|
this.pending.delete(recordId);
|
|
pending.resolve(decision);
|
|
return true;
|
|
}
|
|
|
|
getSnapshot(recordId: string): ExecApprovalRecord | null {
|
|
const entry = this.pending.get(recordId);
|
|
return entry?.record ?? null;
|
|
}
|
|
}
|