From d384ee684e3a47f19112acea9a1ec34d56801a7d Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Fri, 19 Sep 2025 15:54:11 -0700 Subject: [PATCH] fix(api-key): calling client on server side (#4777) --- e2e/smoke/test/ssr.ts | 76 +++++++++++++++++++ .../better-auth/src/api/to-auth-endpoints.ts | 13 +++- 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 e2e/smoke/test/ssr.ts diff --git a/e2e/smoke/test/ssr.ts b/e2e/smoke/test/ssr.ts new file mode 100644 index 0000000000..8e7061f908 --- /dev/null +++ b/e2e/smoke/test/ssr.ts @@ -0,0 +1,76 @@ +import { betterAuth } from "better-auth"; +import { describe, test } from "node:test"; +import { DatabaseSync } from "node:sqlite"; +import { apiKey } from "better-auth/plugins"; +import { createAuthClient } from "better-auth/client"; +import { apiKeyClient } from "better-auth/client/plugins"; +import { getMigrations } from "better-auth/db"; +import assert from "node:assert/strict"; + +describe("server side client", () => { + test("can use api key on server side", async () => { + const database = new DatabaseSync(":memory:"); + const auth = betterAuth({ + baseURL: "http://localhost:3000", + database, + socialProviders: { + github: { + clientId: process.env.GITHUB_CLIENT_ID as string, + clientSecret: process.env.GITHUB_CLIENT_SECRET as string, + }, + }, + emailAndPassword: { + enabled: true, + }, + plugins: [ + apiKey({ + rateLimit: { + enabled: false, + }, + }), + ], + }); + + const { runMigrations } = await getMigrations(auth.options); + await runMigrations(); + + const authClient: ReturnType = createAuthClient({ + baseURL: "http://localhost:3000", + plugins: [apiKeyClient()], + fetchOptions: { + customFetchImpl: async (url, init) => { + return auth.handler(new Request(url, init)); + }, + }, + }); + + const { user } = await auth.api.signUpEmail({ + body: { + name: "Alex", + email: "alex@test.com", + password: "hello123", + }, + }); + + const { key, id, userId } = await auth.api.createApiKey({ + body: { + name: "my-api-key", + userId: user.id, + }, + }); + + const ret = database.prepare(`SELECT * FROM apiKey;`).all(); + assert.equal(ret.length, 1); + const first = ret.at(-1)!; + assert.equal(first.id, id); + assert.equal(first.userId, userId); + + await authClient.getSession({ + fetchOptions: { + headers: { + "x-api-key": key, + }, + }, + }); + }); +}); diff --git a/packages/better-auth/src/api/to-auth-endpoints.ts b/packages/better-auth/src/api/to-auth-endpoints.ts index d8db975af6..381188f9b4 100644 --- a/packages/better-auth/src/api/to-auth-endpoints.ts +++ b/packages/better-auth/src/api/to-auth-endpoints.ts @@ -1,9 +1,9 @@ import { APIError, - toResponse, type EndpointContext, type EndpointOptions, type InputContext, + toResponse, } from "better-call"; import type { AuthEndpoint, AuthMiddleware } from "./call"; import type { AuthContext, HookEndpointContext } from "../types"; @@ -81,7 +81,16 @@ export function toAuthEndpoints>( internalContext = defuReplaceArrays(rest, internalContext); } else if (before) { /* Return before hook response if it's anything other than a context return */ - return before; + return context?.asResponse + ? toResponse(before, { + headers: context?.headers, + }) + : context?.returnHeaders + ? { + headers: context?.headers, + response: before, + } + : before; } internalContext.asResponse = false;