diff --git a/packages/core/package.json b/packages/core/package.json index bbd22c7ac..ebbc6ac50 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -74,6 +74,7 @@ "open": "^11.0.0", "posthog-node": "^5.45.2", "superjson": "^2.2.6", + "tldts": "^7.0.19", "yaml": "^2.9.0", "zustand": "^5.0.14" }, diff --git a/packages/core/src/server/requestOrigin.test.ts b/packages/core/src/server/requestOrigin.test.ts new file mode 100644 index 000000000..027f75ebf --- /dev/null +++ b/packages/core/src/server/requestOrigin.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; + +import { resolveWidgetDomain } from "./requestOrigin.js"; + +describe("resolveWidgetDomain", () => { + it("returns the registrable domain for an ordinary subdomain", () => { + expect(resolveWidgetDomain("https://capitals.skybridge.tech")).toBe( + "skybridge.tech", + ); + }); + + it("keeps tenants isolated on private suffixes", () => { + expect(resolveWidgetDomain("https://alice.github.io")).toBe( + "alice.github.io", + ); + expect(resolveWidgetDomain("https://bob.github.io")).toBe("bob.github.io"); + }); + + it("keeps the origin fallback for non-registrable hosts", () => { + expect(resolveWidgetDomain("http://localhost:3000")).toBe( + "http://localhost:3000", + ); + }); +}); diff --git a/packages/core/src/server/requestOrigin.ts b/packages/core/src/server/requestOrigin.ts index 563c63643..6fa4c114b 100644 --- a/packages/core/src/server/requestOrigin.ts +++ b/packages/core/src/server/requestOrigin.ts @@ -1,3 +1,5 @@ +import { getDomain } from "tldts"; + /** * Resolves this server's public origin from request headers, in precedence * `x-forwarded-host` → `host` → localhost dev fallback. Shared by view serving @@ -24,3 +26,9 @@ export function resolveServerOrigin( } return `http://localhost:${process.env.__PORT || "3000"}`; } + +/** Resolve the registrable host used as the default widget security domain. */ +export function resolveWidgetDomain(serverOrigin: string): string { + const hostname = new URL(serverOrigin).hostname; + return getDomain(hostname, { allowPrivateDomains: true }) ?? serverOrigin; +} diff --git a/packages/core/src/server/server.ts b/packages/core/src/server/server.ts index a35bc1a3a..bbf1f39aa 100644 --- a/packages/core/src/server/server.ts +++ b/packages/core/src/server/server.ts @@ -48,7 +48,7 @@ import type { McpWildcard, } from "./middleware.js"; import { buildMiddlewareChain, getHandlerMaps } from "./middleware.js"; -import { resolveServerOrigin } from "./requestOrigin.js"; +import { resolveServerOrigin, resolveWidgetDomain } from "./requestOrigin.js"; import { discoverSkills, registerSkills, @@ -1127,7 +1127,7 @@ export class McpServer< { resourceDomains: [serverUrl], connectDomains, - domain: serverUrl, + domain: resolveWidgetDomain(serverUrl), baseUriDomains: [serverUrl], }, contentMetaOverrides, diff --git a/packages/core/src/test/view.test.ts b/packages/core/src/test/view.test.ts index 7b6ef074e..ba464ed03 100644 --- a/packages/core/src/test/view.test.ts +++ b/packages/core/src/test/view.test.ts @@ -182,7 +182,7 @@ describe("McpServer.registerTool (unified API)", () => { }>; expect(appsSdkResourceCallback).toBeDefined(); - const host = "myapp.com"; + const host = "capitals.skybridge.tech"; const serverUrl = `https://${host}`; const mockExtra = createMockExtra(host) as unknown as RequestHandlerExtra< ServerRequest, @@ -207,7 +207,7 @@ describe("McpServer.registerTool (unified API)", () => { connectDomains: [serverUrl], baseUriDomains: [serverUrl], }, - domain: serverUrl, + domain: "skybridge.tech", description: "Test view", }, "openai/widgetDescription": "Test view", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf3488164..4298de8b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1398,6 +1398,9 @@ importers: superjson: specifier: ^2.2.6 version: 2.2.6 + tldts: + specifier: ^7.0.19 + version: 7.0.19 yaml: specifier: ^2.9.0 version: 2.9.0