mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 07:55:47 +01:00
31 lines
6.7 KiB
TypeScript
31 lines
6.7 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
import { mkdtemp, chmod, writeFile, rm } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { fileURLToPath } from "node:url";
|
|
import type { Cookie } from "@playwright/test";
|
|
import type { AcceptedEmailFault } from "./runtime-email-fault.js";
|
|
export interface EmailPrivateInput { operation: "setup" | "inspect" | "inspect-publication" | "seed-review" | "resolve"; scenarioId: "recovery.unknown-email-outcome" | "chat-channels.outbound-email" | "chat-channels.reply-to-email"; companyId: string; agentId: string; baseURL: string; inboxId: string; recipient: string; dedicatedDisposableInbox: true; credential: string; cookies: Cookie[]; connectionId?: string; endpointId?: string; subject?: string; textSha256?: string; after?: string; publicationId?: string; accepted?: AcceptedEmailFault; expectedPublicReceipt?: { publicationId: string; providerMessageId: string }; expectedRecipient?: string; expectedReply?: { threadId: string; messageId: string }; reviewer?: { address: string; credential: string; authorization: "reply-to-selected-synthetic-only"; nonce: string } }
|
|
export interface EmailPrivateReceipt { processIsolation: true; testRunnerAttached: false; apiStepRecorder: false; operation: EmailPrivateInput["operation"]; companyId: string; agentId: string; actualSingleDedicatedInbox: true; connectionId?: string; endpointId?: string; actualActiveEndpoint?: true; personId: string; publicSetupOperationCount: number; directProviderProbeCount: number; actualFixtureSendCount?: 2; matchedSent?: Array<{ messageId: string; threadId: string; publicationId: string; contentMatches: true; accepted: true; recipientCopyObserved: boolean }>; originalMessageMatched?: boolean; resolution?: "human-reconciled" | "provider-reconciled"; outcome?: "sent" | "delivered"; inbound?: { earlierMessageId: string; latestMessageId: string; threadId: string; earlierTimestamp: string; latestTimestamp: string; actualSenderQualified: true; actualRecipientQualified: true; earlierTextSha256: string; latestTextSha256: string } }
|
|
export function validateEmailPrivateReceipt(value: unknown, expected: Pick<EmailPrivateInput, "operation" | "companyId" | "agentId">): EmailPrivateReceipt {
|
|
const r = value as EmailPrivateReceipt, allowed = ["processIsolation", "testRunnerAttached", "apiStepRecorder", "operation", "companyId", "agentId", "actualSingleDedicatedInbox", "connectionId", "endpointId", "actualActiveEndpoint", "personId", "publicSetupOperationCount", "directProviderProbeCount", "matchedSent", "originalMessageMatched", "resolution", "outcome", "inbound", "actualFixtureSendCount"];
|
|
const uuid = (v: unknown) => typeof v === "string" && /^[a-f0-9-]{36}$/i.test(v), providerId = (v: unknown) => typeof v === "string" && v.length > 0 && v.length <= 998 && !/[\r\n]/.test(v);
|
|
if (!r || Object.keys(r).some(key => !allowed.includes(key)) || r.processIsolation !== true || r.testRunnerAttached !== false || r.apiStepRecorder !== false || r.operation !== expected.operation || r.companyId !== expected.companyId || r.agentId !== expected.agentId || r.actualSingleDedicatedInbox !== true || !providerId(r.personId) || !Number.isSafeInteger(r.publicSetupOperationCount) || r.publicSetupOperationCount < 0 || !Number.isSafeInteger(r.directProviderProbeCount) || r.directProviderProbeCount < 0
|
|
|| r.operation === "setup" && (!uuid(r.connectionId) || !uuid(r.endpointId) || r.actualActiveEndpoint !== true) || !["setup", "seed-review"].includes(r.operation) && (!Array.isArray(r.matchedSent) || r.matchedSent.length > 20 || r.matchedSent.some(row => Object.keys(row).sort().join() !== ["messageId", "threadId", "publicationId", "contentMatches", "accepted", "recipientCopyObserved"].sort().join() || !providerId(row.messageId) || !providerId(row.threadId) || !uuid(row.publicationId) || row.contentMatches !== true || row.accepted !== true || typeof row.recipientCopyObserved !== "boolean"))
|
|
|| r.operation === "seed-review" && (r.actualFixtureSendCount !== 2 || !r.inbound || Object.keys(r.inbound).sort().join() !== ["earlierMessageId", "latestMessageId", "threadId", "earlierTimestamp", "latestTimestamp", "actualSenderQualified", "actualRecipientQualified", "earlierTextSha256", "latestTextSha256"].sort().join() || !providerId(r.inbound.earlierMessageId) || !providerId(r.inbound.latestMessageId) || r.inbound.earlierMessageId === r.inbound.latestMessageId || !providerId(r.inbound.threadId) || !(Date.parse(r.inbound.latestTimestamp) > Date.parse(r.inbound.earlierTimestamp)) || r.inbound.actualSenderQualified !== true || r.inbound.actualRecipientQualified !== true || !/^[a-f0-9]{64}$/.test(r.inbound.earlierTextSha256) || !/^[a-f0-9]{64}$/.test(r.inbound.latestTextSha256))
|
|
|| r.operation === "resolve" && (!["human-reconciled", "provider-reconciled"].includes(String(r.resolution)) || !["sent", "delivered"].includes(String(r.outcome)))) throw new Error("Private email operation exceeded its exact closed receipt boundary");
|
|
return r;
|
|
}
|
|
export async function privateEmailOperation(input: EmailPrivateInput, privateSecrets: string[]): Promise<EmailPrivateReceipt> {
|
|
if (!input.credential || input.credential.length < 12) throw new Error("Selected real AgentMail credential is required");
|
|
privateSecrets.push(input.credential, ...input.cookies.map(row => row.value), ...(input.reviewer ? [input.reviewer.credential] : [])); const directory = await mkdtemp(path.join(tmpdir(), "paperclip-email-private-")); await chmod(directory, 0o700);
|
|
try {
|
|
const filename = path.join(directory, "request.json"); await writeFile(filename, JSON.stringify(input), { mode: 0o600, flag: "wx" });
|
|
const loader = fileURLToPath(new URL("../../../server/node_modules/tsx/dist/loader.mjs", import.meta.url)), worker = fileURLToPath(new URL("./runtime-email-private-worker.ts", import.meta.url));
|
|
let output: string; try { output = (await promisify(execFile)(process.execPath, ["--import", loader, worker, filename], { timeout: 180000, maxBuffer: 32768, env: { PATH: process.env.PATH ?? "", NODE_ENV: "test", HOME: directory, TMPDIR: directory, DO_NOT_TRACK: "1" } })).stdout; } catch { throw new Error("Actual private AgentMail operation failed; credential-bearing request and provider diagnostics withheld"); }
|
|
if (privateSecrets.some(secret => secret && [secret, encodeURIComponent(secret), Buffer.from(secret).toString("base64")].some(value => output.includes(value)))) throw new Error("Private email child exceeded its secret-free closed output; diagnostics withheld");
|
|
let value: unknown; try { value = JSON.parse(output); } catch { throw new Error("Private email operation returned no closed receipt"); } return validateEmailPrivateReceipt(value, input);
|
|
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
}
|