Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
eslint.config.js linguist-detectable=false
scripts/*.js linguist-detectable=false
scripts/*.sh linguist-detectable=false
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ OpenCode plugin: intercepts OpenAI SDK calls, routes through ChatGPT Codex backe
- Source: root `index.ts` + `lib/`; `dist/` is generated output.
- ESLint flat config: `no-explicit-any` enforced, unused args prefixed `_`.
- Tests relax lint rules (see `eslint.config.js`).
- Build copies `lib/oauth-success.html` to `dist/lib/` via `scripts/copy-oauth-success.js`.
- Build emits `dist/lib/oauth-success.html` from the TypeScript source via `scripts/copy-oauth-success.js`.
- ESM only (`"type": "module"`), Node >= 18.

## ANTI-PATTERNS (THIS PROJECT)
Expand Down
10 changes: 2 additions & 8 deletions lib/auth/server.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import http from "node:http";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { OAuthServerInfo } from "../types.js";
import { logError, logWarn } from "../logger.js";
import { oauthSuccessHtml } from "../oauth-success.js";
import {
OAUTH_CALLBACK_BIND_URL,
OAUTH_CALLBACK_LOOPBACK_HOST,
OAUTH_CALLBACK_PATH,
OAUTH_CALLBACK_PORT,
} from "../runtime-contracts.js";

// Resolve path to oauth-success.html (one level up from auth/ subfolder)
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const successHtml = fs.readFileSync(path.join(__dirname, "..", "oauth-success.html"), "utf-8");

/**
* Start a small local HTTP server that waits for /auth/callback and returns the code
* @param options - OAuth state for validation
Expand Down Expand Up @@ -46,7 +40,7 @@ export function startLocalOAuthServer({ state }: { state: string }): Promise<OAu
res.setHeader("X-Frame-Options", "DENY");
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'none'");
res.end(successHtml);
res.end(oauthSuccessHtml);
(server as http.Server & { _lastCode?: string })._lastCode = code;
} catch (err) {
logError(`Request handler error: ${(err as Error)?.message ?? String(err)}`);
Expand Down
7 changes: 4 additions & 3 deletions lib/oauth-success.html → lib/oauth-success.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
export const oauthSuccessHtml = String.raw`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
Expand Down Expand Up @@ -642,7 +642,7 @@
if (isNearMouse) {
// Matrix green color when near mouse
const intensity = 1 - (distanceFromMouse / hoverRadius);
ctx.fillStyle = `rgba(0, 255, 65, ${intensity * 0.95})`;
ctx.fillStyle = \`rgba(0, 255, 65, \${intensity * 0.95})\`;
ctx.shadowBlur = 20 * intensity;
ctx.shadowColor = '#00ff41';
} else {
Expand Down Expand Up @@ -705,8 +705,9 @@
const moveY = (e.clientY - window.innerHeight / 2) / 50;

document.querySelector('.container').style.transform =
`translate(${moveX}px, ${moveY}px)`;
\`translate(\${moveX}px, \${moveY}px)\`;
});
</script>
</body>
</html>
`;
22 changes: 16 additions & 6 deletions scripts/copy-oauth-success.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { promises as fs } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Expand All @@ -11,20 +11,30 @@ function normalizePathForCompare(path) {
}

function getDefaultPaths() {
const src = join(__dirname, "..", "lib", "oauth-success.html");
const modulePath = join(__dirname, "..", "dist", "lib", "oauth-success.js");
const dest = join(__dirname, "..", "dist", "lib", "oauth-success.html");
return { src, dest };
return { modulePath, dest };
}

async function loadOAuthSuccessHtml(modulePath) {
const moduleUrl = pathToFileURL(modulePath).href;
const mod = await import(moduleUrl);
if (typeof mod.oauthSuccessHtml !== "string") {
throw new TypeError(`Expected oauthSuccessHtml string export from ${modulePath}`);
}
return mod.oauthSuccessHtml;
}

export async function copyOAuthSuccessHtml(options = {}) {
const defaults = getDefaultPaths();
const src = options.src ?? defaults.src;
const modulePath = options.modulePath ?? defaults.modulePath;
const dest = options.dest ?? defaults.dest;
const html = options.html ?? (await loadOAuthSuccessHtml(modulePath));

await fs.mkdir(dirname(dest), { recursive: true });
await fs.copyFile(src, dest);
await fs.writeFile(dest, html, "utf-8");

return { src, dest };
return { modulePath, dest };
}

const isDirectRun = (() => {
Expand Down
38 changes: 18 additions & 20 deletions test/copy-oauth-success.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import { describe, it, expect } from "vitest";
import { mkdtemp, writeFile, readFile, rm } from "node:fs/promises";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";

describe("copy-oauth-success script", () => {
it("exports copyOAuthSuccessHtml() for reuse/testing", async () => {
const mod = await import("../scripts/copy-oauth-success.js");
expect(typeof mod.copyOAuthSuccessHtml).toBe("function");
});
it("exports copyOAuthSuccessHtml() for reuse/testing", async () => {
const mod = await import("../scripts/copy-oauth-success.js");
expect(typeof mod.copyOAuthSuccessHtml).toBe("function");
});

it("copies oauth-success.html to the requested destination", async () => {
const mod = await import("../scripts/copy-oauth-success.js");
it("copies oauth-success.html to the requested destination", async () => {
const mod = await import("../scripts/copy-oauth-success.js");

const root = await mkdtemp(join(tmpdir(), "opencode-oauth-success-"));
const src = join(root, "oauth-success.html");
const dest = join(root, "dist", "lib", "oauth-success.html");
const root = await mkdtemp(join(tmpdir(), "opencode-oauth-success-"));
const dest = join(root, "dist", "lib", "oauth-success.html");

try {
const html = "<!doctype html><html><body>ok</body></html>";
await writeFile(src, html, "utf-8");
try {
const html = "<!doctype html><html><body>ok</body></html>";

await mod.copyOAuthSuccessHtml({ src, dest });
await mod.copyOAuthSuccessHtml({ html, dest });

const copied = await readFile(dest, "utf-8");
expect(copied).toBe(html);
} finally {
await rm(root, { recursive: true, force: true });
}
});
const copied = await readFile(dest, "utf-8");
expect(copied).toBe(html);
} finally {
await rm(root, { recursive: true, force: true });
}
});
});
1 change: 1 addition & 0 deletions test/oauth-server.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe("OAuth Server Integration", () => {
const response = await fetch(callbackUrl);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain("text/html");
expect(await response.text()).toContain("ACCESS GRANTED");

// Server should have captured the code
const result = await serverInfo.waitForCode(testState);
Expand Down
6 changes: 2 additions & 4 deletions test/server.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@ vi.mock('node:http', () => {
};
});

vi.mock('node:fs', () => ({
default: {
readFileSync: vi.fn(() => '<html>Success</html>'),
},
vi.mock('../lib/oauth-success.js', () => ({
oauthSuccessHtml: '<html>Success</html>',
}));

vi.mock('../lib/logger.js', () => ({
Expand Down