Skip to content
Merged
12 changes: 5 additions & 7 deletions sdk/typescript/src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export async function createAuthenticatedGitHub(
} = {},
): Promise<Octokit> {
options.signal?.throwIfAborted();
host = host.toLowerCase();
let token = options.token;
if (token === undefined) {
const trusted = await resolveTrustedExecutable(
Expand Down Expand Up @@ -87,13 +88,10 @@ export async function createAuthenticatedGitHub(
}
return new Octokit({
auth: token,
...(host === "github.com"
? {}
: {
baseUrl: host.toLowerCase().endsWith(".ghe.com")
? `https://api.${host}`
: `https://${host}/api/v3`,
}),
baseUrl:
host === "github.com" || host.endsWith(".ghe.com")
? `https://api.${host}`
: `https://${host}/api/v3`,
});
}

Expand Down
97 changes: 83 additions & 14 deletions sdk/typescript/tests-ts/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import {
captureCli,
} from "./support/cli-run.js";
import { resolving } from "./support/promises.js";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { Octokit } from "@octokit/core";
import { describe, expect, test, mock } from "bun:test";
import { main } from "../src/cli.js";
Expand All @@ -19,6 +20,7 @@ import {
import { dependencies, FakeSignals } from "./cli-fixtures.js";
import type { OnCodex } from "./cli-fixtures.js";
import { throwing } from "./support/errors.js";
import { nodeCommand } from "./support/shell.js";

function alert(number: number, ref = "refs/heads/main") {
return {
Expand Down Expand Up @@ -302,6 +304,86 @@ describe("GitHub code scanning import", () => {
createAuthenticatedGitHub("github.com", { token: " " }),
).rejects.toThrow("must not be empty");
});

test("normalizes the hostname used for GitHub CLI credential lookup", async () => {
const root = await mkdtemp(join(tmpdir(), "codex-security-github-auth-"));
try {
const repository = join(root, "repository");
const script = join(root, "gh.mjs");
await mkdir(repository);
await symlink(
nodeCommand().command,
join(root, process.platform === "win32" ? "gh.exe" : "gh"),
"file",
);
await writeFile(
script,
`import assert from "node:assert/strict";
import { basename } from "node:path";
assert.deepEqual([basename(process.argv[1]), ...process.argv.slice(2)], ["auth", "token", "--hostname", process.env.EXPECTED_GITHUB_HOST]);
console.log("SYNTHETIC_CLI_TOKEN");
process.exit(0);`,
);
for (const host of [
"GitHub.Com",
"Example.GHE.COM",
"GitHub.Example.Com:8443",
]) {
const client = await createAuthenticatedGitHub(host, {
currentDirectory: repository,
environment: {
PATH: root,
SystemRoot: process.env["SystemRoot"],
NODE_OPTIONS: `--import=${pathToFileURL(script).href}`,
EXPECTED_GITHUB_HOST: host.toLowerCase(),
},
});
expect(await client.auth()).toMatchObject({
token: "SYNTHETIC_CLI_TOKEN",
});
}
} finally {
await rm(root, { recursive: true, force: true });
}
});

test.each([
["github.com", "https://api.github.com", "/graphql"],
["GitHub.Com", "https://api.github.com", "/graphql"],
["example.ghe.com", "https://api.example.ghe.com", "/graphql"],
["Example.GHE.COM", "https://api.example.ghe.com", "/graphql"],
[
"github.example.test",
"https://github.example.test/api/v3",
"/api/graphql",
],
[
"GitHub.Example.Com:8443",
"https://github.example.com:8443/api/v3",
"/api/graphql",
],
])(
"selects the REST and GraphQL endpoints for hostname %s",
async (host, baseUrl, graphqlPath) => {
const client = await createAuthenticatedGitHub(host, {
token: "SYNTHETIC_SDK_TOKEN",
environment: {},
});
expect(client.request.endpoint("GET /user")).toMatchObject({
url: `${baseUrl}/user`,
});
await client.graphql("query { viewer { login } }", {
request: {
fetch: async (url: string | URL | Request) => {
expect(String(url)).toBe(
`${new URL(baseUrl).origin}${graphqlPath}`,
);
return Response.json({ data: { viewer: { login: "synthetic" } } });
},
},
});
},
);
});

describe("GitHub import CLI", () => {
Expand Down Expand Up @@ -443,16 +525,3 @@ describe("GitHub import CLI", () => {
expect(signals.listeners.get("SIGTERM")?.size).toBe(0);
});
});

for (const [host, url] of [
["github.com", "https://api.github.com/user"],
["example.ghe.com", "https://api.example.ghe.com/user"],
["github.example.test", "https://github.example.test/api/v3/user"],
]) {
test(`selects the documented API base for ${host}`, async () => {
const client = await createAuthenticatedGitHub(host!, {
token: "synthetic-token",
});
expect(client.request.endpoint("/user").url).toBe(url!);
});
}
Loading