Files
beenvoice-app/lib/auth-cookie.ts
T

128 lines
3.7 KiB
TypeScript

import { getCookie as serializeStoredCookies } from "@better-auth/expo/client";
import * as SecureStore from "expo-secure-store";
import type { createAuthClient } from "better-auth/react";
import { GUEST_AUTH_STORAGE_PREFIX } from "@/lib/auth-storage";
import { normalizeSecureStoreKey } from "@/lib/secure-store-keys";
type AuthClient = ReturnType<typeof createAuthClient>;
const CHUNK_MARKER = "\u0001ba-chunks:";
const SESSION_TOKEN_COOKIE_PART =
/(?:^|;\s*)(?:__Secure-)?[^=]*session_token=([^;]+)/;
const AUTH_COOKIE_DEBUG = process.env.EXPO_PUBLIC_AUTH_COOKIE_DEBUG === "1";
function readSecureStoreValueSync(key: string): string | null {
const value = SecureStore.getItem(key);
if (value == null) return null;
if (!value.startsWith(CHUNK_MARKER)) return value;
const count = Number(value.slice(CHUNK_MARKER.length));
if (!Number.isInteger(count) || count < 1) return null;
let assembled = "";
for (let index = 0; index < count; index += 1) {
const chunk = SecureStore.getItem(`${key}.${index}`);
if (chunk == null) return null;
assembled += chunk;
}
return assembled;
}
function readStoredCookie(storagePrefix: string): string | null {
const raw = readSecureStoreValueSync(
normalizeSecureStoreKey(`${storagePrefix}_cookie`),
);
if (!raw || raw === "{}") return null;
const cookie = serializeStoredCookies(raw);
return cookie.trim() || null;
}
function cookieNames(cookie: string): string[] {
return cookie
.split(";")
.map((part) => part.trim().split("=", 1)[0])
.filter(Boolean);
}
/** Read session cookie string for tRPC requests (Expo client plugin + SecureStore fallback). */
export function getAuthCookie(
authClient: AuthClient,
storagePrefix: string,
): string | null {
const fromClient = (
authClient as AuthClient & { getCookie?: () => string }
).getCookie?.();
if (fromClient?.trim()) {
const cookie = fromClient.trim();
if (AUTH_COOKIE_DEBUG) {
console.info("[auth-cookie] using client cookie", {
storagePrefix,
length: cookie.length,
names: cookieNames(cookie),
});
}
return cookie;
}
const fromPrefix = readStoredCookie(storagePrefix);
if (fromPrefix) {
if (AUTH_COOKIE_DEBUG) {
console.info("[auth-cookie] using stored cookie", {
storagePrefix,
length: fromPrefix.length,
names: cookieNames(fromPrefix),
});
}
return fromPrefix;
}
const fromGuest =
storagePrefix === GUEST_AUTH_STORAGE_PREFIX
? null
: readStoredCookie(GUEST_AUTH_STORAGE_PREFIX);
if (AUTH_COOKIE_DEBUG) {
console.info("[auth-cookie] resolved tRPC cookie", {
storagePrefix,
fallbackPrefix:
fromGuest && storagePrefix !== GUEST_AUTH_STORAGE_PREFIX
? GUEST_AUTH_STORAGE_PREFIX
: null,
hasCookie: Boolean(fromGuest),
length: fromGuest?.length ?? 0,
names: fromGuest ? cookieNames(fromGuest) : [],
});
}
return fromGuest;
}
export function getAuthCookieHeaders(
authClient: AuthClient,
storagePrefix: string,
): Record<string, string> {
const cookie = getAuthCookie(authClient, storagePrefix);
if (!cookie) {
if (AUTH_COOKIE_DEBUG) {
console.info("[auth-cookie] no tRPC auth cookie", { storagePrefix });
}
return {};
}
const sessionToken = cookie.match(SESSION_TOKEN_COOKIE_PART)?.[1];
if (AUTH_COOKIE_DEBUG) {
console.info("[auth-cookie] sending tRPC auth headers", {
storagePrefix,
cookieLength: cookie.length,
cookieNames: cookieNames(cookie),
hasSessionTokenHeader: Boolean(sessionToken),
});
}
return {
cookie,
Cookie: cookie,
"x-beenvoice-auth-cookie": cookie,
...(sessionToken ? { "x-beenvoice-session-token": sessionToken } : {}),
};
}