From 838644734fb88b90e7daa3c232c98b3ceed7fbf6 Mon Sep 17 00:00:00 2001 From: Ivan Pereira <183991+ivanrvpereira@users.noreply.github.com> Date: Tue, 14 Jul 2026 12:06:33 +0100 Subject: [PATCH] fix: address auth and source handling review findings --- src/auth/login.ts | 17 +++-------------- src/commands/login.ts | 2 +- src/index.ts | 9 +++------ src/render/util.ts | 7 ------- src/search/client.ts | 18 +++++++++++++----- src/search/format.ts | 14 ++++++++------ src/util.ts | 6 ++++++ test/auth/login.test.ts | 6 +++--- 8 files changed, 37 insertions(+), 42 deletions(-) create mode 100644 src/util.ts diff --git a/src/auth/login.ts b/src/auth/login.ts index d2a8b04..da76165 100644 --- a/src/auth/login.ts +++ b/src/auth/login.ts @@ -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; @@ -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 { } } -/** 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 { const cached = await loadToken(); if (cached) { diff --git a/src/commands/login.ts b/src/commands/login.ts index 4e706d1..8e4521e 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -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"; diff --git a/src/index.ts b/src/index.ts index f02402a..b885fbd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 }], diff --git a/src/render/util.ts b/src/render/util.ts index 0dd7ed5..321a641 100644 --- a/src/render/util.ts +++ b/src/render/util.ts @@ -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; } diff --git a/src/search/client.ts b/src/search/client.ts index 29e9dac..23c9a8e 100644 --- a/src/search/client.ts +++ b/src/search/client.ts @@ -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 { 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 { - const requestId = crypto.randomUUID(); + const requestId = randomUUID(); const requestBody = buildRequestBody(params); const requestHeaders = buildRequestHeaders(auth, requestId); diff --git a/src/search/format.ts b/src/search/format.ts index 7ed4b0a..f4df734 100644 --- a/src/search/format.ts +++ b/src/search/format.ts @@ -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 diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..80b6108 --- /dev/null +++ b/src/util.ts @@ -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"; +} diff --git a/test/auth/login.test.ts b/test/auth/login.test.ts index 341443c..33fcfec 100644 --- a/test/auth/login.test.ts +++ b/test/auth/login.test.ts @@ -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}`;