Skip to content

Commit b442b78

Browse files
committed
Open a narrow data seam into the Platform
Only platform/lib/data may import query hooks, API contracts and named pure helpers; the design audit enforces it and @/app, @/components and @sim/emcn stay banned. Webpack aliases @sim/emcn to a toast-only shim, and a missing export is now a build error. So the seam's closure reaches no legacy UI, moves the session context out of the session provider and extracts browser storage clearing, query client defaults and the public env attribute, each re-imported unchanged.
1 parent b523f91 commit b442b78

17 files changed

Lines changed: 266 additions & 129 deletions

‎apps/sim/app/_shell/providers/get-query-client.ts‎

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,5 @@
1-
import { isServer, QueryClient } from '@tanstack/react-query'
2-
import { isDesktopApp } from '@/lib/desktop'
3-
4-
export function makeQueryClient() {
5-
return new QueryClient({
6-
defaultOptions: {
7-
queries: {
8-
staleTime: 30 * 1000,
9-
// The desktop app window lives for days, so cross-session changes —
10-
// an admin upgrading your org/workspace role, a workspace you were
11-
// auto-added to, seat/entitlement changes — would otherwise stay
12-
// stale until a manual reload (there is no push channel for them).
13-
// Refetch stale queries when the app window regains focus: the "user
14-
// came back after doing something elsewhere" signal, which is exactly
15-
// this bug class. staleTime still gates it, so rapid focus changes
16-
// cost nothing. Left off on the web, where tab-switch focus events are
17-
// frequent and noisy. Per-query overrides (e.g. useWorkspaceSchedules
18-
// pins this off) always win over this default.
19-
refetchOnWindowFocus: isDesktopApp(),
20-
/**
21-
* Query core already defaults retries to 0 on the server and 3 in the browser;
22-
* only the browser number is ours to change. Stating one value for both would
23-
* silently opt server prefetches into a retry, and because the layout awaits
24-
* them that spends a retry backoff of document latency on a read whose failure
25-
* the client recovers from on its own.
26-
*/
27-
retry: isServer ? 0 : 1,
28-
retryOnMount: false,
29-
},
30-
mutations: {
31-
retry: false,
32-
},
33-
},
34-
})
35-
}
1+
import { isServer, type QueryClient } from '@tanstack/react-query'
2+
import { makeQueryClient } from '@/hooks/queries/utils/query-client'
363

374
let browserQueryClient: QueryClient | undefined
385

‎apps/sim/app/_shell/providers/session-provider.test.tsx‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,9 @@ vi.mock('posthog-js', () => ({
1919
},
2020
}))
2121

22+
import { SessionContext, type SessionHookResult } from '@/lib/auth/session-context'
2223
import type { AppSession } from '@/lib/auth/session-response'
23-
import {
24-
SessionContext,
25-
type SessionHookResult,
26-
SessionProvider,
27-
} from '@/app/_shell/providers/session-provider'
24+
import { SessionProvider } from '@/app/_shell/providers/session-provider'
2825
import { sessionKeys } from '@/hooks/queries/session'
2926

3027
const {

‎apps/sim/app/_shell/providers/session-provider.tsx‎

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
11
'use client'
22

33
import type React from 'react'
4-
import { createContext, useEffect, useMemo } from 'react'
4+
import { useEffect, useMemo } from 'react'
55
import { createLogger } from '@sim/logger'
66
import { useQueryClient } from '@tanstack/react-query'
77
import { client } from '@/lib/auth/client'
8+
import { SessionContext, type SessionHookResult } from '@/lib/auth/session-context'
89
import {
910
type AppSession,
1011
extractSessionDataFromAuthClientResult,
1112
getActiveOrganizationId,
1213
} from '@/lib/auth/session-response'
1314
import { sessionKeys, useSessionQuery } from '@/hooks/queries/session'
1415

15-
export type SessionHookResult = {
16-
data: AppSession
17-
isPending: boolean
18-
error: Error | null
19-
refetch: () => Promise<void>
20-
}
21-
22-
export const SessionContext = createContext<SessionHookResult | null>(null)
23-
2416
const logger = createLogger('SessionProvider')
2517

2618
export function SessionProvider({ children }: { children: React.ReactNode }) {

‎apps/sim/app/_shell/public-env-script.test.tsx‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { describe, expect, it, vi } from 'vitest'
22
import { PUBLIC_ENV_ATTRIBUTE } from '@/lib/core/config/env'
3-
import {
4-
PublicEnvScript,
5-
publicEnvHtmlAttributes,
6-
serializePublicEnv,
7-
} from '@/app/_shell/public-env-script'
3+
import { publicEnvHtmlAttributes } from '@/lib/core/config/public-env'
4+
import { PublicEnvScript, serializePublicEnv } from '@/app/_shell/public-env-script'
85

96
vi.unmock('@/lib/core/config/env')
107

‎apps/sim/app/_shell/public-env-script.tsx‎

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,5 @@
11
import { connection } from 'next/server'
2-
import { PUBLIC_ENV_ATTRIBUTE } from '@/lib/core/config/env'
3-
4-
/**
5-
* Every `NEXT_PUBLIC_*` value currently in `process.env`.
6-
*/
7-
function readPublicEnv(): Record<string, string | undefined> {
8-
return Object.fromEntries(
9-
Object.entries(process.env).filter(([key]) => /^NEXT_PUBLIC_/i.test(key))
10-
)
11-
}
2+
import { readPublicEnv } from '@/lib/core/config/public-env'
123

134
/**
145
* `NEXT_PUBLIC_*` values, captured once when this module is first loaded - i.e.
@@ -23,25 +14,6 @@ function readPublicEnv(): Record<string, string | undefined> {
2314
*/
2415
const HOSTED_PUBLIC_ENV = readPublicEnv()
2516

26-
/**
27-
* Props to spread onto the `<html>` element so the public env is readable by any
28-
* client code that can run at all.
29-
*
30-
* The script below is rendered from the component tree and therefore lands at
31-
* the end of `<head>`, well after the `<script async>` bootstrap tags React
32-
* emits in the preamble — see {@link PUBLIC_ENV_ATTRIBUTE} for the full ordering
33-
* argument and why that gap is reachable. `<html>` is the document's first tag,
34-
* so its attributes are parsed before any script exists to read them.
35-
*
36-
* Read fresh rather than from {@link HOSTED_PUBLIC_ENV} so the one helper serves
37-
* both deployment modes: self-hosted images re-inject env per deploy without a
38-
* rebuild. On hosted the two reads are the same values, because nothing mutates
39-
* `process.env` after boot.
40-
*/
41-
export function publicEnvHtmlAttributes(): Record<string, string> {
42-
return { [PUBLIC_ENV_ATTRIBUTE]: JSON.stringify(readPublicEnv()) }
43-
}
44-
4517
/**
4618
* Serialize embedded JSON defensively so a public value cannot terminate the
4719
* inline script. JSON's two JavaScript line separators are escaped as well for
@@ -78,7 +50,7 @@ function EnvScript({ env }: EnvScriptProps) {
7850
* lost race costs milliseconds instead of the session. It does not make the
7951
* assignment win the race, though: draining that queue was also the only thing
8052
* sequencing the assignment ahead of `hydrate()`, and with the queue empty
81-
* `appBootstrap` hydrates synchronously. {@link publicEnvHtmlAttributes} is what
53+
* `appBootstrap` hydrates synchronously. `publicEnvHtmlAttributes` is what
8254
* closes the remaining window - this tag stays because `window.__ENV` is the
8355
* documented global, and it is what `getEnv` reads first.
8456
*/

‎apps/sim/app/layout.tsx‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
isReactGrabEnabled,
1717
isReactScanEnabled,
1818
} from '@/lib/core/config/env-flags'
19+
import { publicEnvHtmlAttributes } from '@/lib/core/config/public-env'
1920
import { ConsentProvider } from '@/app/_shell/consent/consent-provider'
2021
import { DesktopUpdateGate } from '@/app/_shell/desktop-update-gate'
2122
import { DesktopUpdateNotification } from '@/app/_shell/desktop-update-notification'
@@ -24,11 +25,7 @@ import { QueryProvider } from '@/app/_shell/providers/query-provider'
2425
import { SessionProvider } from '@/app/_shell/providers/session-provider'
2526
import { ThemeProvider } from '@/app/_shell/providers/theme-provider'
2627
import { TooltipProvider } from '@/app/_shell/providers/tooltip-provider'
27-
import {
28-
PublicEnvScript,
29-
publicEnvHtmlAttributes,
30-
RuntimePublicEnvScript,
31-
} from '@/app/_shell/public-env-script'
28+
import { PublicEnvScript, RuntimePublicEnvScript } from '@/app/_shell/public-env-script'
3229
import { season } from '@/app/_styles/fonts/season/season'
3330

3431
export const viewport: Viewport = {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { isServer, QueryClient } from '@tanstack/react-query'
2+
import { isDesktopApp } from '@/lib/desktop'
3+
4+
/** One set of cache defaults for every surface that owns a query client. */
5+
export function makeQueryClient() {
6+
return new QueryClient({
7+
defaultOptions: {
8+
queries: {
9+
staleTime: 30 * 1000,
10+
// The desktop app window lives for days, so cross-session changes —
11+
// an admin upgrading your org/workspace role, a workspace you were
12+
// auto-added to, seat/entitlement changes — would otherwise stay
13+
// stale until a manual reload (there is no push channel for them).
14+
// Refetch stale queries when the app window regains focus: the "user
15+
// came back after doing something elsewhere" signal, which is exactly
16+
// this bug class. staleTime still gates it, so rapid focus changes
17+
// cost nothing. Left off on the web, where tab-switch focus events are
18+
// frequent and noisy. Per-query overrides (e.g. useWorkspaceSchedules
19+
// pins this off) always win over this default.
20+
refetchOnWindowFocus: isDesktopApp(),
21+
/**
22+
* Query core already defaults retries to 0 on the server and 3 in the browser;
23+
* only the browser number is ours to change. Stating one value for both would
24+
* silently opt server prefetches into a retry, and because the layout awaits
25+
* them that spends a retry backoff of document latency on a read whose failure
26+
* the client recovers from on its own.
27+
*/
28+
retry: isServer ? 0 : 1,
29+
retryOnMount: false,
30+
},
31+
mutations: {
32+
retry: false,
33+
},
34+
},
35+
})
36+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createLogger } from '@sim/logger'
2+
3+
const logger = createLogger('BrowserStorage')
4+
5+
export const RECENT_IMPERSONATIONS_STORAGE_KEY = 'recent-impersonations'
6+
7+
export interface ClearBrowserUserDataOptions {
8+
preserveRecentImpersonations?: boolean
9+
}
10+
11+
/**
12+
* Clears what one viewer leaves in this origin's browser storage, keeping only the keys that belong
13+
* to the device. Every surface on the origin shares this storage, so each one clears it at an
14+
* identity boundary. Returns whether all of it was cleared.
15+
*/
16+
export function clearBrowserUserData(options: ClearBrowserUserDataOptions = {}): boolean {
17+
let cleared = true
18+
19+
try {
20+
const keysToKeep = [
21+
'next-favicon',
22+
'sim-theme',
23+
'sim-landing-theme',
24+
...(options.preserveRecentImpersonations ? [RECENT_IMPERSONATIONS_STORAGE_KEY] : []),
25+
]
26+
const keysToRemove = Object.keys(localStorage).filter((key) => !keysToKeep.includes(key))
27+
keysToRemove.forEach((key) => localStorage.removeItem(key))
28+
} catch (error) {
29+
cleared = false
30+
logger.error('Error clearing local user data:', { error })
31+
}
32+
33+
try {
34+
sessionStorage.clear()
35+
} catch (error) {
36+
cleared = false
37+
logger.error('Error clearing tab-scoped user data:', { error })
38+
}
39+
40+
return cleared
41+
}

‎apps/sim/lib/auth/client.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import {
1111
} from 'better-auth/client/plugins'
1212
import { createAuthClient } from 'better-auth/react'
1313
import type { auth } from '@/lib/auth'
14+
import { SessionContext, type SessionHookResult } from '@/lib/auth/session-context'
1415
import { isBillingEnabled, isOrganizationsEnabled, isSsoEnabled } from '@/lib/core/config/env-flags'
1516
import { getBaseUrl, getBrowserOrigin } from '@/lib/core/utils/urls'
16-
import { SessionContext, type SessionHookResult } from '@/app/_shell/providers/session-provider'
1717

1818
function getAuthBaseUrl(): string {
1919
return getBrowserOrigin() ?? getBaseUrl()
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use client'
2+
3+
import { createContext } from 'react'
4+
import type { AppSession } from '@/lib/auth/session-response'
5+
6+
export type SessionHookResult = {
7+
data: AppSession
8+
isPending: boolean
9+
error: Error | null
10+
refetch: () => Promise<void>
11+
}
12+
13+
/** What `useSession` reads. Separate from any provider, so the auth client imports no surface. */
14+
export const SessionContext = createContext<SessionHookResult | null>(null)

0 commit comments

Comments
 (0)