Patch 023 attempted to read process.env.VITE_BRIDGE_TOKEN, but the
vite.config.ts define block uses { 'process.env': {...} } which only
substitutes the literal expression 'process.env' standalone, not when
followed by a member access like 'process.env.X'. So the runtime call
evaluated to undefined and the bridge interceptor never sent a Bearer
header — every /database call got 401.
Switch to Vite's standard pattern: VITE_BRIDGE_TOKEN lives in
apps/client/.env.local (gitignored, must be created locally) and is
auto-exposed via import.meta.env. Verified: the dev server substitutes
it inline at transform time as 'brg_smoketest_admin'.
Patch 025.
116 lines
4.2 KiB
TypeScript
116 lines
4.2 KiB
TypeScript
/**
|
|
* Thin HTTP wrapper for the bridge API (R3.1.a).
|
|
*
|
|
* Why a separate client and not the shared `api` axios instance:
|
|
* The bridge lives at a different origin (VITE_BRIDGE_URL) and uses the
|
|
* DocAdenice JWT forwarded via the Authorization header, whereas `api` targets
|
|
* the Docmost backend at "/api" with cookie-based auth.
|
|
*
|
|
* The JWT is read from the cookie `authToken`. In production the cookie is
|
|
* HttpOnly so JS cannot read it — SSE auth uses the cookie automatically via
|
|
* credentials. For REST calls we rely on the cookie being sent automatically
|
|
* by withCredentials when the bridge is same-site, OR on the server proxying
|
|
* the calls through Docmost (future R3.2). For now we send credentials and
|
|
* leave the Authorization header empty when the token is not readable — the
|
|
* bridge falls back to cookie auth (R2.3b).
|
|
*/
|
|
|
|
import axios, { AxiosInstance } from "axios";
|
|
|
|
/** Resolved bridge base URL: per-instance override > env var > same-origin proxy default.
|
|
*
|
|
* In dev, the Vite server proxies `/bridge/*` to `http://localhost:4000/*` (see vite.config.ts).
|
|
* Same-origin = the auth cookie is sent automatically without CORS gymnastics.
|
|
* In prod, set VITE_BRIDGE_URL at build time to an absolute URL behind your reverse proxy
|
|
* (or keep `/bridge` and proxy server-side).
|
|
*/
|
|
export function resolveBridgeUrl(bridgeUrlOverride?: string | null): string {
|
|
const metaEnv = (import.meta as unknown as { env?: { VITE_BRIDGE_URL?: string } }).env;
|
|
return bridgeUrlOverride ?? metaEnv?.VITE_BRIDGE_URL ?? "/bridge";
|
|
}
|
|
|
|
/** Attempt to read the auth token from JS-accessible cookie or jotai storage. */
|
|
function readTokenFromCookie(): string | null {
|
|
try {
|
|
// authToken is HttpOnly in production; authTokens may be readable.
|
|
const raw = document.cookie
|
|
.split(";")
|
|
.map((c) => c.trim())
|
|
.find((c) => c.startsWith("authTokens="));
|
|
if (raw) {
|
|
const val = decodeURIComponent(raw.slice("authTokens=".length));
|
|
const parsed = JSON.parse(val);
|
|
return typeof parsed?.token === "string" ? parsed.token : null;
|
|
}
|
|
} catch {
|
|
// cookie not accessible or malformed — silently fall through
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Build a one-shot axios instance targeting the resolved bridge URL. */
|
|
export function createBridgeClient(bridgeUrl: string): AxiosInstance {
|
|
const instance = axios.create({
|
|
baseURL: bridgeUrl,
|
|
withCredentials: true,
|
|
timeout: 15_000,
|
|
});
|
|
|
|
// Vite auto-exposes VITE_* vars from apps/client/.env(.local) via
|
|
// import.meta.env. The monorepo-root .env is not picked up automatically
|
|
// for client-side consumption, so the dev token lives in apps/client/.env.local
|
|
// (gitignored).
|
|
const envToken: string | undefined = (
|
|
import.meta as unknown as { env?: { VITE_BRIDGE_TOKEN?: string } }
|
|
).env?.VITE_BRIDGE_TOKEN;
|
|
|
|
instance.interceptors.request.use((config) => {
|
|
// Priority: cookie token (prod) > VITE_BRIDGE_TOKEN env (dev fallback)
|
|
const cookieToken = readTokenFromCookie();
|
|
const token = cookieToken || envToken;
|
|
if (token) {
|
|
config.headers["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
return config;
|
|
});
|
|
|
|
instance.interceptors.response.use(
|
|
(res) => res.data,
|
|
(err) => Promise.reject(err),
|
|
);
|
|
|
|
return instance;
|
|
}
|
|
|
|
/** Singleton map: bridgeUrl -> axios instance (one per origin). */
|
|
const _clients = new Map<string, AxiosInstance>();
|
|
|
|
export function getBridgeClient(bridgeUrl: string): AxiosInstance {
|
|
if (!_clients.has(bridgeUrl)) {
|
|
_clients.set(bridgeUrl, createBridgeClient(bridgeUrl));
|
|
}
|
|
return _clients.get(bridgeUrl)!;
|
|
}
|
|
|
|
/**
|
|
* Patch a single row on the bridge.
|
|
*
|
|
* Why a named helper and not a direct client.patch():
|
|
* Callers (useUpdateRow) would have to resolve the URL themselves. This helper
|
|
* keeps the URL construction in one place and makes the intent explicit.
|
|
*
|
|
* The response is typed as unknown — callers should not assume a specific shape
|
|
* as the bridge returns the updated row in its envelope format.
|
|
*/
|
|
export async function patchRow(
|
|
tableId: string,
|
|
rowId: string,
|
|
payload: { fields: Record<string, unknown> },
|
|
bridgeUrl: string,
|
|
): Promise<unknown> {
|
|
const client = getBridgeClient(bridgeUrl);
|
|
return (client.patch(
|
|
`/api/v1/tables/${tableId}/rows/${rowId}`,
|
|
payload,
|
|
) as unknown) as unknown;
|
|
}
|