feat(standalone): send session credentials on backend API calls
Backends with real auth (session cookies) need credentials:'include' on the editor's cross-origin fetches — ts-rest clients, project file bytes/upload, drift keepalive, lib item GET/PUT, sync-stack resolve. Cookie-less setups are unaffected (the thin identity headers still ride along and same-site Lax cookies simply don't exist). CDN/static-gallery fetches stay credential-less (wildcard CORS rejects credentialed requests). The example backend's CORS now sends allow-credentials — origin reflection was already exact, so this only lets browsers accept those responses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016TxciQ5VkNYUZesgMSEMUr
This commit is contained in:
parent
252aea245f
commit
840173c978
6 changed files with 29 additions and 3 deletions
|
|
@ -150,7 +150,11 @@ async function project(scope: string): Promise<Project> {
|
|||
async function main(): Promise<void> {
|
||||
const app = Fastify({ logger: true, bodyLimit: 1024 * 1024 * 1024 });
|
||||
await app.register(cors, {
|
||||
// `true` REFLECTS the request origin (never the literal `*`), so it stays
|
||||
// valid for the editor's credentialed fetches; allow-credentials is what
|
||||
// lets the browser accept those responses (cookie-less callers unaffected).
|
||||
origin: CORS_ORIGIN === "*" ? true : CORS_ORIGIN.split(","),
|
||||
credentials: true,
|
||||
});
|
||||
app.get("/health", async () => ({ ok: true }));
|
||||
|
||||
|
|
|
|||
|
|
@ -138,5 +138,11 @@ export function reportDriftBeacon(slug: string, body: DriftReportBody): void {
|
|||
} catch {
|
||||
/* fall through to keepalive fetch */
|
||||
}
|
||||
void fetch(url, { method: "POST", body: blob, keepalive: true }).catch(() => {});
|
||||
// credentials: parity with the beacon path (beacons carry same-site cookies).
|
||||
void fetch(url, {
|
||||
method: "POST",
|
||||
body: blob,
|
||||
keepalive: true,
|
||||
credentials: "include",
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,4 +12,8 @@ import { API_BASE_URL, userSlug } from "./config";
|
|||
export const client = initClient(contract, {
|
||||
baseUrl: API_BASE_URL,
|
||||
baseHeaders: { [USER_HEADER]: userSlug() },
|
||||
// Send the backend's session cookie (same-site, different origin): backends
|
||||
// with real auth resolve the user from it and ignore the thin header. On a
|
||||
// cookie-less setup this changes nothing.
|
||||
credentials: "include",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -80,7 +80,12 @@ function remoteProjectSource(): ProjectSource {
|
|||
return res.body;
|
||||
},
|
||||
async fetchFileBytes(slug, relPath) {
|
||||
const res = await fetch(fileUrl(slug, relPath));
|
||||
// credentials: session-cookie auth (see contract-client.ts). The static
|
||||
// gallery fetches below stay credential-less — a CDN's wildcard CORS
|
||||
// rejects credentialed requests.
|
||||
const res = await fetch(fileUrl(slug, relPath), {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) throw new Error(`download failed (${res.status}): ${relPath}`);
|
||||
return new Uint8Array(await res.arrayBuffer());
|
||||
},
|
||||
|
|
@ -93,6 +98,7 @@ function remoteProjectSource(): ProjectSource {
|
|||
const res = await fetch(`${projectsBase()}/${encodeURIComponent(slug)}/files`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) throw new Error(`upload failed (${res.status}): ${relPath}`);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ export function remoteLibsSource(
|
|||
const client = initClient(contract, {
|
||||
baseUrl: apiBase,
|
||||
baseHeaders: reqHeaders,
|
||||
// Session-cookie auth (backends with real auth ignore the thin headers).
|
||||
credentials: "include",
|
||||
});
|
||||
const enc = encodeURIComponent;
|
||||
|
||||
|
|
@ -62,6 +64,7 @@ export function remoteLibsSource(
|
|||
): Promise<string | null> {
|
||||
const res = await fetch(itemUrl(libId, kind, name), {
|
||||
headers: reqHeaders,
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
return await res.text();
|
||||
|
|
@ -77,6 +80,7 @@ export function remoteLibsSource(
|
|||
method: "PUT",
|
||||
headers: { ...reqHeaders, "Content-Type": "text/plain; charset=utf-8" },
|
||||
body,
|
||||
credentials: "include",
|
||||
});
|
||||
return res.ok;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -95,7 +95,9 @@ async function resolveAndOpen(
|
|||
};
|
||||
const res = await fetch(
|
||||
`${opts.apiBase}/api/scopes/${encodeURIComponent(opts.scope)}/libs/${encodeURIComponent(libId)}/sync-stack`,
|
||||
{ method: "POST", headers },
|
||||
// credentials: session-cookie auth; the layer descriptors this returns keep
|
||||
// their own bearer-token channel (sync-client transport is cookie-free).
|
||||
{ method: "POST", headers, credentials: "include" },
|
||||
);
|
||||
if (!res.ok) throw new Error(`sync-stack resolve failed: HTTP ${res.status}`);
|
||||
const body = (await res.json()) as {
|
||||
|
|
|
|||
Loading…
Reference in a new issue