Simplify auth and search client, drop local JWT expiry tracking

Auth:
- Remove jwt.ts — stop decoding JWT exp claims locally
- Simplify OTP login: direct token extraction from verify response,
  remove CookieJar, BFS token extraction, session fallback, CF bypass
- Storage: drop expires field, store token-only; clear on 401/403
- Single auth path: try stored token → macOS app → OTP fallback

Search client:
- Remove Cloudflare subprocess fallback (fetchViaBunRuntime)
- Remove streamFromText, isCloudflareChallenge, BunFetchResult
- Simplify SSE fetch to single fetch() call with abort signal
- Let server validate tokens; clear cache on auth errors

Render:
- Extract shared utilities (asString, truncate) to render/util.ts
- Simplify call.ts and result.ts to import from shared module

Tests:
- Remove jwt.test.ts (module deleted)
- Add otp-flow.test.ts for email OTP authentication
- Simplify login and client tests for reduced code paths

Add debug scripts and plan documents.
This commit is contained in:
Ivan Pereira
2026-02-18 08:03:39 +00:00
parent f42531b07c
commit 501935fe1c
17 changed files with 586 additions and 772 deletions
-42
View File
@@ -1,42 +0,0 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { decodeJwtExpiry, isJwtExpired } from "../../src/auth/jwt.js";
const FIXED_NOW = Date.UTC(2026, 1, 16, 12, 0, 0);
function createJwt(expSeconds: number): string {
const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
const payload = Buffer.from(JSON.stringify({ exp: expSeconds })).toString("base64url");
return `${header}.${payload}.signature`;
}
describe("jwt helpers", () => {
const originalNow = Date.now;
beforeEach(() => {
Date.now = () => FIXED_NOW;
});
afterEach(() => {
Date.now = originalNow;
});
test("decodeJwtExpiry returns expiry in ms with 5 minute safety margin", () => {
const expSeconds = Math.floor((FIXED_NOW + 2 * 60 * 60 * 1000) / 1000);
const token = createJwt(expSeconds);
expect(decodeJwtExpiry(token)).toBe(expSeconds * 1000 - 5 * 60 * 1000);
});
test("decodeJwtExpiry falls back to now + 1h when token is malformed", () => {
expect(decodeJwtExpiry("not-a-jwt")).toBe(FIXED_NOW + 60 * 60 * 1000);
});
test("isJwtExpired honors additional caller-provided buffer", () => {
const expSeconds = Math.floor((FIXED_NOW + 20 * 60 * 1000) / 1000);
const token = createJwt(expSeconds);
expect(isJwtExpired(token)).toBe(false);
expect(isJwtExpired(token, 16 * 60 * 1000)).toBe(true);
});
});
+1 -130
View File
@@ -119,12 +119,11 @@ describe("auth/login", () => {
expect(token).toBe(desktopToken);
});
test("authenticate returns non-expired cached token without desktop or OTP calls", async () => {
test("authenticate returns cached token without desktop or OTP calls", async () => {
const cachedToken = createJwt(Date.now() + 2 * 60 * 60 * 1000);
const loadTokenMock = mock(async () => ({
type: "oauth",
access: cachedToken,
expires: Date.now() + 60 * 60 * 1000,
}) satisfies StoredToken);
const saveTokenMock = mock(async (_token: StoredToken) => undefined);
const clearTokenMock = mock(async () => undefined);
@@ -234,134 +233,6 @@ describe("auth/login", () => {
expect(clearTokenMock).toHaveBeenCalledTimes(0);
});
test("authenticate accepts OTP token from session cookie when body has no token", async () => {
process.env.PI_AUTH_NO_BORROW = "1";
const otpToken = createOpaqueToken();
const loadTokenMock = mock(async () => null);
const saveTokenMock = mock(async (_token: StoredToken) => undefined);
const clearTokenMock = mock(async () => undefined);
mock.module("../../src/auth/storage.js", () => ({
loadToken: loadTokenMock,
saveToken: saveTokenMock,
clearToken: clearTokenMock,
}));
const fetchMock = mock(async (input: RequestInfo | URL) => {
const url = String(input);
if (url.endsWith("/csrf")) {
return new Response(JSON.stringify({ csrfToken: "csrf-token" }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
if (url.endsWith("/signin-email")) {
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
if (url.endsWith("/signin-otp")) {
return new Response(JSON.stringify({ status: "ok" }), {
status: 200,
headers: {
"content-type": "application/json",
"set-cookie": `__Secure-next-auth.session-token=${encodeURIComponent(otpToken)}; Path=/; HttpOnly`,
},
});
}
return new Response("not found", { status: 404 });
});
globalThis.fetch = fetchMock as unknown as typeof fetch;
const { authenticate } = await importLoginModule();
const token = await authenticate({
promptForEmail: async () => "user@example.com",
promptForOtp: async () => "123456",
});
expect(token).toBe(otpToken);
expect(fetchMock).toHaveBeenCalledTimes(3);
expect(saveTokenMock).toHaveBeenCalledTimes(1);
});
test("authenticate falls back to /session when OTP body has no token", async () => {
process.env.PI_AUTH_NO_BORROW = "1";
const otpToken = createOpaqueToken();
const loadTokenMock = mock(async () => null);
const saveTokenMock = mock(async (_token: StoredToken) => undefined);
const clearTokenMock = mock(async () => undefined);
mock.module("../../src/auth/storage.js", () => ({
loadToken: loadTokenMock,
saveToken: saveTokenMock,
clearToken: clearTokenMock,
}));
const fetchMock = mock(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
if (url.endsWith("/csrf")) {
return new Response(JSON.stringify({ csrfToken: "csrf-token" }), {
status: 200,
headers: {
"content-type": "application/json",
"set-cookie": "next-auth.csrf-token=csrf-cookie; Path=/",
},
});
}
if (url.endsWith("/signin-email")) {
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
if (url.endsWith("/signin-otp")) {
return new Response(JSON.stringify({ status: "ok" }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
if (url.endsWith("/session")) {
const cookieHeader = new Headers(init?.headers).get("Cookie") ?? "";
expect(cookieHeader).toContain("next-auth.csrf-token=csrf-cookie");
return new Response(JSON.stringify({ token: otpToken }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
return new Response("not found", { status: 404 });
});
globalThis.fetch = fetchMock as unknown as typeof fetch;
const { authenticate } = await importLoginModule();
const token = await authenticate({
promptForEmail: async () => "user@example.com",
promptForOtp: async () => "123456",
});
expect(token).toBe(otpToken);
expect(fetchMock).toHaveBeenCalledTimes(4);
expect(saveTokenMock).toHaveBeenCalledTimes(1);
expect(clearTokenMock).toHaveBeenCalledTimes(0);
});
test("authenticate throws NO_TOKEN when no cached token and no OTP email input", async () => {
process.env.PI_AUTH_NO_BORROW = "1";
+296
View File
@@ -0,0 +1,296 @@
/**
* OTP login flow tests derived from real captured request/response data.
* See scripts/debug-login-dump.json for the raw fixture.
*/
import { afterEach, describe, expect, mock, test } from "bun:test";
import { AuthError, type StoredToken } from "../../src/search/types.js";
const originalFetch = globalThis.fetch;
const originalBorrow = process.env.PI_AUTH_NO_BORROW;
const originalEmail = process.env.PI_PERPLEXITY_EMAIL;
const originalOtp = process.env.PI_PERPLEXITY_OTP;
// --- Fixtures from real Perplexity responses (scripts/debug-login-dump.json) ---
/** Real JWE token structure: alg=dir, enc=A256GCM — NOT a JWT, opaque to us */
const REAL_JWE_TOKEN =
"eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..AAAAAAAAAAAAAAAA.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.AAAAAAAAAAAAAAAAAAAA";
const CSRF_TOKEN = "0e4f8cc491e3197788492604ad32577f2022747fe30e0f51ba3ba235f07cc9ee";
const TEST_EMAIL = "user@test.com";
const TEST_OTP = "9f3e2-knzol";
// ---
async function importLoginModule() {
return import(`../../src/auth/login.ts?test=${crypto.randomUUID()}`);
}
function restoreEnv(): void {
for (const [key, original] of [
["PI_AUTH_NO_BORROW", originalBorrow],
["PI_PERPLEXITY_EMAIL", originalEmail],
["PI_PERPLEXITY_OTP", originalOtp],
] as const) {
if (original === undefined) {
delete process.env[key];
} else {
process.env[key] = original;
}
}
}
function mockStorage() {
const loadTokenMock = mock(async () => null);
const saveTokenMock = mock(async (_token: StoredToken) => undefined);
const clearTokenMock = mock(async () => undefined);
mock.module("../../src/auth/storage.js", () => ({
loadToken: loadTokenMock,
saveToken: saveTokenMock,
clearToken: clearTokenMock,
}));
return { loadTokenMock, saveTokenMock, clearTokenMock };
}
/** Build a fetch mock that replays real Perplexity response shapes. */
function buildReplayFetchMock(options?: {
/** Override the OTP response body (default: real token+status response) */
otpResponseBody?: unknown;
}) {
const calls: { url: string; init?: RequestInit }[] = [];
const otpBody = options?.otpResponseBody ?? { token: REAL_JWE_TOKEN, status: "success" };
const fetchMock = mock(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
const entry: { url: string; init?: RequestInit } = { url };
if (init !== undefined) entry.init = init;
calls.push(entry);
if (url.endsWith("/csrf")) {
return new Response(JSON.stringify({ csrfToken: CSRF_TOKEN }), {
status: 200,
headers: { "content-type": "application/json; charset=utf-8" },
});
}
if (url.endsWith("/signin-email")) {
return new Response(JSON.stringify({ success: "Email sign in triggered" }), {
status: 200,
headers: { "content-type": "application/json; charset=utf-8" },
});
}
if (url.endsWith("/signin-otp")) {
return new Response(JSON.stringify(otpBody), {
status: 200,
headers: { "content-type": "application/json; charset=utf-8" },
});
}
return new Response("not found", { status: 404 });
});
return { fetchMock, calls };
}
afterEach(() => {
mock.restore();
globalThis.fetch = originalFetch;
restoreEnv();
});
describe("OTP login flow (from real captured responses)", () => {
test("full flow: CSRF → email → OTP, extracts JWE token from response body", async () => {
process.env.PI_AUTH_NO_BORROW = "1";
const { saveTokenMock } = mockStorage();
const { fetchMock } = buildReplayFetchMock();
globalThis.fetch = fetchMock as unknown as typeof fetch;
const { authenticate } = await importLoginModule();
const token = await authenticate({
promptForEmail: async () => TEST_EMAIL,
promptForOtp: async () => TEST_OTP,
});
expect(token).toBe(REAL_JWE_TOKEN);
expect(fetchMock).toHaveBeenCalledTimes(3);
expect(saveTokenMock).toHaveBeenCalledTimes(1);
const saved = saveTokenMock.mock.calls[0]?.[0] as StoredToken;
expect(saved.type).toBe("oauth");
expect(saved.access).toBe(REAL_JWE_TOKEN);
});
test("exactly 3 requests: no /session fallback when token is in body", async () => {
process.env.PI_AUTH_NO_BORROW = "1";
mockStorage();
const { fetchMock, calls } = buildReplayFetchMock();
globalThis.fetch = fetchMock as unknown as typeof fetch;
const { authenticate } = await importLoginModule();
await authenticate({
promptForEmail: async () => TEST_EMAIL,
promptForOtp: async () => TEST_OTP,
});
expect(calls).toHaveLength(3);
expect(calls[0].url).toContain("/csrf");
expect(calls[1].url).toContain("/signin-email");
expect(calls[2].url).toContain("/signin-otp");
});
test("request bodies match expected shape", async () => {
process.env.PI_AUTH_NO_BORROW = "1";
mockStorage();
const { fetchMock, calls } = buildReplayFetchMock();
globalThis.fetch = fetchMock as unknown as typeof fetch;
const { authenticate } = await importLoginModule();
await authenticate({
promptForEmail: async () => TEST_EMAIL,
promptForOtp: async () => TEST_OTP,
});
// CSRF is GET, no body
expect(calls[0].init?.method ?? "GET").toBe("GET");
expect(calls[0].init?.body).toBeFalsy();
// signin-email: POST with email + csrfToken
expect(calls[1].init?.method).toBe("POST");
expect(JSON.parse(String(calls[1].init?.body))).toEqual({
email: TEST_EMAIL,
csrfToken: CSRF_TOKEN,
});
// signin-otp: POST with email + otp + csrfToken
expect(calls[2].init?.method).toBe("POST");
expect(JSON.parse(String(calls[2].init?.body))).toEqual({
email: TEST_EMAIL,
otp: TEST_OTP,
csrfToken: CSRF_TOKEN,
});
});
test("env vars PI_PERPLEXITY_EMAIL and PI_PERPLEXITY_OTP bypass prompts", async () => {
process.env.PI_AUTH_NO_BORROW = "1";
process.env.PI_PERPLEXITY_EMAIL = TEST_EMAIL;
process.env.PI_PERPLEXITY_OTP = TEST_OTP;
mockStorage();
const { fetchMock } = buildReplayFetchMock();
globalThis.fetch = fetchMock as unknown as typeof fetch;
const { authenticate } = await importLoginModule();
const promptForEmail = mock(async () => "should-not-be-called@test.com");
const promptForOtp = mock(async () => "should-not-be-called");
const token = await authenticate({ promptForEmail, promptForOtp });
expect(token).toBe(REAL_JWE_TOKEN);
expect(promptForEmail).toHaveBeenCalledTimes(0);
expect(promptForOtp).toHaveBeenCalledTimes(0);
});
test("throws AuthError NO_TOKEN when email prompt returns undefined", async () => {
process.env.PI_AUTH_NO_BORROW = "1";
mockStorage();
const { authenticate } = await importLoginModule();
let thrown: unknown;
try {
await authenticate({
promptForEmail: async () => undefined,
});
} catch (error) {
thrown = error;
}
expect(thrown).toBeInstanceOf(AuthError);
expect((thrown as AuthError).code).toBe("NO_TOKEN");
});
test("throws AuthError NO_TOKEN when OTP prompt returns undefined", async () => {
process.env.PI_AUTH_NO_BORROW = "1";
mockStorage();
const { fetchMock } = buildReplayFetchMock();
globalThis.fetch = fetchMock as unknown as typeof fetch;
const { authenticate } = await importLoginModule();
let thrown: unknown;
try {
await authenticate({
promptForEmail: async () => TEST_EMAIL,
promptForOtp: async () => undefined,
});
} catch (error) {
thrown = error;
}
expect(thrown).toBeInstanceOf(AuthError);
expect((thrown as AuthError).code).toBe("NO_TOKEN");
});
test("throws when OTP verification returns non-200", async () => {
process.env.PI_AUTH_NO_BORROW = "1";
mockStorage();
const fetchMock = mock(async (input: RequestInfo | URL) => {
const url = String(input);
if (url.endsWith("/csrf")) {
return new Response(JSON.stringify({ csrfToken: CSRF_TOKEN }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
if (url.endsWith("/signin-email")) {
return new Response(JSON.stringify({ success: "Email sign in triggered" }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
if (url.endsWith("/signin-otp")) {
return new Response("Unauthorized", { status: 401 });
}
return new Response("not found", { status: 404 });
});
globalThis.fetch = fetchMock as unknown as typeof fetch;
const { authenticate } = await importLoginModule();
let thrown: unknown;
try {
await authenticate({
promptForEmail: async () => TEST_EMAIL,
promptForOtp: async () => TEST_OTP,
});
} catch (error) {
thrown = error;
}
expect(thrown).toBeInstanceOf(AuthError);
expect((thrown as AuthError).code).toBe("EXTRACTION_FAILED");
expect((thrown as AuthError).message).toContain("OTP verification failed");
});
});