fix(auth): avoid CSRF challenges during OTP login
This commit is contained in:
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.2.2] - 2026-06-22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Route email OTP authentication requests through the Bun-backed Perplexity fetch path to avoid CSRF 403 failures under Node/jiti.
|
||||||
|
- Send browser-like auth headers and include response previews in OTP HTTP errors.
|
||||||
|
|
||||||
## [0.2.1] - 2026-05-11
|
## [0.2.1] - 2026-05-11
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ Queries default to `is_incognito: true`, but you can override that per call or v
|
|||||||
|
|
||||||
The extension calls Perplexity's internal SSE endpoint (`perplexity_ask`) using your subscription credentials obtained from the macOS app or via email OTP. Responses stream as incremental events that are merged into a final result.
|
The extension calls Perplexity's internal SSE endpoint (`perplexity_ask`) using your subscription credentials obtained from the macOS app or via email OTP. Responses stream as incremental events that are merged into a final result.
|
||||||
|
|
||||||
When pi loads extensions under Node/jiti, direct `fetch` to Perplexity gets Cloudflare-challenged, so the search client shells out to a Bun subprocess — that's the only reason Bun is required.
|
When pi loads extensions under Node/jiti, direct `fetch` to Perplexity can get Cloudflare-challenged, so Perplexity network calls shell out to a Bun subprocess — that's the only reason Bun is required.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pi-perplexity",
|
"name": "pi-perplexity",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"description": "Perplexity web search for pi — uses your Pro/Max subscription, no API credits needed",
|
"description": "Perplexity web search for pi — uses your Pro/Max subscription, no API credits needed",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"pi-package",
|
"pi-package",
|
||||||
|
|||||||
+34
-18
@@ -5,6 +5,10 @@ import { AuthError } from "../search/types.js";
|
|||||||
import { errorMessage } from "../render/util.js";
|
import { errorMessage } from "../render/util.js";
|
||||||
import { loadToken, saveToken } from "./storage.js";
|
import { loadToken, saveToken } from "./storage.js";
|
||||||
import { PERPLEXITY_USER_AGENT, PERPLEXITY_API_VERSION } from "../constants.js";
|
import { PERPLEXITY_USER_AGENT, PERPLEXITY_API_VERSION } from "../constants.js";
|
||||||
|
import {
|
||||||
|
perplexityFetchText as fetchAuth,
|
||||||
|
type PerplexityFetchResponse as AuthFetchResponse,
|
||||||
|
} from "../perplexity-fetch.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.";
|
||||||
@@ -29,11 +33,32 @@ function buildAuthHeaders(includeJsonContentType = false): Record<string, string
|
|||||||
return {
|
return {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
...(includeJsonContentType ? { "Content-Type": "application/json" } : {}),
|
...(includeJsonContentType ? { "Content-Type": "application/json" } : {}),
|
||||||
|
Origin: "https://www.perplexity.ai",
|
||||||
|
Referer: "https://www.perplexity.ai/",
|
||||||
"User-Agent": PERPLEXITY_USER_AGENT,
|
"User-Agent": PERPLEXITY_USER_AGENT,
|
||||||
|
"X-App-ApiClient": "default",
|
||||||
"X-App-ApiVersion": PERPLEXITY_API_VERSION,
|
"X-App-ApiVersion": PERPLEXITY_API_VERSION,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseJsonResponse(response: AuthFetchResponse): unknown {
|
||||||
|
try {
|
||||||
|
return JSON.parse(response.bodyText) as unknown;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatHttpFailure(action: string, response: AuthFetchResponse): string {
|
||||||
|
const bodyPreview = response.bodyText.trim().replace(/\s+/g, " ").slice(0, 160);
|
||||||
|
const suffix = bodyPreview ? `: ${bodyPreview}` : "";
|
||||||
|
return `${action} (HTTP ${response.status}${suffix}).`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cookieHeaderFrom(cookies: string[]): string {
|
||||||
|
return cookies.map((cookie) => cookie.split(";")[0]).join("; ");
|
||||||
|
}
|
||||||
|
|
||||||
function extractTokenFromPayload(payload: unknown): string | null {
|
function extractTokenFromPayload(payload: unknown): string | null {
|
||||||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
|
||||||
const obj = payload as Record<string, unknown>;
|
const obj = payload as Record<string, unknown>;
|
||||||
@@ -46,31 +71,23 @@ function extractTokenFromPayload(payload: unknown): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readJsonResponse(response: Response): Promise<unknown> {
|
|
||||||
try {
|
|
||||||
return (await response.json()) as unknown;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loginWithEmailOtp(
|
async function loginWithEmailOtp(
|
||||||
email: string,
|
email: string,
|
||||||
options: AuthenticateOptions,
|
options: AuthenticateOptions,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const signal = options.signal ?? null;
|
const signal = options.signal ?? null;
|
||||||
|
|
||||||
const csrfResponse = await fetch(`${AUTH_BASE_URL}/csrf`, {
|
const csrfResponse = await fetchAuth(`${AUTH_BASE_URL}/csrf`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: buildAuthHeaders(),
|
headers: buildAuthHeaders(),
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!csrfResponse.ok) {
|
if (!csrfResponse.ok) {
|
||||||
throw new Error(`Failed to fetch CSRF token (HTTP ${csrfResponse.status}).`);
|
throw new Error(formatHttpFailure("Failed to fetch CSRF token", csrfResponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
const csrfPayload = (await readJsonResponse(csrfResponse)) as { csrfToken?: unknown } | null;
|
const csrfPayload = parseJsonResponse(csrfResponse) as { csrfToken?: unknown } | null;
|
||||||
const csrfToken =
|
const csrfToken =
|
||||||
csrfPayload && typeof csrfPayload.csrfToken === "string" ? csrfPayload.csrfToken : null;
|
csrfPayload && typeof csrfPayload.csrfToken === "string" ? csrfPayload.csrfToken : null;
|
||||||
|
|
||||||
@@ -78,15 +95,14 @@ async function loginWithEmailOtp(
|
|||||||
throw new Error("CSRF token missing from Perplexity auth response.");
|
throw new Error("CSRF token missing from Perplexity auth response.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const cookies = csrfResponse.headers.getSetCookie?.() ?? [];
|
const cookieHeader = cookieHeaderFrom(csrfResponse.cookies);
|
||||||
const cookieHeader = cookies.map((c) => c.split(";")[0]).join("; ");
|
|
||||||
|
|
||||||
const emailHeaders = buildAuthHeaders(true);
|
const emailHeaders = buildAuthHeaders(true);
|
||||||
if (cookieHeader) {
|
if (cookieHeader) {
|
||||||
emailHeaders.Cookie = cookieHeader;
|
emailHeaders.Cookie = cookieHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailResponse = await fetch(`${AUTH_BASE_URL}/signin-email`, {
|
const emailResponse = await fetchAuth(`${AUTH_BASE_URL}/signin-email`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: emailHeaders,
|
headers: emailHeaders,
|
||||||
body: JSON.stringify({ email, csrfToken }),
|
body: JSON.stringify({ email, csrfToken }),
|
||||||
@@ -94,7 +110,7 @@ async function loginWithEmailOtp(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!emailResponse.ok) {
|
if (!emailResponse.ok) {
|
||||||
throw new Error(`Failed to send OTP email (HTTP ${emailResponse.status}).`);
|
throw new Error(formatHttpFailure("Failed to send OTP email", emailResponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
const otp =
|
const otp =
|
||||||
@@ -113,7 +129,7 @@ async function loginWithEmailOtp(
|
|||||||
otpHeaders.Cookie = cookieHeader;
|
otpHeaders.Cookie = cookieHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
const otpResponse = await fetch(`${AUTH_BASE_URL}/signin-otp`, {
|
const otpResponse = await fetchAuth(`${AUTH_BASE_URL}/signin-otp`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: otpHeaders,
|
headers: otpHeaders,
|
||||||
body: JSON.stringify({ email, otp, csrfToken }),
|
body: JSON.stringify({ email, otp, csrfToken }),
|
||||||
@@ -121,10 +137,10 @@ async function loginWithEmailOtp(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!otpResponse.ok) {
|
if (!otpResponse.ok) {
|
||||||
throw new Error(`OTP verification failed (HTTP ${otpResponse.status}).`);
|
throw new Error(formatHttpFailure("OTP verification failed", otpResponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
const otpPayload = await readJsonResponse(otpResponse);
|
const otpPayload = parseJsonResponse(otpResponse);
|
||||||
const token = extractTokenFromPayload(otpPayload);
|
const token = extractTokenFromPayload(otpPayload);
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error("Perplexity OTP response did not include a token.");
|
throw new Error("Perplexity OTP response did not include a token.");
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
const MAX_BUN_STDOUT = 50 * 1024 * 1024;
|
||||||
|
|
||||||
|
export interface PerplexityFetchOptions {
|
||||||
|
method: "GET" | "POST";
|
||||||
|
headers: Record<string, string>;
|
||||||
|
body?: string;
|
||||||
|
signal?: AbortSignal | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PerplexityFetchResponse {
|
||||||
|
status: number;
|
||||||
|
ok: boolean;
|
||||||
|
bodyText: string;
|
||||||
|
cookies: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perplexity's Cloudflare edge can challenge Node/jiti fetch.
|
||||||
|
* Use native fetch under Bun (tests/direct scripts), otherwise shell out to Bun for its TLS fingerprint.
|
||||||
|
*/
|
||||||
|
export async function perplexityFetchText(
|
||||||
|
url: string,
|
||||||
|
options: PerplexityFetchOptions,
|
||||||
|
): Promise<PerplexityFetchResponse> {
|
||||||
|
if (typeof Bun === "undefined") {
|
||||||
|
return fetchViaBunRuntime(url, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
method: options.method,
|
||||||
|
headers: options.headers,
|
||||||
|
signal: options.signal ?? null,
|
||||||
|
};
|
||||||
|
if (options.body !== undefined) {
|
||||||
|
fetchOptions.body = options.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, fetchOptions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
ok: response.ok,
|
||||||
|
bodyText: await response.text(),
|
||||||
|
cookies: response.headers.getSetCookie?.() ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchViaBunRuntime(
|
||||||
|
url: string,
|
||||||
|
options: PerplexityFetchOptions,
|
||||||
|
): Promise<PerplexityFetchResponse> {
|
||||||
|
const script = `
|
||||||
|
const c = JSON.parse(await Bun.stdin.text());
|
||||||
|
try {
|
||||||
|
const r = await fetch(c.url, {
|
||||||
|
method: c.method,
|
||||||
|
headers: c.headers,
|
||||||
|
body: c.body,
|
||||||
|
});
|
||||||
|
const t = await r.text();
|
||||||
|
process.stdout.write(JSON.stringify({
|
||||||
|
s: r.status,
|
||||||
|
b: t,
|
||||||
|
c: r.headers.getSetCookie?.() ?? [],
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
process.stdout.write(JSON.stringify({ s: 0, b: String(e?.message ?? e), c: [] }));
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { spawn } = await import("node:child_process");
|
||||||
|
|
||||||
|
const stdout = await new Promise<string>((resolve, reject) => {
|
||||||
|
const child = spawn("bun", ["-e", script], {
|
||||||
|
stdio: ["pipe", "pipe", "ignore"],
|
||||||
|
env: { HOME: process.env.HOME, PATH: process.env.PATH },
|
||||||
|
});
|
||||||
|
|
||||||
|
const onAbort = () => child.kill();
|
||||||
|
if (options.signal) {
|
||||||
|
options.signal.addEventListener("abort", onAbort, { once: true });
|
||||||
|
child.on("close", () => options.signal?.removeEventListener("abort", onAbort));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!child.stdin || !child.stdout) {
|
||||||
|
reject(new Error("Failed to open subprocess pipes"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
child.stdin.write(
|
||||||
|
JSON.stringify({
|
||||||
|
url,
|
||||||
|
method: options.method,
|
||||||
|
headers: options.headers,
|
||||||
|
body: options.body,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
child.stdin.end();
|
||||||
|
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
let totalLen = 0;
|
||||||
|
child.stdout.on("data", (chunk: Buffer) => {
|
||||||
|
totalLen += chunk.length;
|
||||||
|
if (totalLen <= MAX_BUN_STDOUT) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
||||||
|
child.on("error", reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
let parsed: unknown;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(stdout);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Bun subprocess returned invalid output: ${stdout.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||||
|
throw new Error("Bun subprocess response is not an object.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = parsed as Record<string, unknown>;
|
||||||
|
if (typeof obj.s !== "number" || typeof obj.b !== "string" || !Array.isArray(obj.c)) {
|
||||||
|
throw new Error("Bun subprocess response missing required fields.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = obj.c.filter((cookie): cookie is string => typeof cookie === "string");
|
||||||
|
return {
|
||||||
|
status: obj.s,
|
||||||
|
ok: obj.s >= 200 && obj.s < 300,
|
||||||
|
bodyText: obj.b,
|
||||||
|
cookies,
|
||||||
|
};
|
||||||
|
}
|
||||||
+16
-104
@@ -3,10 +3,10 @@ import type { SearchResult, StreamEvent, WebResult } from "./types.js";
|
|||||||
import { SearchError } from "./types.js";
|
import { SearchError } from "./types.js";
|
||||||
import { errorMessage } from "../render/util.js";
|
import { errorMessage } from "../render/util.js";
|
||||||
import { PERPLEXITY_USER_AGENT, PERPLEXITY_API_VERSION } from "../constants.js";
|
import { PERPLEXITY_USER_AGENT, PERPLEXITY_API_VERSION } from "../constants.js";
|
||||||
|
import { perplexityFetchText } from "../perplexity-fetch.js";
|
||||||
|
|
||||||
const PERPLEXITY_ENDPOINT = "https://www.perplexity.ai/rest/sse/perplexity_ask";
|
const PERPLEXITY_ENDPOINT = "https://www.perplexity.ai/rest/sse/perplexity_ask";
|
||||||
|
|
||||||
|
|
||||||
function streamFromText(text: string): ReadableStream<Uint8Array> {
|
function streamFromText(text: string): ReadableStream<Uint8Array> {
|
||||||
const bytes = new TextEncoder().encode(text);
|
const bytes = new TextEncoder().encode(text);
|
||||||
return new ReadableStream<Uint8Array>({
|
return new ReadableStream<Uint8Array>({
|
||||||
@@ -17,90 +17,6 @@ function streamFromText(text: string): ReadableStream<Uint8Array> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_BUN_STDOUT = 50 * 1024 * 1024;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a Perplexity request via a Bun subprocess.
|
|
||||||
* Pi loads extensions under Node/jiti whose fetch gets Cloudflare-challenged.
|
|
||||||
* Bun's native fetch has a different TLS fingerprint that passes.
|
|
||||||
*/
|
|
||||||
async function fetchViaBunRuntime(
|
|
||||||
url: string,
|
|
||||||
headers: Record<string, string>,
|
|
||||||
body: string,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
): Promise<{ status: number; bodyText: string }> {
|
|
||||||
const script = `
|
|
||||||
const c = JSON.parse(await Bun.stdin.text());
|
|
||||||
try {
|
|
||||||
const r = await fetch(c.url, { method: "POST", headers: c.headers, body: c.body });
|
|
||||||
const t = await r.text();
|
|
||||||
process.stdout.write(JSON.stringify({ s: r.status, b: t }));
|
|
||||||
} catch (e) {
|
|
||||||
process.stdout.write(JSON.stringify({ s: 0, b: String(e?.message ?? e) }));
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Dynamic import: spawn is only needed under Node/jiti (not Bun),
|
|
||||||
// and Bun's node:child_process polyfill may not export it.
|
|
||||||
const { spawn } = await import("node:child_process");
|
|
||||||
|
|
||||||
const stdout = await new Promise<string>((resolve, reject) => {
|
|
||||||
const child = spawn("bun", ["-e", script], {
|
|
||||||
stdio: ["pipe", "pipe", "ignore"],
|
|
||||||
env: { HOME: process.env.HOME, PATH: process.env.PATH },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (signal) {
|
|
||||||
const onAbort = () => child.kill();
|
|
||||||
signal.addEventListener("abort", onAbort, { once: true });
|
|
||||||
child.on("close", () => signal.removeEventListener("abort", onAbort));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!child.stdin || !child.stdout) {
|
|
||||||
reject(new Error("Failed to open subprocess pipes"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
child.stdin.write(JSON.stringify({ url, headers, body }));
|
|
||||||
child.stdin.end();
|
|
||||||
|
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
let totalLen = 0;
|
|
||||||
child.stdout.on("data", (chunk: Buffer) => {
|
|
||||||
totalLen += chunk.length;
|
|
||||||
if (totalLen <= MAX_BUN_STDOUT) {
|
|
||||||
chunks.push(chunk);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("close", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
||||||
child.on("error", reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
let parsed: unknown;
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(stdout);
|
|
||||||
} catch {
|
|
||||||
throw new Error(`Bun subprocess returned invalid output: ${stdout.slice(0, 200)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!parsed ||
|
|
||||||
typeof parsed !== "object" ||
|
|
||||||
Array.isArray(parsed)
|
|
||||||
) {
|
|
||||||
throw new Error("Bun subprocess response is not an object.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const obj = parsed as Record<string, unknown>;
|
|
||||||
if (typeof obj.s !== "number" || typeof obj.b !== "string") {
|
|
||||||
throw new Error("Bun subprocess response missing required fields.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { status: obj.s, bodyText: obj.b };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchParams {
|
export interface SearchParams {
|
||||||
query: string;
|
query: string;
|
||||||
recency?: "hour" | "day" | "week" | "month" | "year";
|
recency?: "hour" | "day" | "week" | "month" | "year";
|
||||||
@@ -274,20 +190,15 @@ export async function searchPerplexity(
|
|||||||
|
|
||||||
let eventStream: ReadableStream<Uint8Array>;
|
let eventStream: ReadableStream<Uint8Array>;
|
||||||
|
|
||||||
// Bun's native fetch passes Cloudflare; Node/jiti's fetch gets challenged.
|
if (typeof Bun === "undefined") {
|
||||||
// Use native fetch when running under Bun (tests, direct scripts),
|
let response: { status: number; bodyText: string };
|
||||||
// subprocess fallback when running under Node/jiti (pi extension runtime).
|
|
||||||
const useBunSubprocess = typeof Bun === "undefined";
|
|
||||||
|
|
||||||
if (useBunSubprocess) {
|
|
||||||
let bunResult: { status: number; bodyText: string };
|
|
||||||
try {
|
try {
|
||||||
bunResult = await fetchViaBunRuntime(
|
response = await perplexityFetchText(PERPLEXITY_ENDPOINT, {
|
||||||
PERPLEXITY_ENDPOINT,
|
method: "POST",
|
||||||
requestHeaders,
|
headers: requestHeaders,
|
||||||
JSON.stringify(requestBody),
|
body: JSON.stringify(requestBody),
|
||||||
signal,
|
signal: signal ?? null,
|
||||||
);
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
throw new SearchError("NETWORK", "Perplexity request was cancelled.");
|
throw new SearchError("NETWORK", "Perplexity request was cancelled.");
|
||||||
@@ -298,17 +209,18 @@ export async function searchPerplexity(
|
|||||||
`Could not connect to Perplexity. ${errorMessage(error)}`,
|
`Could not connect to Perplexity. ${errorMessage(error)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (bunResult.status === 0) {
|
|
||||||
throw new SearchError("NETWORK", bunResult.bodyText);
|
if (response.status === 0) {
|
||||||
|
throw new SearchError("NETWORK", response.bodyText);
|
||||||
}
|
}
|
||||||
if (bunResult.status !== 200) {
|
if (response.status !== 200) {
|
||||||
throw mapHttpError(bunResult.status);
|
throw mapHttpError(response.status);
|
||||||
}
|
}
|
||||||
if (!bunResult.bodyText) {
|
if (!response.bodyText) {
|
||||||
throw new SearchError("STREAM", "Perplexity returned an empty response.");
|
throw new SearchError("STREAM", "Perplexity returned an empty response.");
|
||||||
}
|
}
|
||||||
|
|
||||||
eventStream = streamFromText(bunResult.bodyText);
|
eventStream = streamFromText(response.bodyText);
|
||||||
} else {
|
} else {
|
||||||
let response: Response;
|
let response: Response;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user