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
54 changes: 54 additions & 0 deletions apps/sim/lib/core/config/env-flags.dom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* @vitest-environment jsdom
* @vitest-environment-options {"url":"https://www.sim.ai"}
*/
import { afterEach, describe, expect, it, vi } from 'vitest'

vi.hoisted(() => {
vi.stubEnv('NEXT_PUBLIC_APP_URL', '')
vi.stubEnv('NEXT_PUBLIC_FORCE_HOSTED', 'false')
vi.stubEnv('NODE_ENV', 'production')
document.documentElement.id = '__next_error__'
})

vi.unmock('@/lib/core/config/env')
vi.unmock('@/lib/core/config/env-flags')
vi.mock('@/lib/oauth/utils', () => ({ getScopesForService: () => [] }))
vi.mock('@/providers/utils', () => ({ getProviderFromModel: () => 'openai' }))

import { getEnv, PUBLIC_ENV_ATTRIBUTE } from '@/lib/core/config/env'
import { isHosted } from '@/lib/core/config/env-flags'
import { evaluateSubBlockCondition } from '@/lib/workflows/subblocks/visibility'
import { getApiKeyCondition } from '@/blocks/utils'
import { getHostedModels } from '@/providers/models'

describe('hosted detection during client recovery', () => {
afterEach(() => {
document.documentElement.removeAttribute(PUBLIC_ENV_ATTRIBUTE)
Reflect.deleteProperty(window, '__ENV')
})

it('hides hosted model keys before the recovered layout installs runtime configuration', () => {
expect(window.__ENV).toBeUndefined()
expect(document.documentElement.getAttribute(PUBLIC_ENV_ATTRIBUTE)).toBeNull()
expect(isHosted).toBe(true)

for (const model of ['gpt-5.6-sol', 'claude-sonnet-5', 'gemini-2.5-pro']) {
expect(getHostedModels()).toContain(model)
expect(evaluateSubBlockCondition(getApiKeyCondition(), { model })).toBe(false)
}

document.documentElement.setAttribute(
PUBLIC_ENV_ATTRIBUTE,
JSON.stringify({ NEXT_PUBLIC_APP_URL: 'https://www.sim.ai' })
)

expect(getEnv('NEXT_PUBLIC_APP_URL')).toBe('https://www.sim.ai')
expect(isHosted).toBe(true)
expect(evaluateSubBlockCondition(getApiKeyCondition(), { model: 'gpt-5.6-sol' })).toBe(false)
})

it('still requires keys for models outside the hosted catalog', () => {
expect(evaluateSubBlockCondition(getApiKeyCondition(), { model: 'custom/model' })).toBe(true)
})
})
23 changes: 23 additions & 0 deletions apps/sim/lib/core/config/env-flags.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @vitest-environment node
*/
import { describe, expect, it, vi } from 'vitest'

vi.hoisted(() => {
vi.stubEnv('NEXT_PUBLIC_APP_URL', 'https://self-hosted.example')
vi.stubEnv('NEXT_PUBLIC_FORCE_HOSTED', 'true')
vi.stubEnv('NODE_ENV', 'production')
vi.stubGlobal('window', { location: { hostname: 'www.sim.ai' } })
})

vi.unmock('@/lib/core/config/env')
vi.unmock('@/lib/core/config/env-flags')

import { isHosted, isProd } from '@/lib/core/config/env-flags'

describe('configured hosted detection', () => {
it('preserves a configured self-hosted URL and ignores the development override in production', () => {
expect(isProd).toBe(true)
expect(isHosted).toBe(false)
})
})
9 changes: 6 additions & 3 deletions apps/sim/lib/core/config/env-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,16 @@ export const isTest = env.NODE_ENV === 'test'
/**
* Is this the hosted version of the application.
* True for sim.ai and any subdomain of sim.ai (e.g. staging.sim.ai, dev.sim.ai).
* The browser hostname remains available when an error document boots without
* the root layout's runtime environment, before client rendering recovers it.
* A valid configured URL takes precedence; server detection stays env-only.
*/
const appUrl = getEnv('NEXT_PUBLIC_APP_URL')
let appHostname = ''
let appHostname = typeof window === 'undefined' ? '' : window.location.hostname
try {
appHostname = appUrl ? new URL(appUrl).hostname : ''
if (appUrl) appHostname = new URL(appUrl).hostname
} catch {
// invalid URL — isHosted stays false
/** Keep the document hostname when the configured URL cannot be parsed. */
}
/**
* Local-development escape hatch for exercising hosted-only paths (the sim-auto
Expand Down
10 changes: 5 additions & 5 deletions apps/sim/lib/core/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ import { z } from 'zod'
* hydration. So on a warm cache both module bodies and the first commit can run
* before the parser has reached the assignment.
*
* An attribute has no such ordering problem. `<html>` is the first tag in the
* document — ~490 bytes ahead of the first bootstrap script — so
* `document.documentElement` already carries this value by the time *any*
* script, framework or application, is able to execute. This is the race-free
* transport; `window.__ENV` stays the public global and the preferred read.
* On a normally rendered document, the attribute is parsed before bootstrap
* scripts can execute. Next's server-rendering error document omits the root
* layout, so client recovery only installs the attribute when that layout
* mounts. Hosted detection also uses the browser hostname during this gap.
* `window.__ENV` stays the public global and the preferred read.
*/
export const PUBLIC_ENV_ATTRIBUTE = 'data-public-env'

Expand Down
Loading