feat(web): add dispatchFor transport helper for same-origin proxy
This commit is contained in:
36
packages/web/src/lib/dispatch.ts
Normal file
36
packages/web/src/lib/dispatch.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { httpFetch } from "@/lib/http";
|
||||
import { isTauri } from "@/lib/platform";
|
||||
|
||||
/** Same-origin path prefix the web build issues all Honcho calls through. */
|
||||
export const API_PREFIX = "/api";
|
||||
/** Request header naming the real Honcho upstream for the proxy to forward to. */
|
||||
export const UPSTREAM_HEADER = "X-Honcho-Upstream";
|
||||
/** Response header the proxy sets on its OWN refusals (so they aren't read as upstream auth). */
|
||||
export const PROXY_REJECT_HEADER = "X-Honcho-Proxy-Reject";
|
||||
|
||||
export interface Dispatch {
|
||||
baseUrl: string;
|
||||
headers: Record<string, string>;
|
||||
fetch: typeof globalThis.fetch;
|
||||
}
|
||||
|
||||
function normalizeUpstream(url: string): string {
|
||||
return url.trim().replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve how to issue a request for an instance.
|
||||
* - Web: same-origin `/api` + `X-Honcho-Upstream` header (proxy forwards server-side, no CORS).
|
||||
* - Tauri: the absolute instance URL via reqwest (no browser same-origin policy).
|
||||
*/
|
||||
export function dispatchFor(instance: { baseUrl: string; token?: string }): Dispatch {
|
||||
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
||||
if (instance.token) headers.Authorization = `Bearer ${instance.token}`;
|
||||
|
||||
if (isTauri()) {
|
||||
return { baseUrl: instance.baseUrl, headers, fetch: httpFetch };
|
||||
}
|
||||
|
||||
headers[UPSTREAM_HEADER] = normalizeUpstream(instance.baseUrl);
|
||||
return { baseUrl: API_PREFIX, headers, fetch: httpFetch };
|
||||
}
|
||||
40
packages/web/src/test/dispatch.test.ts
Normal file
40
packages/web/src/test/dispatch.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { mockIsTauri } = vi.hoisted(() => ({ mockIsTauri: vi.fn() }));
|
||||
vi.mock("@/lib/platform", () => ({ isTauri: () => mockIsTauri() }));
|
||||
|
||||
import { API_PREFIX, dispatchFor, PROXY_REJECT_HEADER, UPSTREAM_HEADER } from "@/lib/dispatch";
|
||||
|
||||
afterEach(() => mockIsTauri.mockReset());
|
||||
|
||||
describe("dispatchFor — web mode", () => {
|
||||
it("targets the /api prefix and carries the upstream header", () => {
|
||||
mockIsTauri.mockReturnValue(false);
|
||||
const d = dispatchFor({ baseUrl: "https://honcho.example.net/", token: "" });
|
||||
expect(d.baseUrl).toBe(API_PREFIX);
|
||||
expect(d.headers[UPSTREAM_HEADER]).toBe("https://honcho.example.net");
|
||||
expect(d.headers.Authorization).toBeUndefined();
|
||||
});
|
||||
|
||||
it("adds Authorization only when a token is present", () => {
|
||||
mockIsTauri.mockReturnValue(false);
|
||||
const d = dispatchFor({ baseUrl: "https://honcho.example.net", token: "sk-1" });
|
||||
expect(d.headers.Authorization).toBe("Bearer sk-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("dispatchFor — tauri mode", () => {
|
||||
it("targets the absolute URL with no upstream header", () => {
|
||||
mockIsTauri.mockReturnValue(true);
|
||||
const d = dispatchFor({ baseUrl: "https://honcho.example.net", token: "sk-1" });
|
||||
expect(d.baseUrl).toBe("https://honcho.example.net");
|
||||
expect(d.headers[UPSTREAM_HEADER]).toBeUndefined();
|
||||
expect(d.headers.Authorization).toBe("Bearer sk-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("proxy reject header constant", () => {
|
||||
it("is the agreed sentinel name", () => {
|
||||
expect(PROXY_REJECT_HEADER).toBe("X-Honcho-Proxy-Reject");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user