import type { LookupAddress } from "node:dns"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import type { LookupFunction } from "node:net"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { test } from "@openwork/testkit"; import { expect } from "vitest"; import type { ServerConfig } from "../../apps/server/src/types.js"; import { callGoogleWorkspaceExtensionAction, createGmailAttachmentPublicLookup, setGmailAttachmentFetchForTests, } from "../../apps/server/src/extensions/google-workspace.js"; function createTestConfig(root: string): ServerConfig { return { host: "127.0.0.1", port: 8787, token: "test-client-token", hostToken: "test-host-token", configPath: join(root, "server.json"), approval: { mode: "auto", timeoutMs: 30_000 }, corsOrigins: ["*"], workspaces: [], authorizedRoots: [], readOnly: false, startedAt: Date.now(), tokenSource: "generated", hostTokenSource: "generated", logFormat: "pretty", logRequests: false, }; } function base64Url(value: string): string { return Buffer.from(value, "utf8").toString("base64url"); } function decodeRawDraft(requestBody: string): string { const raw = requestBody.match(/"raw":"([^"]+)"/)?.[1] ?? ""; return Buffer.from(raw.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8"); } function restoreEnv(name: string, value: string | undefined): void { if (value === undefined) delete process.env[name]; else process.env[name] = value; } async function lookupAll(lookupFunction: LookupFunction, hostname: string): Promise { return await new Promise((resolve, reject) => { lookupFunction(hostname, { all: true }, (error, addresses) => { if (error) { reject(error); return; } if (!Array.isArray(addresses)) { reject(new Error("Expected an all-address DNS lookup result.")); return; } resolve(addresses); }); }); } test("Gmail reply drafts preserve thread fidelity and bind public attachment DNS to the socket", async ({ evidence }) => { const root = await mkdtemp(join(tmpdir(), "openwork-gmail-reply-proof-")); const config = createTestConfig(root); const previousFetch = globalThis.fetch; const previousEnv = { devMode: process.env.OPENWORK_DEV_MODE, plaintextVault: process.env.OPENWORK_GOOGLE_WORKSPACE_ALLOW_PLAINTEXT_VAULT, clientSecret: process.env.GOOGLE_WORKSPACE_OAUTH_CLIENT_SECRET, }; const googleRequests: { url: string; body: string }[] = []; const attachmentRequests: { url: string; redirect?: string }[] = []; try { process.env.OPENWORK_DEV_MODE = "1"; process.env.OPENWORK_GOOGLE_WORKSPACE_ALLOW_PLAINTEXT_VAULT = "1"; process.env.GOOGLE_WORKSPACE_OAUTH_CLIENT_SECRET = "proof-secret"; const vaultPath = join(dirname(config.configPath ?? ""), "extensions", "google-workspace", "oauth.dev-plaintext.json"); await mkdir(dirname(vaultPath), { recursive: true }); await writeFile(vaultPath, JSON.stringify({ version: 2, activeAccountId: "proof-account", accounts: [{ account: { email: "owner@example.com", name: "Owner", sub: "proof-account", picture: null }, scopes: ["openid", "https://www.googleapis.com/auth/gmail.readonly"], token: { accessToken: "proof-access-token", refreshToken: "proof-refresh-token", expiresAt: Date.now() + 60 * 60 * 1000, }, connectedAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }], })); globalThis.fetch = async (input: string | URL | Request, init?: RequestInit) => { const url = String(input instanceof Request ? input.url : input); if (url.includes("/messages/message-1")) { return new Response(JSON.stringify({ id: "message-1", threadId: "thread-1", payload: { headers: [ { name: "Message-ID", value: "" }, { name: "Subject", value: "Design review" }, { name: "From", value: "Alice " }, { name: "Date", value: "Thu, 16 Jul 2026 15:21:00 +0000" }, ], parts: [{ mimeType: "text/html", body: { data: base64Url("
Hello
world & more
") }, }], }, }), { status: 200 }); } googleRequests.push({ url, body: typeof init?.body === "string" ? init.body : "" }); return new Response(JSON.stringify({ id: "draft-1", message: { id: "draft-message-1", threadId: "thread-1" }, }), { status: 200 }); }; setGmailAttachmentFetchForTests(async (input, init) => { attachmentRequests.push({ url: input.toString(), redirect: init?.redirect }); return new Response("PDF BYTES", { status: 200, headers: { "content-type": "application/pdf", "content-disposition": "attachment; filename=design-review.pdf", }, }); }); const reply = await callGoogleWorkspaceExtensionAction(config, "gmail_create_reply_draft", { messageId: "message-1", body: "Thanks for the review.", attachments: [{ url: "https://93.184.216.34/design-review.pdf" }], }, {}); const raw = decodeRawDraft(googleRequests[0]?.body ?? ""); expect(reply?.result).toMatchObject({ draftUrl: "https://mail.google.com/mail/u/0/#drafts?compose=draft-message-1", threadUrl: "https://mail.google.com/mail/u/0/#all/thread-1", reply: { threadId: "thread-1", subject: "Re: Design review", to: ["Alice "], inReplyTo: "", }, }); expect(raw).toContain("Content-Type: multipart/mixed;"); expect(raw).toContain("Content-Type: multipart/alternative;"); expect(raw).toContain("In-Reply-To: "); expect(raw).toContain("> Hello\r\n> world & more"); expect(raw).toContain('
Hello
world & more
"); expect(raw).not.toContain("alert(1)"); expect(raw).not.toContain(" { resolverCalls += 1; return resolverCalls === 1 ? [{ address: "93.184.216.34", family: 4 }] : [{ address: "169.254.169.254", family: 4 }]; }); await expect(lookupAll(socketLookup, "files.example.test")).resolves.toEqual([ { address: "93.184.216.34", family: 4 }, ]); await expect(lookupAll(socketLookup, "files.example.test")).rejects.toThrow( "resolved to a private or reserved address", ); evidence.recordAssertionEvidence( "Reply drafts retain the target thread and Gmail-collapsible quoted history", "The model-callable reply action returned the resolved thread, subject, recipient, and In-Reply-To target; its raw MIME contained matching plain and gmail_quote HTML renditions of an HTML-only original while excluding script content.", true, ); evidence.recordAssertionEvidence( "Reply attachments carry exact public-URL bytes without weakening URL policy", "The reply MIME carried the witnessed PDF filename and exact bytes after one manual-redirect HTTPS request, while an HTTP link-local attachment was rejected before a second download.", true, ); evidence.recordAssertionEvidence( "Attachment DNS validation is bound to the socket lookup", "The connector lookup handed its first public answer to the caller and rejected a later link-local rebinding answer returned by the socket's own lookup.", resolverCalls === 2, ); } finally { setGmailAttachmentFetchForTests(); globalThis.fetch = previousFetch; restoreEnv("OPENWORK_DEV_MODE", previousEnv.devMode); restoreEnv("OPENWORK_GOOGLE_WORKSPACE_ALLOW_PLAINTEXT_VAULT", previousEnv.plaintextVault); restoreEnv("GOOGLE_WORKSPACE_OAUTH_CLIENT_SECRET", previousEnv.clientSecret); await rm(root, { recursive: true, force: true }); } });