diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index a79f8b5..e387428 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -361,7 +361,7 @@ export async function createApp( "https://172.*:*", "http://*.local:*", "https://*.local:*", - ...(process.env.EXTRA_TRUSTED_ORIGINS?.split(",").map((s) => s.trim()) ?? []), + ...(env.extraTrustedOrigins ?? []), ], beforeDeleteUser: async (userId) => { const bots = await prisma.bot.findMany({ @@ -617,7 +617,11 @@ export async function createApp( actor, signal: c.req.raw.signal, userAgent: c.req.header("user-agent"), - webOrigin: requestWebOrigin(c.req.header("origin"), env.webOrigin), + webOrigin: requestWebOrigin( + c.req.header("origin"), + env.webOrigin, + env.extraTrustedOrigins, + ), }, }); if (matched) return c.newResponse(response.body, response); @@ -1027,6 +1031,7 @@ export async function createApp( function isTrustedOrigin(origin: string, env: AppEnv) { if (!origin) return true; if (origin === env.webOrigin || origin === env.apiUrl || origin === env.authUrl) return true; + if (env.extraTrustedOrigins?.includes(origin)) return true; if (origin.startsWith("rakazo://") || origin.startsWith("exp://")) return true; try { const host = new URL(origin).hostname; diff --git a/apps/api/src/env.ts b/apps/api/src/env.ts index b260c21..92284a7 100644 --- a/apps/api/src/env.ts +++ b/apps/api/src/env.ts @@ -20,6 +20,7 @@ export interface AppEnv { authSecret: string; authUrl: string; webOrigin: string; + extraTrustedOrigins: string[]; privacyPolicyUrl?: string; apiUrl: string; apiHost: string; @@ -109,6 +110,10 @@ export function loadEnv(source: NodeJS.ProcessEnv = process.env): AppEnv { authSecret, authUrl: source.BETTER_AUTH_URL ?? source.WEB_ORIGIN ?? "http://127.0.0.1:5173", webOrigin: source.WEB_ORIGIN ?? "http://127.0.0.1:5173", + extraTrustedOrigins: (source.EXTRA_TRUSTED_ORIGINS ?? "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean), privacyPolicyUrl: optional(source.PRIVACY_POLICY_URL), apiUrl: source.API_URL ?? "http://127.0.0.1:3100", apiHost: source.API_HOST ?? "127.0.0.1", diff --git a/apps/api/src/web-origin.test.ts b/apps/api/src/web-origin.test.ts index 943d75b..a61bd0d 100644 --- a/apps/api/src/web-origin.test.ts +++ b/apps/api/src/web-origin.test.ts @@ -25,4 +25,12 @@ describe("requestWebOrigin", () => { "https://evoo.example", ); }); + + it("accepts origins listed in extraTrustedOrigins", () => { + expect( + requestWebOrigin("https://host.example.ts.net:8443", configured, [ + "https://host.example.ts.net:8443", + ]), + ).toBe("https://host.example.ts.net:8443"); + }); }); diff --git a/apps/api/src/web-origin.ts b/apps/api/src/web-origin.ts index 3a210fa..dfef37c 100644 --- a/apps/api/src/web-origin.ts +++ b/apps/api/src/web-origin.ts @@ -14,15 +14,20 @@ export function isLoopbackHost(host: string): boolean { } /** The web origin that browser-facing URLs (OAuth returns, screen links) should - * point at for this request. A browser opened over a loopback or private-network - * address gets its own origin back so a phone on the LAN is not sent to - * 127.0.0.1; any other caller gets the configured WEB_ORIGIN. */ -export function requestWebOrigin(origin: string | undefined, webOrigin: string): string { + * point at for this request. A browser opened over a loopback, private-network, + * or explicitly trusted origin gets its own origin back so a phone on the LAN + * or Tailscale is not sent to 127.0.0.1; any other caller gets WEB_ORIGIN. */ +export function requestWebOrigin( + origin: string | undefined, + webOrigin: string, + extraTrustedOrigins: readonly string[] = [], +): string { if (!origin) return webOrigin; try { const url = new URL(origin); if (url.protocol !== "http:" && url.protocol !== "https:") return webOrigin; if (url.origin === new URL(webOrigin).origin) return url.origin; + if (extraTrustedOrigins.includes(url.origin)) return url.origin; return isPrivateOrLocalHost(url.hostname) ? url.origin : webOrigin; } catch { return webOrigin;