diff --git a/package.json b/package.json index c1604a4..7d0aea1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bool-sdk", - "version": "0.2.0-next.16", + "version": "0.2.0-next.17", "description": "Client SDK for apps built on Bool — gateway data access, end-user auth, the AI battery, the React auth layer, and the local-dev CLI (link, types, deploy).", "type": "module", "main": "./dist/index.js", diff --git a/src/client.test.ts b/src/client.test.ts index 40498c8..ee5ada8 100644 --- a/src/client.test.ts +++ b/src/client.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, test } from "bun:test"; import { createBoolClient, getDefaultBoolClient, + isDeploymentSubdomain, BoolAiError, type BoolClientConfig, } from "./client"; @@ -72,6 +73,18 @@ describe("gateway routing", () => { expect(calls[0]!.url).toBe("/_bool/v1/db/rest/v1/todos?select=*"); }); + test("stays same-origin when the live subdomain differs from the baked slug (renamed app)", async () => { + // The bundle was built with slug "my-app" but the project was later renamed + // to "renamed-app". The proxy resolves the gateway slug from the host, so a + // relative path still reaches the right gateway. Comparing the live host to + // the baked slug (the old behavior) routed cross-origin to /served/my-app, + // which no longer exists → 404 (the "Continue with Google 404s" bug). + (globalThis as any).location = { host: "renamed-app.bool.test" }; + const client = createBoolClient(CONFIG); + await client.db.from("todos").select("*"); + expect(calls[0]!.url).toBe("/_bool/v1/db/rest/v1/todos?select=*"); + }); + test("Storage calls go through the gateway too", async () => { const client = createBoolClient(CONFIG); await client.db.storage.from("uploads").list(); @@ -409,3 +422,33 @@ describe("default client registry", () => { expect(getDefaultBoolClient()).toBe(second); }); }); + +describe("isDeploymentSubdomain", () => { + test("any single-label subdomain of appHost qualifies — not just the baked slug", () => { + expect(isDeploymentSubdomain("my-app.bool.test", "bool.test")).toBe(true); + expect(isDeploymentSubdomain("renamed-app.bool.test", "bool.test")).toBe(true); + expect(isDeploymentSubdomain("a1b2c3.bool.test", "bool.test")).toBe(true); + }); + + test("ignores a :port so it holds in local dev", () => { + expect(isDeploymentSubdomain("my-app.bool.test:3010", "bool.test")).toBe(true); + }); + + test("the bare apex is not a deployment subdomain", () => { + expect(isDeploymentSubdomain("bool.test", "bool.test")).toBe(false); + }); + + test("multi-label hosts don't qualify (proxy only rewrites single-label)", () => { + expect(isDeploymentSubdomain("foo.bar.bool.test", "bool.test")).toBe(false); + }); + + test("a different registrable domain (custom domain / preview sandbox) is cross-origin", () => { + expect(isDeploymentSubdomain("my-app.example.com", "bool.test")).toBe(false); + expect(isDeploymentSubdomain("abc123.vercel.run", "bool.test")).toBe(false); + }); + + test("empty inputs are safe", () => { + expect(isDeploymentSubdomain("", "bool.test")).toBe(false); + expect(isDeploymentSubdomain("my-app.bool.test", "")).toBe(false); + }); +}); diff --git a/src/client.ts b/src/client.ts index e56e603..a6e822b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -30,6 +30,26 @@ const GATEWAY_API = "v1"; // used, and the server never returns a token), so it's never exposed there. const EU_SESSION_KEY = "bool_eu_session_token"; +/** True when `host` is a single-label deployment subdomain of `appHost` (e.g. + * "acme.bool.so" under "bool.so") — the exact shape the platform proxy rewrites + * to /served/