mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-18 11:32:15 -05:00
fix(integrations): migrate cookie integrations to hooks.finally
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"better-auth": patch
|
||||
---
|
||||
|
||||
Migrate `nextCookies`, `sveltekitCookies`, and `tanstackStartCookies` (React and Solid) cookie integrations to use `hooks.finally`. Cookies set by plugins running later in the array are now forwarded to the framework cookie store regardless of plugin declaration order. Closes #8911.
|
||||
@@ -160,4 +160,69 @@ describe("next-js integration", () => {
|
||||
|
||||
expect(session).not.toBeNull();
|
||||
});
|
||||
|
||||
/**
|
||||
* @see https://github.com/better-auth/better-auth/issues/8911
|
||||
*/
|
||||
it("forwards cookies set by plugins ordered after nextCookies()", async () => {
|
||||
const cookiesSet: Array<{ name: string; value: string }> = [];
|
||||
const cookiesMock = vi.fn(async () => ({
|
||||
set: vi.fn((name: string, value: string) => {
|
||||
cookiesSet.push({ name, value });
|
||||
}),
|
||||
delete: vi.fn(),
|
||||
get: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.doMock("next/headers.js", () => ({
|
||||
cookies: cookiesMock,
|
||||
headers: vi.fn(async () => new Headers()),
|
||||
}));
|
||||
|
||||
const [{ getTestInstance }, { nextCookies }, { createAuthMiddleware }] =
|
||||
await Promise.all([
|
||||
import("../test-utils/test-instance"),
|
||||
import("./next-js"),
|
||||
import("@better-auth/core/api"),
|
||||
]);
|
||||
|
||||
const lateCookieName = "late-plugin-cookie";
|
||||
const lateCookieValue = "late-value";
|
||||
const lateCookiePlugin = {
|
||||
id: "late-cookie",
|
||||
hooks: {
|
||||
after: [
|
||||
{
|
||||
matcher: () => true,
|
||||
handler: createAuthMiddleware(async (ctx) => {
|
||||
ctx.setCookie(lateCookieName, lateCookieValue);
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const { auth, testUser } = await getTestInstance({
|
||||
plugins: [nextCookies(), lateCookiePlugin],
|
||||
});
|
||||
|
||||
const signInRes = await auth.api.signInEmail({
|
||||
body: {
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
},
|
||||
returnHeaders: true,
|
||||
});
|
||||
|
||||
const setCookieHeaders = signInRes.headers.getSetCookie();
|
||||
const lateInResponseHeaders = setCookieHeaders.some((c) =>
|
||||
c.includes(lateCookieName),
|
||||
);
|
||||
expect(lateInResponseHeaders).toBe(true);
|
||||
|
||||
const lateInCookieStore = cookiesSet.some(
|
||||
(c) => c.name === lateCookieName && c.value === lateCookieValue,
|
||||
);
|
||||
expect(lateInCookieStore).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,50 +67,48 @@ export const nextCookies = () => {
|
||||
}),
|
||||
},
|
||||
],
|
||||
after: [
|
||||
finally: [
|
||||
{
|
||||
matcher(ctx) {
|
||||
matcher() {
|
||||
return true;
|
||||
},
|
||||
handler: createAuthMiddleware(async (ctx) => {
|
||||
const returned = ctx.context.responseHeaders;
|
||||
if ("_flag" in ctx && ctx._flag === "router") {
|
||||
return;
|
||||
}
|
||||
if (returned instanceof Headers) {
|
||||
const setCookies = returned?.get("set-cookie");
|
||||
if (!setCookies) return;
|
||||
const parsed = parseSetCookieHeader(setCookies);
|
||||
let cookieHelper: Awaited<
|
||||
ReturnType<typeof import("next/headers.js").cookies>
|
||||
>;
|
||||
try {
|
||||
const { cookies } = await import("next/headers.js");
|
||||
cookieHelper = await cookies();
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
(error.message.startsWith(
|
||||
"`cookies` was called outside a request scope.",
|
||||
) ||
|
||||
error.message.includes("Cannot find module"))
|
||||
) {
|
||||
// Monorepo workspaces outside of Next.js hit this path.
|
||||
// @see https://nextjs.org/docs/messages/next-dynamic-api-wrong-context
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
const returned = ctx.context.responseHeaders;
|
||||
if (!(returned instanceof Headers)) return;
|
||||
const setCookies = returned.get("set-cookie");
|
||||
if (!setCookies) return;
|
||||
const parsed = parseSetCookieHeader(setCookies);
|
||||
let cookieHelper: Awaited<
|
||||
ReturnType<typeof import("next/headers.js").cookies>
|
||||
>;
|
||||
try {
|
||||
const { cookies } = await import("next/headers.js");
|
||||
cookieHelper = await cookies();
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
(error.message.startsWith(
|
||||
"`cookies` was called outside a request scope.",
|
||||
) ||
|
||||
error.message.includes("Cannot find module"))
|
||||
) {
|
||||
// Monorepo workspaces outside of Next.js hit this path.
|
||||
// @see https://nextjs.org/docs/messages/next-dynamic-api-wrong-context
|
||||
return;
|
||||
}
|
||||
parsed.forEach((value, key) => {
|
||||
if (!key) return;
|
||||
try {
|
||||
cookieHelper.set(key, value.value, toCookieOptions(value));
|
||||
} catch {
|
||||
// this will fail if the cookie is being set on server component
|
||||
}
|
||||
});
|
||||
return;
|
||||
throw error;
|
||||
}
|
||||
parsed.forEach((value, key) => {
|
||||
if (!key) return;
|
||||
try {
|
||||
cookieHelper.set(key, value.value, toCookieOptions(value));
|
||||
} catch {
|
||||
// this will fail if the cookie is being set on server component
|
||||
}
|
||||
});
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -61,32 +61,30 @@ export const sveltekitCookies = (
|
||||
id: "sveltekit-cookies",
|
||||
version: PACKAGE_VERSION,
|
||||
hooks: {
|
||||
after: [
|
||||
finally: [
|
||||
{
|
||||
matcher() {
|
||||
return true;
|
||||
},
|
||||
handler: createAuthMiddleware(async (ctx) => {
|
||||
const returned = ctx.context.responseHeaders;
|
||||
if ("_flag" in ctx && ctx._flag === "router") {
|
||||
return;
|
||||
}
|
||||
if (returned instanceof Headers) {
|
||||
const setCookies = returned?.get("set-cookie");
|
||||
if (!setCookies) return;
|
||||
const event = getRequestEvent();
|
||||
if (!event) return;
|
||||
const parsed = parseSetCookieHeader(setCookies);
|
||||
|
||||
for (const [name, attributes] of parsed) {
|
||||
try {
|
||||
event.cookies.set(name, attributes.value, {
|
||||
...toCookieOptions(attributes),
|
||||
path: attributes.path || "/",
|
||||
});
|
||||
} catch {
|
||||
// this will avoid any issue related to already streamed response
|
||||
}
|
||||
const returned = ctx.context.responseHeaders;
|
||||
if (!(returned instanceof Headers)) return;
|
||||
const setCookies = returned.get("set-cookie");
|
||||
if (!setCookies) return;
|
||||
const event = getRequestEvent();
|
||||
if (!event) return;
|
||||
const parsed = parseSetCookieHeader(setCookies);
|
||||
for (const [name, attributes] of parsed) {
|
||||
try {
|
||||
event.cookies.set(name, attributes.value, {
|
||||
...toCookieOptions(attributes),
|
||||
path: attributes.path || "/",
|
||||
});
|
||||
} catch {
|
||||
// this will avoid any issue related to already streamed response
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -25,33 +25,29 @@ export const tanstackStartCookies = () => {
|
||||
id: "tanstack-start-cookies-solid",
|
||||
version: PACKAGE_VERSION,
|
||||
hooks: {
|
||||
after: [
|
||||
finally: [
|
||||
{
|
||||
matcher(ctx) {
|
||||
matcher() {
|
||||
return true;
|
||||
},
|
||||
handler: createAuthMiddleware(async (ctx) => {
|
||||
const returned = ctx.context.responseHeaders;
|
||||
if ("_flag" in ctx && ctx._flag === "router") {
|
||||
return;
|
||||
}
|
||||
if (returned instanceof Headers) {
|
||||
const setCookies = returned?.get("set-cookie");
|
||||
if (!setCookies) return;
|
||||
const parsed = parseSetCookieHeader(setCookies);
|
||||
const { setCookie } = await import(
|
||||
"@tanstack/solid-start/server"
|
||||
);
|
||||
parsed.forEach((value, key) => {
|
||||
if (!key) return;
|
||||
try {
|
||||
setCookie(key, value.value, toCookieOptions(value));
|
||||
} catch {
|
||||
// this will fail if the cookie is being set on server component
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
const returned = ctx.context.responseHeaders;
|
||||
if (!(returned instanceof Headers)) return;
|
||||
const setCookies = returned.get("set-cookie");
|
||||
if (!setCookies) return;
|
||||
const parsed = parseSetCookieHeader(setCookies);
|
||||
const { setCookie } = await import("@tanstack/solid-start/server");
|
||||
parsed.forEach((value, key) => {
|
||||
if (!key) return;
|
||||
try {
|
||||
setCookie(key, value.value, toCookieOptions(value));
|
||||
} catch {
|
||||
// this will fail if the cookie is being set on server component
|
||||
}
|
||||
});
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -25,33 +25,29 @@ export const tanstackStartCookies = () => {
|
||||
id: "tanstack-start-cookies",
|
||||
version: PACKAGE_VERSION,
|
||||
hooks: {
|
||||
after: [
|
||||
finally: [
|
||||
{
|
||||
matcher(ctx) {
|
||||
matcher() {
|
||||
return true;
|
||||
},
|
||||
handler: createAuthMiddleware(async (ctx) => {
|
||||
const returned = ctx.context.responseHeaders;
|
||||
if ("_flag" in ctx && ctx._flag === "router") {
|
||||
return;
|
||||
}
|
||||
if (returned instanceof Headers) {
|
||||
const setCookies = returned?.get("set-cookie");
|
||||
if (!setCookies) return;
|
||||
const parsed = parseSetCookieHeader(setCookies);
|
||||
const { setCookie } = await import(
|
||||
"@tanstack/react-start/server"
|
||||
);
|
||||
parsed.forEach((value, key) => {
|
||||
if (!key) return;
|
||||
try {
|
||||
setCookie(key, value.value, toCookieOptions(value));
|
||||
} catch {
|
||||
// this will fail if the cookie is being set on server component
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
const returned = ctx.context.responseHeaders;
|
||||
if (!(returned instanceof Headers)) return;
|
||||
const setCookies = returned.get("set-cookie");
|
||||
if (!setCookies) return;
|
||||
const parsed = parseSetCookieHeader(setCookies);
|
||||
const { setCookie } = await import("@tanstack/react-start/server");
|
||||
parsed.forEach((value, key) => {
|
||||
if (!key) return;
|
||||
try {
|
||||
setCookie(key, value.value, toCookieOptions(value));
|
||||
} catch {
|
||||
// this will fail if the cookie is being set on server component
|
||||
}
|
||||
});
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user