Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/node-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ jobs:
TMP: ${{ steps.windows-temp.outputs.path || runner.temp }}
TMPDIR: ${{ steps.windows-temp.outputs.path || runner.temp }}
CODEX_SECURITY_ALLOW_MACHINE_POLICY_TEST: ${{ runner.environment == 'github-hosted' && runner.os == 'Windows' && 'true' || 'false' }}
run: pnpm --dir sdk/typescript run test
run: pnpm --dir sdk/typescript run test ${{ runner.os == 'Windows' && '--timeout 60000' || '' }}

- name: Check formatting
run: pnpm --dir sdk/typescript run format
Expand Down
38 changes: 23 additions & 15 deletions sdk/typescript/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
94 changes: 91 additions & 3 deletions sdk/typescript/tests-ts/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { spawnSync } from "node:child_process";
import { execFile, spawnSync } from "node:child_process";
import { existsSync, renameSync, symlinkSync } from "node:fs";
import {
chmod,
Expand Down Expand Up @@ -29,6 +29,7 @@ import {
sep,
} from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { promisify } from "node:util";
import { brotliDecompressSync } from "node:zlib";
import { afterEach, describe, expect, mock, test } from "bun:test";
import { strToU8, zipSync } from "fflate";
Expand Down Expand Up @@ -1937,6 +1938,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<typeof lstat>[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<typeof lstat>[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");
Expand Down Expand Up @@ -2477,7 +2566,7 @@ describe("runtime directories and plugin Python boundary", () => {
"$unexpected = @($acl.Access | Where-Object { $_.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Allow -and $trusted -notcontains $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value })",
"[pscustomobject]@{ unexpected = $unexpected.Count } | ConvertTo-Json -Compress",
].join("; ");
const result = spawnSync(
const result = await promisify(execFile)(
powershell,
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", command],
{
Expand All @@ -2488,7 +2577,6 @@ describe("runtime directories and plugin Python boundary", () => {
},
);

expect(result.status).toBe(0);
expect(JSON.parse(result.stdout)).toEqual({ unexpected: 0 });
},
);
Expand Down
17 changes: 17 additions & 0 deletions sdk/typescript/tests-ts/skeleton.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,23 @@ describe("TypeScript package skeleton", () => {
}
});

test("gives Windows credential integration tests a larger CI timeout", async () => {
const packageJson = JSON.parse(
await readFile(new URL("../package.json", import.meta.url), "utf8"),
);
const ciWorkflow = await readFile(
new URL("../../../.github/workflows/node-ci.yml", import.meta.url),
"utf8",
);

expect(packageJson.scripts.test).toBe(
"bun test --timeout 30000 ./tests-ts",
);
expect(ciWorkflow).toContain(
"run: pnpm --dir sdk/typescript run test ${{ runner.os == 'Windows' && '--timeout 60000' || '' }}",
);
});

test("builds packages without a preinstalled package manager and provides a production audit", async () => {
const packageJson = JSON.parse(
await readFile(new URL("../package.json", import.meta.url), "utf8"),
Expand Down
Loading