fix: address auth and source handling review findings

This commit is contained in:
Ivan Pereira
2026-07-14 12:06:33 +01:00
parent e6a194bd85
commit 838644734f
8 changed files with 37 additions and 42 deletions
+3 -14
View File
@@ -2,7 +2,7 @@ import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { AuthError, type StoredToken } from "../search/types.js";
import { errorMessage } from "../render/util.js";
import { errorMessage } from "../util.js";
import { loadToken, saveToken } from "./storage.js";
import {
BROWSER_AUTH_HELP,
@@ -16,8 +16,6 @@ import {
type PerplexityFetchResponse as AuthFetchResponse,
} from "../perplexity-fetch.js";
export { parseBrowserAuthInput } from "./browser.js";
const DESKTOP_AUTH_HELP =
"Install the Perplexity desktop app and sign in, or set PI_AUTH_NO_BORROW=1 to skip desktop token borrowing.";
const OTP_AUTH_HELP =
@@ -28,13 +26,6 @@ const COOKIE_ENV_KEYS = ["PI_PERPLEXITY_COOKIE", "PI_PERPLEXITY_COOKIES"] as con
const execFileAsync = promisify(execFile);
class BrowserChallengeError extends Error {
constructor(message: string) {
super(message);
this.name = "BrowserChallengeError";
}
}
export interface AuthenticateOptions {
signal?: AbortSignal;
promptForEmail?: () => Promise<string | null | undefined>;
@@ -130,9 +121,7 @@ function isBrowserChallengeResponse(response: AuthFetchResponse): boolean {
function throwHttpFailure(action: string, response: AuthFetchResponse): never {
const failure = formatHttpFailure(action, response);
if (isBrowserChallengeResponse(response)) {
throw new BrowserChallengeError(
`${failure} Perplexity returned a browser challenge that Node fetch cannot solve.`,
);
throw new Error(`${failure} Perplexity returned a browser challenge that Node fetch cannot solve.`);
}
throw new Error(failure);
@@ -256,7 +245,7 @@ export async function extractFromDesktopApp(): Promise<string | null> {
}
}
/** Run auth strategy: load cached → env token/cookies → desktop extraction → email OTP → browser paste fallback. */
/** Run auth strategy: load cached → env token/cookies → desktop extraction → email OTP. Browser paste is handled via /perplexity-login --browser. */
export async function authenticate(options: AuthenticateOptions = {}): Promise<StoredToken> {
const cached = await loadToken();
if (cached) {
+1 -1
View File
@@ -4,7 +4,7 @@ import { browserLoginInstructions } from "../auth/browser.js";
import { authenticate, saveBrowserAuthInput } from "../auth/login.js";
import { clearToken } from "../auth/storage.js";
import { AuthError } from "../search/types.js";
import { errorMessage } from "../render/util.js";
import { errorMessage } from "../util.js";
const LOGIN_COMMAND_NAME = "perplexity-login";
+3 -6
View File
@@ -7,11 +7,11 @@ import { registerPerplexityCommands } from "./commands/login.js";
import { authenticate } from "./auth/login.js";
import { loadConfig, resolveSearchDefaults } from "./config.js";
import { formatForLLM } from "./search/format.js";
import { effectiveSourceCount, formatForLLM } from "./search/format.js";
import { searchPerplexity } from "./search/client.js";
import { renderPerplexityCall } from "./render/call.js";
import { renderPerplexityResult } from "./render/result.js";
import { errorMessage } from "./render/util.js";
import { errorMessage } from "./util.js";
import { AuthError, SearchError } from "./search/types.js";
export default function (pi: ExtensionAPI) {
@@ -89,10 +89,7 @@ export default function (pi: ExtensionAPI) {
);
const formatted = formatForLLM(result, params.limit);
sourceCount =
typeof params.limit === "number"
? Math.min(params.limit, result.sources.length)
: result.sources.length;
sourceCount = effectiveSourceCount(result.sources.length, params.limit);
return {
content: [{ type: "text", text: formatted }],
-7
View File
@@ -2,13 +2,6 @@ export function asString(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}
/** Safely extract a message from an unknown caught value. */
export function errorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === "string") return error;
return "Unknown error";
}
export function asNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
+13 -5
View File
@@ -1,7 +1,9 @@
import { randomUUID } from "node:crypto";
import { mergeEvent, readSseEvents } from "./stream.js";
import type { SearchResult, StoredToken, StreamEvent, WebResult } from "./types.js";
import { SearchError } from "./types.js";
import { errorMessage } from "../render/util.js";
import { errorMessage } from "../util.js";
import { PERPLEXITY_USER_AGENT, PERPLEXITY_API_VERSION } from "../constants.js";
const PERPLEXITY_ENDPOINT = "https://www.perplexity.ai/rest/sse/perplexity_ask";
@@ -14,7 +16,13 @@ export interface SearchParams {
}
function normalizeUrl(url: string): string {
return url.trim().replace(/\/$/, "").toLowerCase();
const trimmed = url.trim().replace(/\/$/, "");
try {
// URL lowercases scheme and host; paths/queries stay case-sensitive.
return new URL(trimmed).href.replace(/\/$/, "");
} catch {
return trimmed.toLowerCase();
}
}
function dedupeSourcesByUrl(sources: WebResult[]): WebResult[] {
@@ -118,8 +126,8 @@ function buildRequestBody(params: SearchParams): Record<string, unknown> {
model_preference: params.model,
sources: ["web"],
attachments: [],
frontend_uuid: crypto.randomUUID(),
frontend_context_uuid: crypto.randomUUID(),
frontend_uuid: randomUUID(),
frontend_context_uuid: randomUUID(),
version: PERPLEXITY_API_VERSION,
language: "en-US",
timezone,
@@ -184,7 +192,7 @@ export async function searchPerplexity(
auth: AuthCredentials,
signal?: AbortSignal,
): Promise<SearchResult> {
const requestId = crypto.randomUUID();
const requestId = randomUUID();
const requestBody = buildRequestBody(params);
const requestHeaders = buildRequestHeaders(auth, requestId);
+8 -6
View File
@@ -58,14 +58,16 @@ function formatSource(source: WebResult, index: number): string {
return lines.join("\n");
}
/** Number of sources actually rendered for a given total and requested limit. */
export function effectiveSourceCount(total: number, limit?: number): number {
const sourceLimit =
typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : total;
return Math.min(sourceLimit, total);
}
/** Format a SearchResult into LLM-friendly text with ## Answer, ## Sources, ## Meta sections. */
export function formatForLLM(result: SearchResult, limit?: number): string {
const sourceLimit =
typeof limit === "number" && Number.isFinite(limit)
? Math.max(0, Math.floor(limit))
: result.sources.length;
const limitedSources = result.sources.slice(0, sourceLimit);
const limitedSources = result.sources.slice(0, effectiveSourceCount(result.sources.length, limit));
const sourceSection =
limitedSources.length === 0
+6
View File
@@ -0,0 +1,6 @@
/** Safely extract a message from an unknown caught value. */
export function errorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === "string") return error;
return "Unknown error";
}
+3 -3
View File
@@ -342,7 +342,7 @@ describe("auth/login", () => {
-H 'cookie: pplx.visitor-id=visitor; __Secure-next-auth.session-token=${browserToken}; cf_clearance=clearance' \\
--data-raw '{"query":"hello"}'`;
const { parseBrowserAuthInput } = await importLoginModule();
const { parseBrowserAuthInput } = await import("../../src/auth/browser.js");
const parsed = parseBrowserAuthInput(curl);
expect(parsed?.cookies).toBe(
@@ -357,7 +357,7 @@ describe("auth/login", () => {
--cookie='pplx.visitor-id=visitor; __Secure-next-auth.session-token=${browserToken}; cf_clearance=clearance' \\
--data-raw '{"query":"hello"}'`;
const { parseBrowserAuthInput } = await importLoginModule();
const { parseBrowserAuthInput } = await import("../../src/auth/browser.js");
const parsed = parseBrowserAuthInput(curl);
expect(parsed?.cookies).toBe(
@@ -368,7 +368,7 @@ describe("auth/login", () => {
test("parseBrowserAuthInput extracts cookies from unquoted -b and --cookie cURL forms", async () => {
const browserToken = createJwt(Date.now() + 2 * 60 * 60 * 1000);
const { parseBrowserAuthInput } = await importLoginModule();
const { parseBrowserAuthInput } = await import("../../src/auth/browser.js");
for (const flag of ["-b", "--cookie"]) {
const curl = `curl 'https://www.perplexity.ai/rest/sse/perplexity_ask' ${flag} __Secure-next-auth.session-token=${browserToken}`;