Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
43 changes: 43 additions & 0 deletions src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, test } from "bun:test";
import {
createBoolClient,
getDefaultBoolClient,
isDeploymentSubdomain,
BoolAiError,
type BoolClientConfig,
} from "./client";
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
});
});
39 changes: 32 additions & 7 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<label>/… keyed on the request host. Mirrors `deploymentSlugFromHost`
* in the platform proxy: the label must be a valid slug (lowercase letters,
* digits, dashes) with no further dots, so multi-label hosts and the bare apex
* don't qualify. The port is ignored so it holds in local dev too.
*
* Deliberately does NOT compare the label to the baked `VITE_BOOL_SLUG`: the
* proxy resolves the slug from the live host, so any deployment subdomain is
* same-origin. Tying it to the baked slug is what made a renamed app route its
* gateway calls to the old slug cross-origin and 404. */
export function isDeploymentSubdomain(host: string, appHost: string): boolean {
if (!host || !appHost) return false;
const h = host.split(":")[0]!; // drop any :port
const suffix = "." + appHost;
if (!h.endsWith(suffix)) return false;
const label = h.slice(0, -suffix.length);
return /^[a-z0-9-]+$/.test(label);
}

/** Everything the client needs to reach its Bool. Injected by Bool at boot
* (the app's `src/lib/supabase.ts` passes these from `import.meta.env`). */
export type BoolClientConfig = {
Expand Down Expand Up @@ -218,17 +238,22 @@ export function createBoolClient(config: BoolClientConfig): BoolClient {
}

// Where the gateway lives, decided at runtime by where the app is running:
// - Deployed at <slug>.<host>: the gateway is same-origin (the platform
// proxy rewrites /_bool → /served/<slug>/_bool), so use a relative path —
// no CORS.
// - Preview (sandbox) or a custom domain: reach the slug gateway
// cross-origin.
// - Deployed on a <label>.<appHost> subdomain: the gateway is same-origin
// (the platform proxy rewrites /_bool → /served/<slug>/_bool keyed on the
// REQUEST host, not the baked slug), so use a relative path — no CORS. This
// matches on the subdomain SHAPE, not on the baked slug: a project whose
// slug/domain changed after this bundle was built serves a stale
// VITE_BOOL_SLUG, but the host is always current, so relative URLs keep
// hitting the right gateway. (Comparing to the baked slug was the bug: a
// renamed app fell through to a cross-origin URL for the OLD slug and 404'd
// — most visibly on the "Continue with Google" tab.)
// - Preview (sandbox *.vercel.run) or a custom domain: reach the slug gateway
// cross-origin at the baked appOrigin.
function boolGatewayBase(): string {
if (
appHost &&
slug &&
typeof location !== "undefined" &&
location.host === slug + "." + appHost
isDeploymentSubdomain(location.host, appHost)
) {
return "";
}
Expand Down
Loading