Harden token storage and declare Bun runtime

Enforce 0600 permissions after every auth token write so existing files
with broader modes are corrected.

Add a regression test for the permission hardening path and document the
Bun runtime dependency used by the Node/jiti search subprocess path.
Also declare bun in package dependencies and engines.
This commit is contained in:
Ivan Pereira
2026-02-23 10:47:05 +00:00
parent a3ddb02a65
commit 76dbc20064
5 changed files with 91 additions and 5 deletions
+9 -4
View File
@@ -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
+27
View File
@@ -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=="],
+6
View File
@@ -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"
}
}
+3 -1
View File
@@ -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<StoredToken | null> {
export async function saveToken(token: StoredToken): Promise<void> {
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. */
+46
View File
@@ -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 });
}
});
});