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 { promisify } from "node:util";
|
||||||
|
|
||||||
import { AuthError, type StoredToken } from "../search/types.js";
|
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 { loadToken, saveToken } from "./storage.js";
|
||||||
import {
|
import {
|
||||||
BROWSER_AUTH_HELP,
|
BROWSER_AUTH_HELP,
|
||||||
@@ -16,8 +16,6 @@ import {
|
|||||||
type PerplexityFetchResponse as AuthFetchResponse,
|
type PerplexityFetchResponse as AuthFetchResponse,
|
||||||
} from "../perplexity-fetch.js";
|
} from "../perplexity-fetch.js";
|
||||||
|
|
||||||
export { parseBrowserAuthInput } from "./browser.js";
|
|
||||||
|
|
||||||
const DESKTOP_AUTH_HELP =
|
const DESKTOP_AUTH_HELP =
|
||||||
"Install the Perplexity desktop app and sign in, or set PI_AUTH_NO_BORROW=1 to skip desktop token borrowing.";
|
"Install the Perplexity desktop app and sign in, or set PI_AUTH_NO_BORROW=1 to skip desktop token borrowing.";
|
||||||
const OTP_AUTH_HELP =
|
const OTP_AUTH_HELP =
|
||||||
@@ -28,13 +26,6 @@ const COOKIE_ENV_KEYS = ["PI_PERPLEXITY_COOKIE", "PI_PERPLEXITY_COOKIES"] as con
|
|||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
class BrowserChallengeError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
this.name = "BrowserChallengeError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthenticateOptions {
|
export interface AuthenticateOptions {
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
promptForEmail?: () => Promise<string | null | undefined>;
|
promptForEmail?: () => Promise<string | null | undefined>;
|
||||||
@@ -130,9 +121,7 @@ function isBrowserChallengeResponse(response: AuthFetchResponse): boolean {
|
|||||||
function throwHttpFailure(action: string, response: AuthFetchResponse): never {
|
function throwHttpFailure(action: string, response: AuthFetchResponse): never {
|
||||||
const failure = formatHttpFailure(action, response);
|
const failure = formatHttpFailure(action, response);
|
||||||
if (isBrowserChallengeResponse(response)) {
|
if (isBrowserChallengeResponse(response)) {
|
||||||
throw new BrowserChallengeError(
|
throw new Error(`${failure} Perplexity returned a browser challenge that Node fetch cannot solve.`);
|
||||||
`${failure} Perplexity returned a browser challenge that Node fetch cannot solve.`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(failure);
|
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> {
|
export async function authenticate(options: AuthenticateOptions = {}): Promise<StoredToken> {
|
||||||
const cached = await loadToken();
|
const cached = await loadToken();
|
||||||
if (cached) {
|
if (cached) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { browserLoginInstructions } from "../auth/browser.js";
|
|||||||
import { authenticate, saveBrowserAuthInput } from "../auth/login.js";
|
import { authenticate, saveBrowserAuthInput } from "../auth/login.js";
|
||||||
import { clearToken } from "../auth/storage.js";
|
import { clearToken } from "../auth/storage.js";
|
||||||
import { AuthError } from "../search/types.js";
|
import { AuthError } from "../search/types.js";
|
||||||
import { errorMessage } from "../render/util.js";
|
import { errorMessage } from "../util.js";
|
||||||
|
|
||||||
const LOGIN_COMMAND_NAME = "perplexity-login";
|
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 { authenticate } from "./auth/login.js";
|
||||||
import { loadConfig, resolveSearchDefaults } from "./config.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 { searchPerplexity } from "./search/client.js";
|
||||||
import { renderPerplexityCall } from "./render/call.js";
|
import { renderPerplexityCall } from "./render/call.js";
|
||||||
import { renderPerplexityResult } from "./render/result.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";
|
import { AuthError, SearchError } from "./search/types.js";
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
export default function (pi: ExtensionAPI) {
|
||||||
@@ -89,10 +89,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const formatted = formatForLLM(result, params.limit);
|
const formatted = formatForLLM(result, params.limit);
|
||||||
sourceCount =
|
sourceCount = effectiveSourceCount(result.sources.length, params.limit);
|
||||||
typeof params.limit === "number"
|
|
||||||
? Math.min(params.limit, result.sources.length)
|
|
||||||
: result.sources.length;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: formatted }],
|
content: [{ type: "text", text: formatted }],
|
||||||
|
|||||||
@@ -2,13 +2,6 @@ export function asString(value: unknown): string | undefined {
|
|||||||
return typeof value === "string" ? value : 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 {
|
export function asNumber(value: unknown): number | undefined {
|
||||||
return typeof value === "number" && Number.isFinite(value) ? value : 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 { mergeEvent, readSseEvents } from "./stream.js";
|
||||||
import type { SearchResult, StoredToken, StreamEvent, WebResult } from "./types.js";
|
import type { SearchResult, StoredToken, StreamEvent, WebResult } from "./types.js";
|
||||||
import { SearchError } 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";
|
import { PERPLEXITY_USER_AGENT, PERPLEXITY_API_VERSION } from "../constants.js";
|
||||||
|
|
||||||
const PERPLEXITY_ENDPOINT = "https://www.perplexity.ai/rest/sse/perplexity_ask";
|
const PERPLEXITY_ENDPOINT = "https://www.perplexity.ai/rest/sse/perplexity_ask";
|
||||||
@@ -14,7 +16,13 @@ export interface SearchParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeUrl(url: string): string {
|
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[] {
|
function dedupeSourcesByUrl(sources: WebResult[]): WebResult[] {
|
||||||
@@ -118,8 +126,8 @@ function buildRequestBody(params: SearchParams): Record<string, unknown> {
|
|||||||
model_preference: params.model,
|
model_preference: params.model,
|
||||||
sources: ["web"],
|
sources: ["web"],
|
||||||
attachments: [],
|
attachments: [],
|
||||||
frontend_uuid: crypto.randomUUID(),
|
frontend_uuid: randomUUID(),
|
||||||
frontend_context_uuid: crypto.randomUUID(),
|
frontend_context_uuid: randomUUID(),
|
||||||
version: PERPLEXITY_API_VERSION,
|
version: PERPLEXITY_API_VERSION,
|
||||||
language: "en-US",
|
language: "en-US",
|
||||||
timezone,
|
timezone,
|
||||||
@@ -184,7 +192,7 @@ export async function searchPerplexity(
|
|||||||
auth: AuthCredentials,
|
auth: AuthCredentials,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<SearchResult> {
|
): Promise<SearchResult> {
|
||||||
const requestId = crypto.randomUUID();
|
const requestId = randomUUID();
|
||||||
const requestBody = buildRequestBody(params);
|
const requestBody = buildRequestBody(params);
|
||||||
const requestHeaders = buildRequestHeaders(auth, requestId);
|
const requestHeaders = buildRequestHeaders(auth, requestId);
|
||||||
|
|
||||||
|
|||||||
@@ -58,14 +58,16 @@ function formatSource(source: WebResult, index: number): string {
|
|||||||
return lines.join("\n");
|
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. */
|
/** Format a SearchResult into LLM-friendly text with ## Answer, ## Sources, ## Meta sections. */
|
||||||
export function formatForLLM(result: SearchResult, limit?: number): string {
|
export function formatForLLM(result: SearchResult, limit?: number): string {
|
||||||
const sourceLimit =
|
const limitedSources = result.sources.slice(0, effectiveSourceCount(result.sources.length, limit));
|
||||||
typeof limit === "number" && Number.isFinite(limit)
|
|
||||||
? Math.max(0, Math.floor(limit))
|
|
||||||
: result.sources.length;
|
|
||||||
|
|
||||||
const limitedSources = result.sources.slice(0, sourceLimit);
|
|
||||||
|
|
||||||
const sourceSection =
|
const sourceSection =
|
||||||
limitedSources.length === 0
|
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' \\
|
-H 'cookie: pplx.visitor-id=visitor; __Secure-next-auth.session-token=${browserToken}; cf_clearance=clearance' \\
|
||||||
--data-raw '{"query":"hello"}'`;
|
--data-raw '{"query":"hello"}'`;
|
||||||
|
|
||||||
const { parseBrowserAuthInput } = await importLoginModule();
|
const { parseBrowserAuthInput } = await import("../../src/auth/browser.js");
|
||||||
const parsed = parseBrowserAuthInput(curl);
|
const parsed = parseBrowserAuthInput(curl);
|
||||||
|
|
||||||
expect(parsed?.cookies).toBe(
|
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' \\
|
--cookie='pplx.visitor-id=visitor; __Secure-next-auth.session-token=${browserToken}; cf_clearance=clearance' \\
|
||||||
--data-raw '{"query":"hello"}'`;
|
--data-raw '{"query":"hello"}'`;
|
||||||
|
|
||||||
const { parseBrowserAuthInput } = await importLoginModule();
|
const { parseBrowserAuthInput } = await import("../../src/auth/browser.js");
|
||||||
const parsed = parseBrowserAuthInput(curl);
|
const parsed = parseBrowserAuthInput(curl);
|
||||||
|
|
||||||
expect(parsed?.cookies).toBe(
|
expect(parsed?.cookies).toBe(
|
||||||
@@ -368,7 +368,7 @@ describe("auth/login", () => {
|
|||||||
|
|
||||||
test("parseBrowserAuthInput extracts cookies from unquoted -b and --cookie cURL forms", async () => {
|
test("parseBrowserAuthInput extracts cookies from unquoted -b and --cookie cURL forms", async () => {
|
||||||
const browserToken = createJwt(Date.now() + 2 * 60 * 60 * 1000);
|
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"]) {
|
for (const flag of ["-b", "--cookie"]) {
|
||||||
const curl = `curl 'https://www.perplexity.ai/rest/sse/perplexity_ask' ${flag} __Secure-next-auth.session-token=${browserToken}`;
|
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