Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions src/plugins/cross-domain/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,34 +159,34 @@ describe("crossDomainClient", () => {
}

describe("getSessionData", () => {
it("returns null when storage is empty", () => {
it("returns null when storage is empty", async () => {
const actions = getActions();
expect(actions.getSessionData()).toBeNull();
expect(await actions.getSessionData()).toBeNull();
});

it("returns null for empty object in storage", () => {
it("returns null for empty object in storage", async () => {
storage.set(localCacheName, "{}");
const actions = getActions();
expect(actions.getSessionData()).toBeNull();
expect(await actions.getSessionData()).toBeNull();
});

it("returns parsed session data", () => {
it("returns parsed session data", async () => {
const sessionData = { session: { id: "123" }, user: { name: "test" } };
storage.set(localCacheName, JSON.stringify(sessionData));
const actions = getActions();
expect(actions.getSessionData()).toEqual(sessionData);
expect(await actions.getSessionData()).toEqual(sessionData);
});

it("returns null for stored 'null' string", () => {
it("returns null for stored 'null' string", async () => {
storage.set(localCacheName, "null");
const actions = getActions();
expect(actions.getSessionData()).toBeNull();
expect(await actions.getSessionData()).toBeNull();
});

it("returns null for corrupt JSON in storage", () => {
it("returns null for corrupt JSON in storage", async () => {
storage.set(localCacheName, "not-valid-json");
const actions = getActions();
expect(actions.getSessionData()).toBeNull();
expect(await actions.getSessionData()).toBeNull();
});
});

Expand Down
31 changes: 15 additions & 16 deletions src/plugins/cross-domain/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { BetterAuthClientPlugin, ClientStore } from "better-auth/client";
import { parseSetCookieHeader } from "better-auth/cookies";
import type { BetterFetchOption } from "@better-fetch/fetch";
import type { crossDomain } from "./index.js";
import { VERSION } from "../../version.js";

Expand All @@ -10,9 +9,9 @@ interface StoredCookie {
}

type CrossDomainActions = {
getCookie: () => string;
getCookie: () => Promise<string>;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
updateSession: () => void;
getSessionData: () => Record<string, unknown> | null;
getSessionData: () => Promise<Record<string, unknown> | null>;
};

type CrossDomainClientPlugin = Omit<
Expand Down Expand Up @@ -73,8 +72,8 @@ export function getCookie(cookie: string) {
export const crossDomainClient = (
opts: {
storage?: {
setItem: (key: string, value: string) => any;
getItem: (key: string) => string | null;
setItem: (key: string, value: string) => void | Promise<void>;
getItem: (key: string) => Promise<string | null> | string | null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
rg -n 'setItem\s*:|setItem\s*\(' src/plugins/cross-domain

Repository: get-convex/better-auth

Length of output: 1100


🏁 Script executed:

#!/usr/bin/env bash
sed -n '60,280p' src/plugins/cross-domain/client.ts

Repository: get-convex/better-auth

Length of output: 6919


🏁 Script executed:

#!/usr/bin/env bash
rg -n 'CrossDomainClientPlugin|storage\?:|setItem:\s*\(|getItem:\s*\(' src/plugins src | sed -n '1,220p'

Repository: get-convex/better-auth

Length of output: 1962


Type setItem as promise-capable and await the cache writes. getItem already accepts async storage, so setItem should return void | Promise<void> too; otherwise the storage.setItem(...) calls in the success/cache path can drop rejected writes and let onSuccess finish before persistence completes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/plugins/cross-domain/client.ts` at line 76, Update the storage interface
alongside getItem so setItem returns void or Promise<void>, then make the
storage.setItem calls in the success/cache path awaitable and await them before
completing onSuccess. Preserve synchronous storage compatibility while ensuring
rejected async writes propagate instead of being dropped.

};
storagePrefix?: string;
disableCache?: boolean;
Expand All @@ -101,15 +100,15 @@ export const crossDomainClient = (
*
* @example
* ```ts
* const cookie = client.getCookie();
* const cookie = await client.getCookie();
* fetch("https://api.example.com", {
* headers: {
* cookie,
* },
* });
*/
getCookie: () => {
const cookie = storage?.getItem(cookieName);
getCookie: async () => {
const cookie = await storage?.getItem(cookieName);
return getCookie(cookie || "{}");
},
/**
Expand All @@ -131,11 +130,11 @@ export const crossDomainClient = (
*
* @example
* ```ts
* const sessionData = client.getSessionData();
* const sessionData = await client.getSessionData();
* ```
*/
getSessionData: (): Record<string, unknown> | null => {
const sessionData = storage?.getItem(localCacheName);
getSessionData: async (): Promise<Record<string, unknown> | null> => {
const sessionData = await storage?.getItem(localCacheName);
if (!sessionData) return null;
try {
const parsed = JSON.parse(sessionData);
Expand Down Expand Up @@ -166,7 +165,7 @@ export const crossDomainClient = (
"set-better-auth-cookie"
);
if (setCookie) {
const prevCookie = storage.getItem(cookieName);
const prevCookie = await storage.getItem(cookieName);
const toSetCookie = getSetCookie(
setCookie || "",
prevCookie ?? undefined
Comment on lines +168 to 171

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Serialize cookie read-modify-write operations.

The new await storage.getItem(...) introduces a yield between reading and writing. Concurrent success handlers can therefore merge against the same stale cookie and let the last write discard another response’s session or challenge cookie. Serialize these mutations or use an atomic storage operation.

Also applies to: 210-213

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/plugins/cross-domain/client.ts` around lines 168 - 171, Serialize the
cookie read-modify-write sequence around getSetCookie in the success handlers so
concurrent operations cannot read the same stale cookie and overwrite each
other. Apply the same synchronization to both cookie mutation paths, including
the corresponding logic near the second occurrence, while preserving the
existing merge and write behavior.

Expand Down Expand Up @@ -208,7 +207,7 @@ export const crossDomainClient = (
// Previously this unconditionally set cookieName to "{}",
// which wiped the two_factor challenge token needed for
// verifyTotp in cross-domain setups.
const prev = storage.getItem(cookieName);
const prev = await storage.getItem(cookieName);
try {
const parsed = JSON.parse(prev || "{}") as Record<
string,
Expand Down Expand Up @@ -236,11 +235,11 @@ export const crossDomainClient = (
if (!storage) {
return {
url,
options: options as BetterFetchOption,
options: options,
};
}
options = options || {};
const storedCookie = storage.getItem(cookieName);
const storedCookie = await storage.getItem(cookieName);
const cookie = getCookie(storedCookie || "{}");
options.credentials = "omit";
options.headers = {
Expand All @@ -258,7 +257,7 @@ export const crossDomainClient = (
}
return {
url,
options: options as BetterFetchOption,
options: options ,
};
},
},
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/cross-domain/client.types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ it("composes with Better Auth client plugins and infers cross-domain actions", (
plugins: [convexClient(), crossDomainClient({ storage }), adminClient()],
});

expectTypeOf(authClient.getCookie).toEqualTypeOf<() => string>();
expectTypeOf(authClient.getCookie).toEqualTypeOf<() => Promise<string>>();
});