From 2d43c996e7526204139a1c1a616f42a0ba4b97c3 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Thu, 10 Sep 2026 20:48:40 +0100 Subject: [PATCH] fix(auth): derive the Google hd hint from an address-only allow list `primaryWorkspaceDomain()` reads `allowList().domains[0]`, so it returns undefined whenever ALLOWED_SIGN_IN holds only addresses. `auth.ts` then omits `google.hd`, and a solo self-hoster on `me@acme.com` gets the full account chooser while one on `acme.com` does not, despite both naming one workspace. Derive the domain from the addresses when no bare domain is configured, and only when every address shares one: `hd` narrows the chooser to a single domain, so sending it for one of several would hide the rest. `isWorkspaceEmail()` is untouched, so who may sign in does not change. `workspaceDomains()` is untouched deliberately: it drives the sync decision about which side of a thread is internal, and widening it would file a colleague as a lead. Co-Authored-By: Claude Opus 5 --- packages/auth/src/workspace.ts | 14 ++++++++- packages/auth/test/workspace.spec.ts | 44 ++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 packages/auth/test/workspace.spec.ts diff --git a/packages/auth/src/workspace.ts b/packages/auth/src/workspace.ts index d94bc1c20..a6575b7ac 100644 --- a/packages/auth/src/workspace.ts +++ b/packages/auth/src/workspace.ts @@ -33,7 +33,19 @@ export function workspaceDomains(): readonly string[] { } export function primaryWorkspaceDomain(): string | undefined { - return allowList().domains[0]; + const { domains, addresses } = allowList(); + + if (domains[0]) return domains[0]; + + // An address-only allow list still names a workspace domain, and a solo + // self-hoster on `me@acme.com` wants the same account chooser as one on + // `acme.com`. Only when every allowed address shares a domain: `hd` narrows + // the chooser to one, so sending it for one of several would hide the rest. + const hosts = new Set( + addresses.map((address) => address.split("@")[1]).filter(Boolean), + ); + + return hosts.size === 1 ? [...hosts][0] : undefined; } export function hasSignInAllowList(): boolean { diff --git a/packages/auth/test/workspace.spec.ts b/packages/auth/test/workspace.spec.ts new file mode 100644 index 000000000..9ba1c7d73 --- /dev/null +++ b/packages/auth/test/workspace.spec.ts @@ -0,0 +1,44 @@ +import { afterEach, describe, expect, it } from "bun:test"; +import { isWorkspaceEmail, primaryWorkspaceDomain } from "../src/workspace"; + +const original = process.env.ALLOWED_SIGN_IN; + +afterEach(() => { + if (original === undefined) delete process.env.ALLOWED_SIGN_IN; + else process.env.ALLOWED_SIGN_IN = original; +}); + +describe("the domain behind the account chooser", () => { + it("is the configured domain when one is configured", () => { + process.env.ALLOWED_SIGN_IN = "acme.com"; + expect(primaryWorkspaceDomain()).toBe("acme.com"); + }); + + it("is derived from a single address, so a solo self-hoster gets the hint too", () => { + process.env.ALLOWED_SIGN_IN = "rep@acme.com"; + expect(primaryWorkspaceDomain()).toBe("acme.com"); + }); + + it("is withheld when the addresses span more than one domain", () => { + // `hd` narrows the chooser to one domain, so sending it for one of two + // would hide the other rather than help. + process.env.ALLOWED_SIGN_IN = "rep@acme.com,other@beta.com"; + expect(primaryWorkspaceDomain()).toBeUndefined(); + }); + + it("still prefers a configured domain over an address", () => { + process.env.ALLOWED_SIGN_IN = "rep@beta.com,acme.com"; + expect(primaryWorkspaceDomain()).toBe("acme.com"); + }); + + it("is nothing when the list is empty, which fails closed", () => { + process.env.ALLOWED_SIGN_IN = ""; + expect(primaryWorkspaceDomain()).toBeUndefined(); + }); + + it("does not widen who may sign in", () => { + process.env.ALLOWED_SIGN_IN = "rep@acme.com"; + expect(isWorkspaceEmail("rep@acme.com")).toBe(true); + expect(isWorkspaceEmail("someone-else@acme.com")).toBe(false); + }); +});