Fix mobile account session selection
This commit is contained in:
+19
-2
@@ -13,6 +13,23 @@ export type SavedAccount = {
|
||||
lastUsedAt: number;
|
||||
};
|
||||
|
||||
function isSavedAccount(value: unknown): value is SavedAccount {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
|
||||
const account = value as Partial<SavedAccount>;
|
||||
return (
|
||||
typeof account.id === "string" &&
|
||||
account.id.length > 0 &&
|
||||
typeof account.instanceUrl === "string" &&
|
||||
account.instanceUrl.length > 0 &&
|
||||
typeof account.userId === "string" &&
|
||||
account.userId.length > 0 &&
|
||||
typeof account.email === "string" &&
|
||||
typeof account.name === "string" &&
|
||||
typeof account.lastUsedAt === "number"
|
||||
);
|
||||
}
|
||||
|
||||
export function buildAccountId(instanceUrl: string, userId: string) {
|
||||
const host = instanceUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
||||
return `${host}::${userId}`;
|
||||
@@ -26,8 +43,8 @@ export async function loadAccounts(): Promise<SavedAccount[]> {
|
||||
const raw = await AsyncStorage.getItem(ACCOUNTS_KEY);
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as SavedAccount[];
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
return Array.isArray(parsed) ? parsed.filter(isSavedAccount) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
+75
-11
@@ -2,6 +2,7 @@ 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>;
|
||||
@@ -9,6 +10,7 @@ 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);
|
||||
@@ -27,16 +29,7 @@ function readSecureStoreValueSync(key: string): string | null {
|
||||
return assembled;
|
||||
}
|
||||
|
||||
/** 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()) return fromClient.trim();
|
||||
|
||||
function readStoredCookie(storagePrefix: string): string | null {
|
||||
const raw = readSecureStoreValueSync(
|
||||
normalizeSecureStoreKey(`${storagePrefix}_cookie`),
|
||||
);
|
||||
@@ -46,14 +39,85 @@ export function getAuthCookie(
|
||||
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) return {};
|
||||
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,
|
||||
|
||||
+14
-3
@@ -66,8 +66,13 @@ export async function readStoredSessionUser(prefix: string): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateAuthStorage(fromPrefix: string, toPrefix: string): Promise<void> {
|
||||
export async function migrateAuthStorage(
|
||||
fromPrefix: string,
|
||||
toPrefix: string,
|
||||
options: { clearSource?: boolean } = {},
|
||||
): Promise<void> {
|
||||
if (fromPrefix === toPrefix) return;
|
||||
const clearSource = options.clearSource ?? true;
|
||||
|
||||
await Promise.all(
|
||||
AUTH_STORAGE_SUFFIXES.map((suffix) =>
|
||||
@@ -75,7 +80,9 @@ export async function migrateAuthStorage(fromPrefix: string, toPrefix: string):
|
||||
),
|
||||
);
|
||||
|
||||
await clearAuthStorage(fromPrefix);
|
||||
if (clearSource) {
|
||||
await clearAuthStorage(fromPrefix);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearAuthStorage(prefix: string): Promise<void> {
|
||||
@@ -124,7 +131,11 @@ export async function finalizeAuthenticatedAccount(input: {
|
||||
? authStoragePrefix(input.activeAccountId)
|
||||
: GUEST_AUTH_STORAGE_PREFIX;
|
||||
|
||||
await migrateAuthStorage(sourcePrefix, targetPrefix);
|
||||
if (sourcePrefix !== targetPrefix) {
|
||||
await clearAuthStorage(targetPrefix);
|
||||
}
|
||||
|
||||
await migrateAuthStorage(sourcePrefix, targetPrefix, { clearSource: false });
|
||||
|
||||
await input.registerAccount({
|
||||
instanceUrl: input.apiUrl,
|
||||
|
||||
Reference in New Issue
Block a user