Files
simstudioai--sim/apps/sim/lib/core/utils/urls.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

225 lines
6.7 KiB
TypeScript

import { env, getEnv } from '@/lib/core/config/env'
import { isProd } from '@/lib/core/config/env-flags'
/** Canonical base URL for the public-facing marketing site. No trailing slash. */
export const SITE_URL = 'https://www.sim.ai'
function hasHttpProtocol(url: string): boolean {
return /^https?:\/\//i.test(url)
}
function normalizeBaseUrl(url: string): string {
if (hasHttpProtocol(url)) {
return url
}
const protocol = isProd ? 'https://' : 'http://'
return `${protocol}${url}`
}
/**
* Returns the base URL of the application from NEXT_PUBLIC_APP_URL
* This ensures webhooks, callbacks, and other integrations always use the correct public URL
* @returns The base URL string (e.g., 'http://localhost:3000' or 'https://example.com')
* @throws Error if NEXT_PUBLIC_APP_URL is not configured
*/
export function getBaseUrl(): string {
const baseUrl = getEnv('NEXT_PUBLIC_APP_URL')?.trim()
if (!baseUrl) {
throw new Error(
'NEXT_PUBLIC_APP_URL must be configured for webhooks and callbacks to work correctly'
)
}
return normalizeBaseUrl(baseUrl)
}
/**
* Returns the base URL used by server-side internal API calls.
* Falls back to NEXT_PUBLIC_APP_URL when INTERNAL_API_BASE_URL is not set.
*/
export function getInternalApiBaseUrl(): string {
const internalBaseUrl = getEnv('INTERNAL_API_BASE_URL')?.trim()
if (!internalBaseUrl) {
return getBaseUrl()
}
if (!hasHttpProtocol(internalBaseUrl)) {
throw new Error(
'INTERNAL_API_BASE_URL must include protocol (http:// or https://), e.g. http://sim-app.default.svc.cluster.local:3000'
)
}
return internalBaseUrl
}
/**
* Ensures a URL is absolute by prefixing the base URL when a relative path is provided.
* @param pathOrUrl - Relative path (e.g., /api/files/serve/...) or absolute URL
*/
export function ensureAbsoluteUrl(pathOrUrl: string): string {
if (!pathOrUrl) {
throw new Error('URL is required')
}
if (pathOrUrl.startsWith('/')) {
return `${getBaseUrl()}${pathOrUrl}`
}
return pathOrUrl
}
/**
* Returns just the domain and port part of the application URL
* @returns The domain with port if applicable (e.g., 'localhost:3000' or 'sim.ai')
*/
export function getBaseDomain(): string {
try {
const url = new URL(getBaseUrl())
return url.host // host includes port if specified
} catch (_e) {
const fallbackUrl = getEnv('NEXT_PUBLIC_APP_URL') || 'http://localhost:3000'
try {
return new URL(fallbackUrl).host
} catch {
return isProd ? 'sim.ai' : 'localhost:3000'
}
}
}
/**
* Returns the domain for email addresses, stripping www subdomain for Resend compatibility
* @returns The email domain (e.g., 'sim.ai' instead of 'www.sim.ai')
*/
export function getEmailDomain(): string {
try {
const baseDomain = getBaseDomain()
return baseDomain.startsWith('www.') ? baseDomain.substring(4) : baseDomain
} catch (_e) {
return isProd ? 'sim.ai' : 'localhost:3000'
}
}
const DEFAULT_SOCKET_URL = 'http://localhost:3002'
const DEFAULT_OLLAMA_URL = 'http://localhost:11434'
export const LOCALHOST_HOSTNAMES: ReadonlySet<string> = new Set([
'localhost',
'127.0.0.1',
'[::1]',
'::1',
])
export function isLoopbackHostname(hostname: string): boolean {
return LOCALHOST_HOSTNAMES.has(hostname)
}
/**
* Parses a comma-separated list of origins (e.g. from a `TRUSTED_ORIGINS` env
* var) into a deduped array of normalized origins. Invalid entries are dropped.
*
* @param raw - Comma-separated origin list, or undefined/empty
* @param onInvalid - Optional callback invoked once per invalid entry
*/
export function parseOriginList(
raw: string | undefined | null,
onInvalid?: (value: string) => void
): string[] {
if (!raw) return []
const seen = new Set<string>()
const origins: string[] = []
for (const candidate of raw.split(',')) {
const trimmed = candidate.trim()
if (!trimmed) continue
try {
const { origin } = new URL(trimmed)
if (!seen.has(origin)) {
seen.add(origin)
origins.push(origin)
}
} catch {
onInvalid?.(trimmed)
}
}
return origins
}
/**
* Returns true when the given URL points at a localhost loopback host.
* Used to detect misconfigured deployments where `NEXT_PUBLIC_APP_URL` is left
* at its development default in production.
*/
export function isLocalhostUrl(url: string): boolean {
try {
const { hostname } = new URL(url)
return LOCALHOST_HOSTNAMES.has(hostname)
} catch {
return false
}
}
/**
* Returns the current browser origin, or `null` when called server-side.
*
* Use this when an absolute URL is needed for a same-origin resource (auth API,
* reverse-proxied socket, etc.) so a misconfigured `NEXT_PUBLIC_*` env var
* baked into the client bundle at build time can't pin requests to the wrong host.
*/
export function getBrowserOrigin(): string | null {
return typeof window !== 'undefined' ? window.location.origin : null
}
/**
* Validates that a URL uses an http(s) scheme before it is opened in a new window.
* Rejects `javascript:`, `data:`, `blob:`, `vbscript:`, and other schemes that could
* execute script in the chat origin, since `file.url` originates from untrusted
* workflow/agent output.
*/
export function isSafeHttpUrl(url: string): boolean {
try {
const parsed = new URL(url, getBrowserOrigin() ?? undefined)
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
} catch {
return false
}
}
/**
* Returns the socket server URL for server-side internal API calls.
* Reads from SOCKET_SERVER_URL with a localhost fallback for development.
*/
export function getSocketServerUrl(): string {
return env.SOCKET_SERVER_URL || DEFAULT_SOCKET_URL
}
/**
* Returns the socket server URL for client-side Socket.IO connections.
*
* Resolution order:
* 1. `NEXT_PUBLIC_SOCKET_URL` if explicitly set (subdomain, separate host:port)
* 2. In the browser when the page is served from a non-localhost origin, the
* page's own origin — assumes the reverse proxy routes `/socket.io` to the
* realtime service. This avoids shipping a hardcoded `localhost:3002` to
* self-hosters behind nginx/Cloudflare.
* 3. `http://localhost:3002` for local development and SSR.
*/
export function getSocketUrl(): string {
const explicit = getEnv('NEXT_PUBLIC_SOCKET_URL')?.trim()
if (explicit) return explicit
const browserOrigin = getBrowserOrigin()
if (browserOrigin && !LOCALHOST_HOSTNAMES.has(new URL(browserOrigin).hostname)) {
return browserOrigin
}
return DEFAULT_SOCKET_URL
}
/**
* Returns the Ollama server URL.
* Reads from OLLAMA_URL with a localhost fallback for development.
*/
export function getOllamaUrl(): string {
return env.OLLAMA_URL || DEFAULT_OLLAMA_URL
}