fix: address auth and source handling review findings
This commit is contained in:
+3
-14
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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 }],
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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}`;
|
||||
|
||||
Reference in New Issue
Block a user