From fc379a2f9104a98eb43576212ce9c6543fac0c8d Mon Sep 17 00:00:00 2001 From: Ian Webster Date: Fri, 7 Aug 2026 10:50:06 -0700 Subject: [PATCH] fix: repeat Windows credential checks after file removal --- sdk/typescript/src/runtime.ts | 38 ++++++----- sdk/typescript/tests-ts/runtime.test.ts | 88 +++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 15 deletions(-) diff --git a/sdk/typescript/src/runtime.ts b/sdk/typescript/src/runtime.ts index bf6c96e0..aad407da 100644 --- a/sdk/typescript/src/runtime.ts +++ b/sdk/typescript/src/runtime.ts @@ -577,23 +577,31 @@ export async function verifyStableWindowsCredentialDescendants( for (let attempt = 0; attempt < 3; attempt += 1) { let descendants = 0; const pending = [path]; - while (pending.length !== 0) { - const current = pending.pop()!; - const directory = await opendir(current); - for await (const entry of directory) { - const child = join(current, entry.name); - const metadata = await lstat(child); - if (metadata.isSymbolicLink()) { - throw new Error( - "Windows credential home contains a symbolic link or junction", - ); - } - if (!metadata.isDirectory() && !metadata.isFile()) { - throw new Error("Windows credential home contains an unsafe entry"); + try { + while (pending.length !== 0) { + const current = pending.pop()!; + const directory = await opendir(current); + for await (const entry of directory) { + const child = join(current, entry.name); + const metadata = await lstat(child); + if (metadata.isSymbolicLink()) { + throw new Error( + "Windows credential home contains a symbolic link or junction", + ); + } + if (!metadata.isDirectory() && !metadata.isFile()) { + throw new Error("Windows credential home contains an unsafe entry"); + } + descendants += 1; + if (metadata.isDirectory()) pending.push(child); } - descendants += 1; - if (metadata.isDirectory()) pending.push(child); } + } catch (error) { + const failure = error as NodeJS.ErrnoException; + if (failure.code === "ENOENT" && failure.path !== path) { + continue; + } + throw error; } if (descendants === 0) return; diff --git a/sdk/typescript/tests-ts/runtime.test.ts b/sdk/typescript/tests-ts/runtime.test.ts index 00f6e96f..927228b6 100644 --- a/sdk/typescript/tests-ts/runtime.test.ts +++ b/sdk/typescript/tests-ts/runtime.test.ts @@ -1937,6 +1937,94 @@ describe("runtime directories and plugin Python boundary", () => { expect(attempts).toBe(2); }); + test("retries Windows credential verification when a descendant disappears", async () => { + const root = await temporaryDirectory(); + const home = join(root, "home"); + const temporary = join(home, ".auth-temporary"); + await mkdir(home); + await writeFile(join(home, "auth.json"), "credential\n"); + await writeFile(temporary, "temporary credential\n"); + const originalLstat = fsPromises.lstat; + let removed = false; + let inspections = 0; + mock.module("node:fs/promises", () => ({ + ...fsPromises, + lstat: async (path: Parameters[0]) => { + if (path === temporary && !removed) { + removed = true; + await rm(temporary); + } + return originalLstat(path); + }, + })); + + try { + await verifyStableWindowsCredentialDescendants(home, async () => { + inspections += 1; + return 1; + }); + } finally { + mock.module("node:fs/promises", () => ({ + ...fsPromises, + lstat: originalLstat, + })); + } + + expect(removed).toBe(true); + expect(inspections).toBe(1); + }); + + test("rejects Windows credential descendants that repeatedly disappear", async () => { + const root = await temporaryDirectory(); + const home = join(root, "home"); + const credential = join(home, "auth.json"); + await mkdir(home); + await writeFile(credential, "credential\n"); + const originalLstat = fsPromises.lstat; + let attempts = 0; + mock.module("node:fs/promises", () => ({ + ...fsPromises, + lstat: async (path: Parameters[0]) => { + if (path === credential) { + attempts += 1; + throw Object.assign(new Error("credential disappeared"), { + code: "ENOENT", + path, + }); + } + return originalLstat(path); + }, + })); + + try { + await expect( + verifyStableWindowsCredentialDescendants(home, async () => 1), + ).rejects.toThrow("Windows credential descendants could not be verified"); + } finally { + mock.module("node:fs/promises", () => ({ + ...fsPromises, + lstat: originalLstat, + })); + } + + expect(attempts).toBe(3); + }); + + test("does not retry a missing Windows credential home", async () => { + const root = await temporaryDirectory(); + const home = join(root, "missing-home"); + let inspections = 0; + + await expect( + verifyStableWindowsCredentialDescendants(home, async () => { + inspections += 1; + return 0; + }), + ).rejects.toMatchObject({ code: "ENOENT", path: home }); + + expect(inspections).toBe(0); + }); + test("rejects Windows credential descendants that never stabilize", async () => { const root = await temporaryDirectory(); const home = join(root, "home");