diff --git a/README.md b/README.md index b926faf..698aa6e 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ Model: pplx_pro_upgraded ```bash bun install # Install dev dependencies -bun test # Run tests (29 tests across 5 files) +bun test # Run tests (30 tests across 6 files) bunx tsc --noEmit # Type check ``` @@ -110,20 +110,25 @@ src/ util.ts # Shared render utilities ``` -### Zero Runtime Dependencies +### Runtime Dependencies -This extension has **no npm dependencies**. Everything uses platform globals: +This extension depends on the `bun` runtime package at execution time. + +Why: when pi loads extensions under Node/jiti, direct `fetch` to Perplexity is Cloudflare-challenged, so searches are executed through a Bun subprocess. + +Everything else uses platform globals: - `fetch` — HTTP requests - `crypto.randomUUID()` — request IDs - `ReadableStream` — SSE parsing - `Intl.DateTimeFormat` — timezone detection -Only peer dependencies (`@sinclair/typebox`, `@mariozechner/pi-tui`, `@mariozechner/pi-ai`) are used, provided by pi at runtime. +Peer dependencies (`@sinclair/typebox`, `@mariozechner/pi-tui`, `@mariozechner/pi-ai`) are provided by pi at runtime. ## Requirements - Perplexity **Pro** or **Max** subscription +- Bun installed and available on `PATH` (used by the search client when running under Node/jiti) - macOS (for desktop app token extraction) or interactive terminal (for email OTP) ## License diff --git a/bun.lock b/bun.lock index 450b323..164532b 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,9 @@ "workspaces": { "": { "name": "pi-perplexity", + "dependencies": { + "bun": "^1.3.9", + }, "devDependencies": { "@mariozechner/pi-ai": "*", "@mariozechner/pi-coding-agent": "*", @@ -132,6 +135,28 @@ "@mistralai/mistralai": ["@mistralai/mistralai@1.10.0", "", { "dependencies": { "zod": "^3.20.0", "zod-to-json-schema": "^3.24.1" } }, "sha512-tdIgWs4Le8vpvPiUEWne6tK0qbVc+jMenujnvTqOjogrJUsCSQhus0tHTU1avDDh5//Rq2dFgP9mWRAdIEoBqg=="], + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-df7smckMWSUfaT5mzwN9Lfpd3ZGkOqo+vmQ8VV2a32gl14v6uZ/qeeo+1RlANXn8M0uzXPWWCkrKZIWSZUR0qw=="], + + "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-YiLxfsPzQqaVvT2a+nxH9do0YfUjrlxF3tKP0b1DDgvfgCcVKGsrQH3Wa82qHgL4dnT8h2bqi94JxXESEuPmcA=="], + + "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-XbhsA2XAFzvFr0vPSV6SNqGxab4xHKdPmVTLqoSHAx9tffrSq/012BDptOskulwnD+YNsrJUx2D2Ve1xvfgGcg=="], + + "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-VaNQTu0Up4gnwZLQ6/Hmho6jAlLxTQ1PwxEth8EsXHf82FOXXPV5OCQ6KC9mmmocjKlmWFaIGebThrOy8DUo4g=="], + + "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-t8uimCVBTw5f9K2QTZE5wN6UOrFETNrh/Xr7qtXT9nAOzaOnIFvYA+HcHbGfi31fRlCVfTxqm/EiCwJ1gEw9YQ=="], + + "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-oQyAW3+ugulvXTZ+XYeUMmNPR94sJeMokfHQoKwPvVwhVkgRuMhcLGV2ZesHCADVu30Oz2MFXbgdC8x4/o9dRg=="], + + "@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-nZ12g22cy7pEOBwAxz2tp0wVqekaCn9QRKuGTHqOdLlyAqR4SCdErDvDhUWd51bIyHTQoCmj72TegGTgG0WNPw=="], + + "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-4ZjIUgCxEyKwcKXideB5sX0KJpnHTZtu778w73VNq2uNH2fNpMZv98+DBgJyQ9OfFoRhmKn1bmLmSefvnHzI9w=="], + + "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-3FXQgtYFsT0YOmAdMcJn56pLM5kzSl6y942rJJIl5l2KummB9Ea3J/vMJMzQk7NCAGhleZGWU/pJSS/uXKGa7w=="], + + "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.9", "", { "os": "win32", "cpu": "x64" }, "sha512-/d6vAmgKvkoYlsGPsRPlPmOK1slPis/F40UG02pYwypTH0wmY0smgzdFqR4YmryxFh17XrW1kITv+U99Oajk9Q=="], + + "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.9", "", { "os": "win32", "cpu": "x64" }, "sha512-a/+hSrrDpMD7THyXvE2KJy1skxzAD0cnW4K1WjuI/91VqsphjNzvf5t/ZgxEVL4wb6f+hKrSJ5J3aH47zPr61g=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], @@ -286,6 +311,8 @@ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "bun": ["bun@1.3.9", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.9", "@oven/bun-darwin-x64": "1.3.9", "@oven/bun-darwin-x64-baseline": "1.3.9", "@oven/bun-linux-aarch64": "1.3.9", "@oven/bun-linux-aarch64-musl": "1.3.9", "@oven/bun-linux-x64": "1.3.9", "@oven/bun-linux-x64-baseline": "1.3.9", "@oven/bun-linux-x64-musl": "1.3.9", "@oven/bun-linux-x64-musl-baseline": "1.3.9", "@oven/bun-windows-x64": "1.3.9", "@oven/bun-windows-x64-baseline": "1.3.9" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-v5hkh1us7sMNjfimWE70flYbD5I1/qWQaqmJ45q2qk5H/7muQVa478LSVRSFyGTBUBog2LsPQnfIRdjyWJRY+A=="], + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], diff --git a/package.json b/package.json index 92b91b3..0827233 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,9 @@ "@mariozechner/pi-ai": "*", "@sinclair/typebox": "*" }, + "dependencies": { + "bun": "^1.3.9" + }, "devDependencies": { "@mariozechner/pi-coding-agent": "*", "@mariozechner/pi-tui": "*", @@ -22,5 +25,8 @@ "scripts": { "typecheck": "tsc --noEmit", "test": "bun test" + }, + "engines": { + "bun": ">=1.3.9" } } diff --git a/src/auth/storage.ts b/src/auth/storage.ts index 212cecd..7949aff 100644 --- a/src/auth/storage.ts +++ b/src/auth/storage.ts @@ -1,4 +1,4 @@ -import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; @@ -38,6 +38,8 @@ export async function loadToken(): Promise { export async function saveToken(token: StoredToken): Promise { await mkdir(dirname(TOKEN_PATH), { recursive: true }); await writeFile(TOKEN_PATH, `${JSON.stringify(token, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); + // writeFile mode only applies on create; enforce on existing files too. + await chmod(TOKEN_PATH, 0o600); } /** Delete the stored token file. No-op if missing. */ diff --git a/test/auth/storage.test.ts b/test/auth/storage.test.ts new file mode 100644 index 0000000..54e99f1 --- /dev/null +++ b/test/auth/storage.test.ts @@ -0,0 +1,46 @@ +import { afterEach, describe, expect, mock, test } from "bun:test"; +import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; + +async function importStorageModule() { + return import(`../../src/auth/storage.ts?test=${crypto.randomUUID()}`); +} + +afterEach(() => { + mock.restore(); +}); + +describe("auth/storage", () => { + test("saveToken enforces 0600 permissions even when token file already exists", async () => { + const homeDir = await mkdtemp(join(tmpdir(), "pi-perplexity-storage-")); + const tokenPath = join(homeDir, ".config", "pi-perplexity", "auth.json"); + + try { + mock.module("node:os", () => ({ + homedir: () => homeDir, + })); + + await mkdir(dirname(tokenPath), { recursive: true }); + await writeFile(tokenPath, '{"type":"oauth","access":"old"}\n', { + encoding: "utf8", + mode: 0o644, + }); + + const { saveToken } = await importStorageModule(); + await saveToken({ type: "oauth", access: "new-token" }); + + const fileMode = (await stat(tokenPath)).mode & 0o777; + expect(fileMode).toBe(0o600); + + const saved = JSON.parse(await readFile(tokenPath, "utf8")) as { + type: string; + access: string; + }; + expect(saved.type).toBe("oauth"); + expect(saved.access).toBe("new-token"); + } finally { + await rm(homeDir, { recursive: true, force: true }); + } + }); +});