chore: import upstream snapshot with attribution
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,38 @@
import { defaultShouldDehydrateQuery, isServer, QueryClient } from '@tanstack/react-query'
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 30 * 1000,
gcTime: 5 * 60 * 1000,
refetchOnWindowFocus: false,
retry: 1,
retryOnMount: false,
},
mutations: {
retry: false,
},
dehydrate: {
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
},
},
})
}
let browserQueryClient: QueryClient | undefined
/**
* Returns a QueryClient instance. On the server, creates a new instance per request.
* On the client, reuses a singleton instance.
*/
export function getQueryClient() {
if (isServer) {
return makeQueryClient()
}
if (!browserQueryClient) {
browserQueryClient = makeQueryClient()
}
return browserQueryClient
}
@@ -0,0 +1,65 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import type { PostHog } from 'posthog-js'
import { getEnv, isTruthy } from '@/lib/core/config/env'
const logger = createLogger('PostHogProvider')
export function PostHogProvider({ children }: { children: React.ReactNode }) {
const [Provider, setProvider] = useState<React.ComponentType<{
client: PostHog
children: React.ReactNode
}> | null>(null)
const clientRef = useRef<PostHog | null>(null)
useEffect(() => {
const posthogEnabled = getEnv('NEXT_PUBLIC_POSTHOG_ENABLED')
const posthogKey = getEnv('NEXT_PUBLIC_POSTHOG_KEY')
if (!isTruthy(posthogEnabled) || !posthogKey) return
Promise.all([import('posthog-js'), import('posthog-js/react')])
.then(([posthogModule, { PostHogProvider: PHProvider }]) => {
const posthog = posthogModule.default
if (!posthog.__loaded) {
posthog.init(posthogKey, {
api_host: '/ingest',
ui_host: 'https://us.posthog.com',
defaults: '2025-05-24',
person_profiles: 'identified_only',
autocapture: false,
capture_pageview: false,
capture_pageleave: false,
capture_performance: false,
capture_dead_clicks: false,
enable_heatmaps: false,
disable_session_recording: true,
session_recording: {
maskAllInputs: false,
maskInputOptions: {
password: true,
email: false,
},
recordCrossOriginIframes: false,
recordHeaders: false,
recordBody: false,
},
persistence: 'localStorage+cookie',
})
}
clientRef.current = posthog
setProvider(() => PHProvider)
})
.catch((err) => {
logger.error('Failed to load PostHog', { error: err })
})
}, [])
if (Provider && clientRef.current) {
return <Provider client={clientRef.current}>{children}</Provider>
}
return <>{children}</>
}
@@ -0,0 +1,12 @@
'use client'
import type { ReactNode } from 'react'
import { QueryClientProvider } from '@tanstack/react-query'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
export { getQueryClient } from '@/app/_shell/providers/get-query-client'
export function QueryProvider({ children }: { children: ReactNode }) {
const queryClient = getQueryClient()
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
@@ -0,0 +1,278 @@
/**
* @vitest-environment jsdom
*/
import { act, useContext } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { mockGetSession, mockSetActive, mockRequestJson } = vi.hoisted(() => ({
mockGetSession: vi.fn(),
mockSetActive: vi.fn(),
mockRequestJson: vi.fn(),
}))
vi.mock('@/lib/auth/auth-client', () => ({
client: {
getSession: mockGetSession,
organization: { setActive: mockSetActive },
},
}))
vi.mock('@/lib/api/client/request', () => ({
requestJson: mockRequestJson,
}))
vi.mock('posthog-js', () => ({
default: {
identify: vi.fn(),
reset: vi.fn(),
startSessionRecording: vi.fn(),
sessionRecordingStarted: vi.fn(() => true),
},
}))
import type { AppSession } from '@/lib/auth/session-response'
import {
SessionContext,
type SessionHookResult,
SessionProvider,
} from '@/app/_shell/providers/session-provider'
import { sessionKeys, useSessionQuery } from '@/hooks/queries/session'
/** Deferred promise: lets a test resolve a mocked async call at a chosen moment. */
function defer<T>() {
let resolve!: (value: T) => void
let reject!: (reason?: unknown) => void
const promise = new Promise<T>((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
/** Set the jsdom URL search string before rendering the provider. */
function setSearch(search: string) {
window.history.replaceState({}, '', `/${search}`)
}
const STALE_SESSION: AppSession = {
user: { id: 'user-1', email: 'u@x.com', name: 'Stale plan' },
session: { id: 's1', userId: 'user-1', activeOrganizationId: 'org-1' },
}
const FRESH_SESSION: AppSession = {
user: { id: 'user-1', email: 'u@x.com', name: 'Fresh plan' },
session: { id: 's1', userId: 'user-1', activeOrganizationId: 'org-1' },
}
interface Harness {
ctx: () => SessionHookResult | null
queryClient: QueryClient
unmount: () => void
}
/**
* Mounts SessionProvider in a real React 19 root under jsdom with a real
* QueryClient, capturing the live context value via a probe consumer.
*/
function renderProvider(): Harness {
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
const container = document.createElement('div')
const root: Root = createRoot(container)
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
let latest: SessionHookResult | null = null
function Probe() {
latest = useContext(SessionContext)
return null
}
act(() => {
root.render(
<QueryClientProvider client={queryClient}>
<SessionProvider>
<Probe />
</SessionProvider>
</QueryClientProvider>
)
})
return {
ctx: () => latest,
queryClient,
unmount: () => act(() => root.unmount()),
}
}
/**
* Flush pending work inside an act() boundary. Drains the microtask queue and
* then yields one macrotask tick, so React Query's notifyManager (which can
* schedule observer notifications on a timer) and any deferred renders settle
* deterministically — microtask-only flushing raced the query→render update.
*/
async function flush() {
await act(async () => {
await Promise.resolve()
await Promise.resolve()
await Promise.resolve()
await new Promise<void>((resolve) => setTimeout(resolve, 0))
})
}
/** Repeatedly flush until `predicate` holds or the budget runs out. */
async function flushUntil(predicate: () => boolean, attempts = 40) {
for (let i = 0; i < attempts; i++) {
if (predicate()) return
await flush()
}
}
/** True when the getSession call is the upgrade (disableCookieCache) read. */
function isUpgradeCall(arg: unknown): boolean {
return Boolean(
arg &&
typeof arg === 'object' &&
'query' in (arg as Record<string, unknown>) &&
(arg as { query?: { disableCookieCache?: boolean } }).query?.disableCookieCache === true
)
}
describe('useSessionQuery', () => {
it('uses an all-rooted key factory and a 5-minute staleTime', () => {
expect(sessionKeys.all).toEqual(['session'])
expect(sessionKeys.detail()).toEqual(['session', 'detail'])
// The hook is exported and reads from the same detail key.
expect(typeof useSessionQuery).toBe('function')
})
})
describe('SessionProvider', () => {
beforeEach(() => {
vi.clearAllMocks()
setSearch('')
})
afterEach(() => {
vi.restoreAllMocks()
})
it('exposes the contract context shape and the loaded session on a normal load', async () => {
mockGetSession.mockResolvedValue({ data: STALE_SESSION })
const h = renderProvider()
await flushUntil(() => h.ctx()?.data != null)
const ctx = h.ctx()
expect(ctx).not.toBeNull()
expect(ctx).toMatchObject({
data: expect.any(Object),
isPending: expect.any(Boolean),
error: null,
})
expect(typeof ctx?.refetch).toBe('function')
expect(ctx?.data).toEqual(STALE_SESSION)
expect(ctx?.isPending).toBe(false)
h.unmount()
})
it('upgrade path: fresh disableCookieCache read wins even when the stale mount query resolves LAST', async () => {
setSearch('?upgraded=true')
const mount = defer<{ data: AppSession }>()
const upgrade = defer<{ data: AppSession }>()
mockGetSession.mockImplementation((arg?: unknown) => {
if (isUpgradeCall(arg)) return upgrade.promise
// Honor the abort signal like the real fetch-backed client: cancelQueries
// aborts the in-flight mount read, so it rejects rather than resolving late.
const signal = (arg as { fetchOptions?: { signal?: AbortSignal } })?.fetchOptions?.signal
signal?.addEventListener('abort', () =>
mount.reject(new DOMException('Aborted', 'AbortError'))
)
return mount.promise
})
// activeOrganizationId is present, so setActive / listCreatorOrganizations are not reached.
const h = renderProvider()
await flush()
// Resolve the fresh upgrade read FIRST. The cancelQueries guard runs before
// setQueryData, cancelling (aborting) the in-flight stale mount query.
await act(async () => {
upgrade.resolve({ data: FRESH_SESSION })
await Promise.resolve()
})
await flushUntil(() => h.queryClient.getQueryData(sessionKeys.detail()) != null)
// Assert on the cache — the contended state cancelQueries + setQueryData
// govern. The fresh value wins; the aborted stale mount read never clobbers it.
expect(h.queryClient.getQueryData(sessionKeys.detail())).toEqual(FRESH_SESSION)
expect(h.queryClient.getQueryData(sessionKeys.detail())).not.toEqual(STALE_SESSION)
h.unmount()
})
it('upgrade path: a failed fresh read keeps the user signed in and still reconciles plan surfaces', async () => {
setSearch('?upgraded=true')
const mount = defer<{ data: AppSession }>()
const upgrade = defer<{ data: AppSession }>()
mockGetSession.mockImplementation((arg?: unknown) =>
isUpgradeCall(arg) ? upgrade.promise : mount.promise
)
const invalidateSpy = vi.spyOn(QueryClient.prototype, 'invalidateQueries')
const invalidatedKeys = () =>
invalidateSpy.mock.calls.map(([arg]) => (arg as { queryKey?: unknown[] })?.queryKey)
const h = renderProvider()
await flush()
// The fresh disableCookieCache read fails.
await act(async () => {
upgrade.reject(new Error('refresh failed'))
await Promise.resolve()
})
await flush()
// The normal cookie-cached mount query lands AFTER the failure.
await act(async () => {
mount.resolve({ data: STALE_SESSION })
await Promise.resolve()
})
await flushUntil(
() =>
h.queryClient.getQueryData(sessionKeys.detail()) != null &&
invalidatedKeys().some((k) => Array.isArray(k) && k[0] === 'subscription')
)
// The valid cookie-cached session is still cached — a failed upgrade refresh
// must not sign the user out, and it must not surface as a session error.
expect(h.queryClient.getQueryData(sessionKeys.detail())).toEqual(STALE_SESSION)
expect(h.queryClient.getQueryState(sessionKeys.detail())?.error ?? null).toBeNull()
// Plan surfaces read server truth, so they still reconcile after the failure.
expect(invalidatedKeys()).toContainEqual(['organizations'])
expect(invalidatedKeys()).toContainEqual(['subscription'])
invalidateSpy.mockRestore()
h.unmount()
})
it('strips the upgraded param from the URL', async () => {
setSearch('?upgraded=true&keep=1')
mockGetSession.mockResolvedValue({ data: FRESH_SESSION })
const h = renderProvider()
await flush()
expect(window.location.search).not.toContain('upgraded')
expect(window.location.search).toContain('keep=1')
h.unmount()
})
})
@@ -0,0 +1,156 @@
'use client'
import type React from 'react'
import { createContext, useEffect, useMemo } from 'react'
import { createLogger } from '@sim/logger'
import { useQueryClient } from '@tanstack/react-query'
import { requestJson } from '@/lib/api/client/request'
import { listCreatorOrganizationsContract } from '@/lib/api/contracts/organizations'
import { client } from '@/lib/auth/auth-client'
import {
type AppSession,
extractSessionDataFromAuthClientResult,
} from '@/lib/auth/session-response'
import { sessionKeys, useSessionQuery } from '@/hooks/queries/session'
export type SessionHookResult = {
data: AppSession
isPending: boolean
error: Error | null
refetch: () => Promise<void>
}
export const SessionContext = createContext<SessionHookResult | null>(null)
const logger = createLogger('SessionProvider')
export function SessionProvider({ children }: { children: React.ReactNode }) {
const queryClient = useQueryClient()
const query = useSessionQuery()
const { data, isPending, error, refetch } = query
useEffect(() => {
let isCancelled = false
const params = new URLSearchParams(window.location.search)
const wasUpgraded = params.get('upgraded') === 'true'
if (!wasUpgraded) {
return
}
params.delete('upgraded')
const newUrl = params.toString()
? `${window.location.pathname}?${params.toString()}`
: window.location.pathname
window.history.replaceState({}, '', newUrl)
const refreshAfterUpgrade = async () => {
const res = await client.getSession({ query: { disableCookieCache: true } })
const fresh = extractSessionDataFromAuthClientResult(res) as AppSession
if (isCancelled) return null
await queryClient.cancelQueries({ queryKey: sessionKeys.detail() })
queryClient.setQueryData(sessionKeys.detail(), fresh)
return fresh
}
const initializeSession = async () => {
let session: AppSession = null
try {
session = await refreshAfterUpgrade()
} catch (e) {
logger.warn('Failed to refresh session after subscription upgrade', { error: e })
}
if (isCancelled) {
return
}
// Refresh the plan surfaces even if the cookie-bypass read above failed: they
// query server truth (not the session cookie cache), so they still reconcile.
queryClient.invalidateQueries({ queryKey: ['organizations'] })
queryClient.invalidateQueries({ queryKey: ['subscription'] })
const activeOrganizationId = session?.session?.activeOrganizationId ?? null
if (!session || activeOrganizationId) {
return
}
try {
const orgData = await requestJson(listCreatorOrganizationsContract, {}).catch(() => null)
if (!orgData) return
const organizationId = orgData.organizations?.[0]?.id
if (!organizationId || isCancelled) {
return
}
await client.organization.setActive({ organizationId })
if (!isCancelled) {
const res = await client.getSession({ query: { disableCookieCache: true } })
const fresh = extractSessionDataFromAuthClientResult(res) as AppSession
if (!isCancelled) {
await queryClient.cancelQueries({ queryKey: sessionKeys.detail() })
queryClient.setQueryData(sessionKeys.detail(), fresh)
}
}
} catch (error) {
logger.warn('Failed to activate organization after subscription upgrade', { error })
}
}
void initializeSession()
return () => {
isCancelled = true
}
}, [queryClient])
useEffect(() => {
if (isPending) return
import('posthog-js')
.then(({ default: posthog }) => {
try {
if (typeof posthog.identify !== 'function') return
if (data?.user) {
posthog.identify(data.user.id, {
email: data.user.email,
name: data.user.name,
email_verified: data.user.emailVerified,
created_at: data.user.createdAt,
})
if (
typeof posthog.startSessionRecording === 'function' &&
typeof posthog.sessionRecordingStarted === 'function' &&
!posthog.sessionRecordingStarted()
) {
posthog.startSessionRecording()
}
} else {
posthog.reset()
}
} catch {}
})
.catch(() => {})
}, [data, isPending])
const value = useMemo<SessionHookResult>(
() => ({
data: data ?? null,
isPending,
error,
refetch: async () => {
await refetch()
},
}),
[data, isPending, error, refetch]
)
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>
}
@@ -0,0 +1,44 @@
'use client'
import { usePathname } from 'next/navigation'
import type { ThemeProviderProps } from 'next-themes'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const pathname = usePathname()
// Force light mode on public/marketing pages, allow user preference elsewhere
const isLightModePage =
pathname === '/' ||
pathname.startsWith('/login') ||
pathname.startsWith('/signup') ||
pathname.startsWith('/reset-password') ||
pathname.startsWith('/sso') ||
pathname.startsWith('/terms') ||
pathname.startsWith('/privacy') ||
pathname.startsWith('/invite') ||
pathname.startsWith('/verify') ||
pathname.startsWith('/changelog') ||
pathname.startsWith('/chat') ||
pathname.startsWith('/blog') ||
pathname.startsWith('/resume') ||
pathname.startsWith('/oauth') ||
pathname.startsWith('/f/') ||
pathname.startsWith('/unsubscribe')
const forcedTheme = isLightModePage ? 'light' : undefined
return (
<NextThemesProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
storageKey='sim-theme'
forcedTheme={forcedTheme}
{...props}
>
{children}
</NextThemesProvider>
)
}
@@ -0,0 +1,11 @@
'use client'
import { Tooltip } from '@sim/emcn'
interface TooltipProviderProps {
children: React.ReactNode
}
export function TooltipProvider({ children }: TooltipProviderProps) {
return <Tooltip.Provider>{children}</Tooltip.Provider>
}