Skip to content

Commit fdbc7a7

Browse files
authored
fix(env): preserve hosted detection during client recovery (#7445)
1 parent cdc6973 commit fdbc7a7

4 files changed

Lines changed: 88 additions & 8 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* @vitest-environment jsdom
3+
* @vitest-environment-options {"url":"https://www.sim.ai"}
4+
*/
5+
import { afterEach, describe, expect, it, vi } from 'vitest'
6+
7+
vi.hoisted(() => {
8+
vi.stubEnv('NEXT_PUBLIC_APP_URL', '')
9+
vi.stubEnv('NEXT_PUBLIC_FORCE_HOSTED', 'false')
10+
vi.stubEnv('NODE_ENV', 'production')
11+
document.documentElement.id = '__next_error__'
12+
})
13+
14+
vi.unmock('@/lib/core/config/env')
15+
vi.unmock('@/lib/core/config/env-flags')
16+
vi.mock('@/lib/oauth/utils', () => ({ getScopesForService: () => [] }))
17+
vi.mock('@/providers/utils', () => ({ getProviderFromModel: () => 'openai' }))
18+
19+
import { getEnv, PUBLIC_ENV_ATTRIBUTE } from '@/lib/core/config/env'
20+
import { isHosted } from '@/lib/core/config/env-flags'
21+
import { evaluateSubBlockCondition } from '@/lib/workflows/subblocks/visibility'
22+
import { getApiKeyCondition } from '@/blocks/utils'
23+
import { getHostedModels } from '@/providers/models'
24+
25+
describe('hosted detection during client recovery', () => {
26+
afterEach(() => {
27+
document.documentElement.removeAttribute(PUBLIC_ENV_ATTRIBUTE)
28+
Reflect.deleteProperty(window, '__ENV')
29+
})
30+
31+
it('hides hosted model keys before the recovered layout installs runtime configuration', () => {
32+
expect(window.__ENV).toBeUndefined()
33+
expect(document.documentElement.getAttribute(PUBLIC_ENV_ATTRIBUTE)).toBeNull()
34+
expect(isHosted).toBe(true)
35+
36+
for (const model of ['gpt-5.6-sol', 'claude-sonnet-5', 'gemini-2.5-pro']) {
37+
expect(getHostedModels()).toContain(model)
38+
expect(evaluateSubBlockCondition(getApiKeyCondition(), { model })).toBe(false)
39+
}
40+
41+
document.documentElement.setAttribute(
42+
PUBLIC_ENV_ATTRIBUTE,
43+
JSON.stringify({ NEXT_PUBLIC_APP_URL: 'https://www.sim.ai' })
44+
)
45+
46+
expect(getEnv('NEXT_PUBLIC_APP_URL')).toBe('https://www.sim.ai')
47+
expect(isHosted).toBe(true)
48+
expect(evaluateSubBlockCondition(getApiKeyCondition(), { model: 'gpt-5.6-sol' })).toBe(false)
49+
})
50+
51+
it('still requires keys for models outside the hosted catalog', () => {
52+
expect(evaluateSubBlockCondition(getApiKeyCondition(), { model: 'custom/model' })).toBe(true)
53+
})
54+
})
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it, vi } from 'vitest'
5+
6+
vi.hoisted(() => {
7+
vi.stubEnv('NEXT_PUBLIC_APP_URL', 'https://self-hosted.example')
8+
vi.stubEnv('NEXT_PUBLIC_FORCE_HOSTED', 'true')
9+
vi.stubEnv('NODE_ENV', 'production')
10+
vi.stubGlobal('window', { location: { hostname: 'www.sim.ai' } })
11+
})
12+
13+
vi.unmock('@/lib/core/config/env')
14+
vi.unmock('@/lib/core/config/env-flags')
15+
16+
import { isHosted, isProd } from '@/lib/core/config/env-flags'
17+
18+
describe('configured hosted detection', () => {
19+
it('preserves a configured self-hosted URL and ignores the development override in production', () => {
20+
expect(isProd).toBe(true)
21+
expect(isHosted).toBe(false)
22+
})
23+
})

apps/sim/lib/core/config/env-flags.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,16 @@ export const isTest = env.NODE_ENV === 'test'
3535
/**
3636
* Is this the hosted version of the application.
3737
* True for sim.ai and any subdomain of sim.ai (e.g. staging.sim.ai, dev.sim.ai).
38+
* The browser hostname remains available when an error document boots without
39+
* the root layout's runtime environment, before client rendering recovers it.
40+
* A valid configured URL takes precedence; server detection stays env-only.
3841
*/
3942
const appUrl = getEnv('NEXT_PUBLIC_APP_URL')
40-
let appHostname = ''
43+
let appHostname = typeof window === 'undefined' ? '' : window.location.hostname
4144
try {
42-
appHostname = appUrl ? new URL(appUrl).hostname : ''
45+
if (appUrl) appHostname = new URL(appUrl).hostname
4346
} catch {
44-
// invalid URL — isHosted stays false
47+
/** Keep the document hostname when the configured URL cannot be parsed. */
4548
}
4649
/**
4750
* Local-development escape hatch for exercising hosted-only paths (the sim-auto

apps/sim/lib/core/config/env.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ import { z } from 'zod'
2424
* hydration. So on a warm cache both module bodies and the first commit can run
2525
* before the parser has reached the assignment.
2626
*
27-
* An attribute has no such ordering problem. `<html>` is the first tag in the
28-
* document — ~490 bytes ahead of the first bootstrap script — so
29-
* `document.documentElement` already carries this value by the time *any*
30-
* script, framework or application, is able to execute. This is the race-free
31-
* transport; `window.__ENV` stays the public global and the preferred read.
27+
* On a normally rendered document, the attribute is parsed before bootstrap
28+
* scripts can execute. Next's server-rendering error document omits the root
29+
* layout, so client recovery only installs the attribute when that layout
30+
* mounts. Hosted detection also uses the browser hostname during this gap.
31+
* `window.__ENV` stays the public global and the preferred read.
3232
*/
3333
export const PUBLIC_ENV_ATTRIBUTE = 'data-public-env'
3434

0 commit comments

Comments
 (0)