fix(auth): session secret'i response header'ından (X-Fallback-Cookies) oku

Sorun:
- createEmailPasswordSession response body'sinde 'secret' alanı boş/redacted geliyor
- Cookie'ye boş string yazılıyordu
- requireUser() çağrısında account.get() 401 dönüyor → login'e geri redirect
- Şifre doğru olsa bile bir döngüye giriyor

Çözüm:
- account.createEmailPasswordSession artık awRaw kullanıyor (Response objesi)
- extractSessionFromHeaders helper'ı X-Fallback-Cookies (JSON) veya
  Set-Cookie header'ından gerçek session secret'i parse ediyor
- Bu secret cookie'ye yazılıyor

Browser SDK kaynağında 'cookieFallback' tam olarak bu mantığı kullanıyor —
secret'i header'dan alıp localStorage'a yazıyor.
This commit is contained in:
Ege Can Komur
2026-05-20 02:35:27 +03:00
parent 7eb0c1acc2
commit edd0af76dc
+33 -3
View File
@@ -42,7 +42,7 @@ type FetchOpts = {
formData?: FormData; formData?: FormData;
}; };
async function aw<T>(path: string, opts: FetchOpts = {}): Promise<T> { async function awRaw(path: string, opts: FetchOpts = {}): Promise<Response> {
const url = new URL(ENDPOINT + path); const url = new URL(ENDPOINT + path);
if (opts.query) { if (opts.query) {
for (const [k, v] of Object.entries(opts.query)) { for (const [k, v] of Object.entries(opts.query)) {
@@ -86,7 +86,11 @@ async function aw<T>(path: string, opts: FetchOpts = {}): Promise<T> {
data.type, data.type,
); );
} }
return res;
}
async function aw<T>(path: string, opts: FetchOpts = {}): Promise<T> {
const res = await awRaw(path, opts);
const ct = res.headers.get("content-type") ?? ""; const ct = res.headers.get("content-type") ?? "";
if (ct.includes("application/json")) { if (ct.includes("application/json")) {
return (await res.json()) as T; return (await res.json()) as T;
@@ -94,6 +98,27 @@ async function aw<T>(path: string, opts: FetchOpts = {}): Promise<T> {
return undefined as T; return undefined as T;
} }
function extractSessionFromHeaders(res: Response): string | null {
// Appwrite returns the session secret in X-Fallback-Cookies as JSON,
// or in Set-Cookie header. The response body may have empty/redacted secret.
const fallback = res.headers.get("x-fallback-cookies");
if (fallback) {
try {
const parsed = JSON.parse(fallback) as Record<string, string>;
const first = Object.values(parsed)[0];
if (first) return first;
} catch {
/* ignore */
}
}
const setCookie = res.headers.get("set-cookie");
if (setCookie) {
const m = setCookie.match(/a_session_[^=]+=([^;]+)/);
if (m?.[1]) return decodeURIComponent(m[1]);
}
return null;
}
// ─── Query helpers ─────────────────────────────────────────────── // ─── Query helpers ───────────────────────────────────────────────
export const Q = { export const Q = {
@@ -149,11 +174,16 @@ export interface AwUser {
// ─── Account ───────────────────────────────────────────────────── // ─── Account ─────────────────────────────────────────────────────
export const account = { export const account = {
createEmailPasswordSession(email: string, password: string) { async createEmailPasswordSession(email: string, password: string) {
return aw<AwSession>("/account/sessions/email", { const res = await awRaw("/account/sessions/email", {
method: "POST", method: "POST",
body: { email, password }, body: { email, password },
}); });
const body = (await res.json()) as AwSession;
// Appwrite redacts `secret` in the response body — read it from the
// X-Fallback-Cookies header (or Set-Cookie) where the real secret lives.
const secretFromHeader = extractSessionFromHeaders(res);
return { ...body, secret: secretFromHeader || body.secret };
}, },
get(session: string) { get(session: string) {
return aw<AwUser>("/account", { session }); return aw<AwUser>("/account", { session });