From f042fd4d0762a2f8e182c19e2165410192a4a968 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 5 May 2026 16:24:44 -0400 Subject: [PATCH 1/4] support custom auth api domain --- README.md | 17 +++++++++++++++++ eslint.config.js | 2 -- src/client/index.ts | 22 ++++++++++++++-------- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9265436..8cae86b 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,23 @@ authKit.registerRoutes(http); export default http; ``` +## Custom domain + +If you've configured a [custom WorkOS authentication API domain](https://workos.com/docs/custom-domains/auth-api), set the `apiHostname` option (or `WORKOS_API_HOSTNAME` env var) when constructing the AuthKit client. This routes the WorkOS SDK through your domain and updates the JWT issuer and JWKS URLs to match. + +```ts +// convex/auth.ts +export const authKit = new AuthKit(components.workOSAuthKit, { + apiHostname: "auth.example.com", +}); +``` + +Or set it on the deployment: + +```sh +npx convex env set WORKOS_API_HOSTNAME=auth.example.com +``` + ## Usage User create/update/delete in WorkOS will be automatically synced by the diff --git a/eslint.config.js b/eslint.config.js index 69c4c5a..ea86ee0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,8 +10,6 @@ export default [ ignores: [ "dist/**", "example/dist/**", - "*.config.{js,mjs,cjs,ts,tsx}", - "example/**/*.config.{js,mjs,cjs,ts,tsx}", "**/_generated/", "initTemplate.mjs", ], diff --git a/src/client/index.ts b/src/client/index.ts index fe1fcf2..37cf559 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -6,7 +6,6 @@ import { type HttpRouter, createFunctionHandle, httpActionGeneric, - internalActionGeneric, internalMutationGeneric, } from "convex/server"; import type { RunQueryCtx } from "./types.js"; @@ -31,6 +30,7 @@ type Options = { authFunctions?: AuthFunctions; clientId?: string; apiKey?: string; + apiHostname?: string; webhookSecret?: string; webhookPath?: string; additionalEventTypes?: WorkOSEvent["event"][]; @@ -95,27 +95,33 @@ export class AuthKit { apiKey, webhookSecret, actionSecret: options?.actionSecret ?? process.env.WORKOS_ACTION_SECRET, + apiHostname: options?.apiHostname ?? process.env.WORKOS_API_HOSTNAME, webhookPath: options?.webhookPath ?? "/workos/webhook", }; - this.workos = new WorkOS(this.config.apiKey); + this.workos = new WorkOS(this.config.apiKey, { + clientId: this.config.clientId, + apiHostname: this.config.apiHostname, + }); } - getAuthConfigProviders = () => - [ + getAuthConfigProviders = () => { + const apiBaseUrl = `https://${this.config.apiHostname ?? "api.workos.com"}`; + return [ { type: "customJwt", - issuer: `https://api.workos.com/`, + issuer: apiBaseUrl, algorithm: "RS256", - jwks: `https://api.workos.com/sso/jwks/${this.config.clientId}`, + jwks: `${apiBaseUrl}/sso/jwks/${this.config.clientId}`, applicationID: this.config.clientId, }, { type: "customJwt", - issuer: `https://api.workos.com/user_management/${this.config.clientId}`, + issuer: `${apiBaseUrl}/user_management/${this.config.clientId}`, algorithm: "RS256", - jwks: `https://api.workos.com/sso/jwks/${this.config.clientId}`, + jwks: `${apiBaseUrl}/sso/jwks/${this.config.clientId}`, }, ] satisfies AuthConfig["providers"]; + }; async getAuthUser(ctx: RunQueryCtx) { const identity = await ctx.auth.getUserIdentity(); From 83d57fb0992d9e1c1021f855363c058b24c2a08b Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 5 May 2026 16:27:32 -0400 Subject: [PATCH 2/4] preserve issuer trailing slash --- src/client/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/index.ts b/src/client/index.ts index 37cf559..f71c3b5 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -109,7 +109,7 @@ export class AuthKit { return [ { type: "customJwt", - issuer: apiBaseUrl, + issuer: `${apiBaseUrl}/`, algorithm: "RS256", jwks: `${apiBaseUrl}/sso/jwks/${this.config.clientId}`, applicationID: this.config.clientId, From b29b3198f193454dbf57bce6555c92a9dcecb908 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 5 May 2026 19:02:33 -0400 Subject: [PATCH 3/4] avoid env var usage, add tests --- README.md | 8 +-- src/client/index.test.ts | 109 +++++++++++++++++++++++++++++++++++++++ src/client/index.ts | 1 - 3 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 src/client/index.test.ts diff --git a/README.md b/README.md index 8cae86b..6be76d7 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ export default http; ## Custom domain -If you've configured a [custom WorkOS authentication API domain](https://workos.com/docs/custom-domains/auth-api), set the `apiHostname` option (or `WORKOS_API_HOSTNAME` env var) when constructing the AuthKit client. This routes the WorkOS SDK through your domain and updates the JWT issuer and JWKS URLs to match. +If you've configured a [custom WorkOS authentication API domain](https://workos.com/docs/custom-domains/auth-api), pass it as `apiHostname` when constructing the AuthKit client. This routes the WorkOS SDK through your domain and updates the JWT issuer and JWKS URLs to match. ```ts // convex/auth.ts @@ -99,12 +99,6 @@ export const authKit = new AuthKit(components.workOSAuthKit, { }); ``` -Or set it on the deployment: - -```sh -npx convex env set WORKOS_API_HOSTNAME=auth.example.com -``` - ## Usage User create/update/delete in WorkOS will be automatically synced by the diff --git a/src/client/index.test.ts b/src/client/index.test.ts new file mode 100644 index 0000000..6a6b542 --- /dev/null +++ b/src/client/index.test.ts @@ -0,0 +1,109 @@ +/// +import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"; +import { WorkOS } from "@workos-inc/node"; +import { AuthKit } from "./index.js"; +import type { ComponentApi } from "../component/_generated/component.js"; + +vi.mock("@workos-inc/node", () => { + return { + WorkOS: vi.fn().mockImplementation(function () { + return {}; + }), + }; +}); + +const requiredEnv = { + WORKOS_CLIENT_ID: "client_test", + WORKOS_API_KEY: "sk_test", + WORKOS_WEBHOOK_SECRET: "whsec_test", +}; + +const fakeComponent = {} as ComponentApi; + +describe("AuthKit constructor", () => { + beforeEach(() => { + for (const [k, v] of Object.entries(requiredEnv)) { + process.env[k] = v; + } + }); + + afterEach(() => { + for (const k of Object.keys(requiredEnv)) { + delete process.env[k]; + } + vi.clearAllMocks(); + }); + + describe("apiHostname", () => { + test("forwards apiHostname option to the WorkOS SDK", () => { + new AuthKit(fakeComponent, { apiHostname: "auth.example.com" }); + expect(vi.mocked(WorkOS)).toHaveBeenCalledWith( + "sk_test", + expect.objectContaining({ apiHostname: "auth.example.com" }) + ); + }); + + test("forwards undefined when option is not set", () => { + new AuthKit(fakeComponent); + expect(vi.mocked(WorkOS)).toHaveBeenCalledWith( + "sk_test", + expect.objectContaining({ apiHostname: undefined }) + ); + }); + }); + + test("clientId is forwarded to the WorkOS SDK", () => { + new AuthKit(fakeComponent); + expect(vi.mocked(WorkOS)).toHaveBeenCalledWith( + "sk_test", + expect.objectContaining({ clientId: "client_test" }) + ); + }); +}); + +describe("AuthKit.getAuthConfigProviders", () => { + beforeEach(() => { + for (const [k, v] of Object.entries(requiredEnv)) { + process.env[k] = v; + } + }); + + afterEach(() => { + for (const k of Object.keys(requiredEnv)) { + delete process.env[k]; + } + vi.clearAllMocks(); + }); + + test("falls back to api.workos.com when no custom hostname is set", () => { + const authKit = new AuthKit(fakeComponent); + const providers = authKit.getAuthConfigProviders(); + expect(providers[0].issuer).toBe("https://api.workos.com/"); + expect(providers[0].jwks).toBe( + "https://api.workos.com/sso/jwks/client_test" + ); + expect(providers[1].issuer).toBe( + "https://api.workos.com/user_management/client_test" + ); + expect(providers[1].jwks).toBe( + "https://api.workos.com/sso/jwks/client_test" + ); + }); + + test("uses custom hostname from option in both providers", () => { + const authKit = new AuthKit(fakeComponent, { + apiHostname: "auth.example.com", + }); + const providers = authKit.getAuthConfigProviders(); + expect(providers[0].issuer).toBe("https://auth.example.com/"); + expect(providers[0].jwks).toBe( + "https://auth.example.com/sso/jwks/client_test" + ); + expect(providers[1].issuer).toBe( + "https://auth.example.com/user_management/client_test" + ); + expect(providers[1].jwks).toBe( + "https://auth.example.com/sso/jwks/client_test" + ); + }); +}); diff --git a/src/client/index.ts b/src/client/index.ts index f71c3b5..5c76a1d 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -95,7 +95,6 @@ export class AuthKit { apiKey, webhookSecret, actionSecret: options?.actionSecret ?? process.env.WORKOS_ACTION_SECRET, - apiHostname: options?.apiHostname ?? process.env.WORKOS_API_HOSTNAME, webhookPath: options?.webhookPath ?? "/workos/webhook", }; this.workos = new WorkOS(this.config.apiKey, { From 99d6a561c1fddd252e4c1d3031da6dd349947694 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 5 May 2026 19:31:46 -0400 Subject: [PATCH 4/4] only use apiHostname for issuer --- src/client/index.test.ts | 6 +++--- src/client/index.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/index.test.ts b/src/client/index.test.ts index 6a6b542..5da4672 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -90,20 +90,20 @@ describe("AuthKit.getAuthConfigProviders", () => { ); }); - test("uses custom hostname from option in both providers", () => { + test("custom hostname rewrites issuer but not jwks", () => { const authKit = new AuthKit(fakeComponent, { apiHostname: "auth.example.com", }); const providers = authKit.getAuthConfigProviders(); expect(providers[0].issuer).toBe("https://auth.example.com/"); expect(providers[0].jwks).toBe( - "https://auth.example.com/sso/jwks/client_test" + "https://api.workos.com/sso/jwks/client_test" ); expect(providers[1].issuer).toBe( "https://auth.example.com/user_management/client_test" ); expect(providers[1].jwks).toBe( - "https://auth.example.com/sso/jwks/client_test" + "https://api.workos.com/sso/jwks/client_test" ); }); }); diff --git a/src/client/index.ts b/src/client/index.ts index 5c76a1d..9d62c0e 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -110,14 +110,14 @@ export class AuthKit { type: "customJwt", issuer: `${apiBaseUrl}/`, algorithm: "RS256", - jwks: `${apiBaseUrl}/sso/jwks/${this.config.clientId}`, + jwks: `https://api.workos.com/sso/jwks/${this.config.clientId}`, applicationID: this.config.clientId, }, { type: "customJwt", issuer: `${apiBaseUrl}/user_management/${this.config.clientId}`, algorithm: "RS256", - jwks: `${apiBaseUrl}/sso/jwks/${this.config.clientId}`, + jwks: `https://api.workos.com/sso/jwks/${this.config.clientId}`, }, ] satisfies AuthConfig["providers"]; };