@sanity/sdk 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/index.d.ts +429 -27
  2. package/dist/index.js +657 -266
  3. package/dist/index.js.map +1 -1
  4. package/package.json +4 -3
  5. package/src/_exports/index.ts +18 -3
  6. package/src/auth/authMode.test.ts +56 -0
  7. package/src/auth/authMode.ts +71 -0
  8. package/src/auth/authStore.test.ts +85 -4
  9. package/src/auth/authStore.ts +63 -125
  10. package/src/auth/authStrategy.ts +39 -0
  11. package/src/auth/dashboardAuth.ts +132 -0
  12. package/src/auth/standaloneAuth.ts +109 -0
  13. package/src/auth/studioAuth.ts +217 -0
  14. package/src/auth/studioModeAuth.test.ts +43 -1
  15. package/src/auth/studioModeAuth.ts +10 -1
  16. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +21 -6
  17. package/src/client/clientStore.test.ts +45 -43
  18. package/src/client/clientStore.ts +23 -9
  19. package/src/config/loggingConfig.ts +149 -0
  20. package/src/config/sanityConfig.ts +82 -22
  21. package/src/projection/getProjectionState.ts +6 -5
  22. package/src/projection/projectionQuery.test.ts +38 -55
  23. package/src/projection/projectionQuery.ts +27 -31
  24. package/src/projection/projectionStore.test.ts +4 -4
  25. package/src/projection/projectionStore.ts +3 -2
  26. package/src/projection/resolveProjection.ts +2 -2
  27. package/src/projection/statusQuery.test.ts +35 -0
  28. package/src/projection/statusQuery.ts +71 -0
  29. package/src/projection/subscribeToStateAndFetchBatches.test.ts +63 -50
  30. package/src/projection/subscribeToStateAndFetchBatches.ts +106 -27
  31. package/src/projection/types.ts +12 -0
  32. package/src/projection/util.ts +0 -1
  33. package/src/query/queryStore.test.ts +64 -0
  34. package/src/query/queryStore.ts +33 -11
  35. package/src/releases/getPerspectiveState.test.ts +17 -14
  36. package/src/releases/getPerspectiveState.ts +58 -38
  37. package/src/releases/releasesStore.test.ts +59 -61
  38. package/src/releases/releasesStore.ts +21 -35
  39. package/src/releases/utils/isReleasePerspective.ts +7 -0
  40. package/src/store/createActionBinder.test.ts +211 -1
  41. package/src/store/createActionBinder.ts +102 -13
  42. package/src/store/createSanityInstance.test.ts +85 -1
  43. package/src/store/createSanityInstance.ts +55 -4
  44. package/src/utils/logger-usage-example.md +141 -0
  45. package/src/utils/logger.test.ts +757 -0
  46. package/src/utils/logger.ts +537 -0
@@ -0,0 +1,132 @@
1
+ import {type Subscription} from 'rxjs'
2
+
3
+ import {type StoreContext} from '../store/defineStore'
4
+ import {DEFAULT_BASE} from './authConstants'
5
+ import {AuthStateType} from './authStateType'
6
+ import {type AuthStoreState, type DashboardContext} from './authStore'
7
+ import {type AuthStrategyOptions, type AuthStrategyResult} from './authStrategy'
8
+ import {refreshStampedToken} from './refreshStampedToken'
9
+ import {subscribeToStateAndFetchCurrentUser} from './subscribeToStateAndFetchCurrentUser'
10
+ import {getAuthCode, getTokenFromLocation} from './utils'
11
+
12
+ /**
13
+ * Parses the dashboard context from a location href's `_context` URL parameter.
14
+ * Strips the `sid` property from the parsed context (it's handled separately).
15
+ *
16
+ * @internal
17
+ */
18
+ function parseDashboardContext(locationHref: string): DashboardContext {
19
+ try {
20
+ const parsedUrl = new URL(locationHref, DEFAULT_BASE)
21
+ const contextParam = parsedUrl.searchParams.get('_context')
22
+ if (contextParam) {
23
+ const parsedContext = JSON.parse(contextParam)
24
+
25
+ if (
26
+ parsedContext &&
27
+ typeof parsedContext === 'object' &&
28
+ !Array.isArray(parsedContext) &&
29
+ Object.keys(parsedContext).length > 0
30
+ ) {
31
+ // Explicitly remove the 'sid' property before assigning
32
+ delete parsedContext.sid
33
+ return parsedContext as DashboardContext
34
+ }
35
+ }
36
+ } catch (err) {
37
+ // eslint-disable-next-line no-console
38
+ console.error('Failed to parse dashboard context from initial location:', err)
39
+ }
40
+ return {} // Empty dashboard context if parsing fails
41
+ }
42
+
43
+ /**
44
+ * Resolves the initial auth state for Dashboard mode.
45
+ *
46
+ * In dashboard mode the token is provided by the parent frame via Comlink,
47
+ * so the SDK starts in a `LOGGED_OUT` state and waits for the token to arrive.
48
+ * The `_context` URL parameter provides dashboard metadata (orgId, mode, env).
49
+ *
50
+ * A provided token (`auth.token`) or an auth code/callback still takes
51
+ * precedence, even when running inside the dashboard.
52
+ *
53
+ * @internal
54
+ */
55
+ export function getDashboardInitialState(options: AuthStrategyOptions): AuthStrategyResult {
56
+ const {authConfig, initialLocationHref} = options
57
+ const providedToken = authConfig.token
58
+ const callbackUrl = authConfig.callbackUrl
59
+ const storageKey = '__sanity_auth_token'
60
+
61
+ const dashboardContext = parseDashboardContext(initialLocationHref)
62
+
63
+ // Dashboard does NOT use localStorage — token comes from the parent frame
64
+ const storageArea = undefined
65
+
66
+ // Provided token always wins, even in dashboard
67
+ if (providedToken) {
68
+ return {
69
+ authState: {type: AuthStateType.LOGGED_IN, token: providedToken, currentUser: null},
70
+ storageKey,
71
+ storageArea,
72
+ authMethod: undefined,
73
+ dashboardContext,
74
+ }
75
+ }
76
+
77
+ // Check for auth code or token-from-location (callback handling)
78
+ if (getAuthCode(callbackUrl, initialLocationHref) || getTokenFromLocation(initialLocationHref)) {
79
+ return {
80
+ authState: {type: AuthStateType.LOGGING_IN, isExchangingToken: false},
81
+ storageKey,
82
+ storageArea,
83
+ authMethod: undefined,
84
+ dashboardContext,
85
+ }
86
+ }
87
+
88
+ // Default: logged out, waiting for Comlink to provide token
89
+ return {
90
+ authState: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
91
+ storageKey,
92
+ storageArea,
93
+ authMethod: undefined,
94
+ dashboardContext,
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Initialize Dashboard auth subscriptions:
100
+ * - Subscribe to state changes and fetch current user
101
+ * - Start stamped token refresh
102
+ *
103
+ * Note: storage events are NOT subscribed in dashboard mode since
104
+ * the dashboard does not use localStorage for token persistence.
105
+ *
106
+ * @internal
107
+ */
108
+ export function initializeDashboardAuth(
109
+ context: StoreContext<AuthStoreState>,
110
+ tokenRefresherRunning: boolean,
111
+ ): {dispose: () => void; tokenRefresherStarted: boolean} {
112
+ const subscriptions: Subscription[] = []
113
+ let startedRefresher = false
114
+
115
+ subscriptions.push(subscribeToStateAndFetchCurrentUser(context, {useProjectHostname: false}))
116
+
117
+ // Dashboard does not subscribe to storage events
118
+
119
+ if (!tokenRefresherRunning) {
120
+ startedRefresher = true
121
+ subscriptions.push(refreshStampedToken(context))
122
+ }
123
+
124
+ return {
125
+ dispose: () => {
126
+ for (const subscription of subscriptions) {
127
+ subscription.unsubscribe()
128
+ }
129
+ },
130
+ tokenRefresherStarted: startedRefresher,
131
+ }
132
+ }
@@ -0,0 +1,109 @@
1
+ import {type Subscription} from 'rxjs'
2
+
3
+ import {type StoreContext} from '../store/defineStore'
4
+ import {AuthStateType} from './authStateType'
5
+ import {type AuthStoreState} from './authStore'
6
+ import {type AuthStrategyOptions, type AuthStrategyResult} from './authStrategy'
7
+ import {refreshStampedToken} from './refreshStampedToken'
8
+ import {subscribeToStateAndFetchCurrentUser} from './subscribeToStateAndFetchCurrentUser'
9
+ import {subscribeToStorageEventsAndSetToken} from './subscribeToStorageEventsAndSetToken'
10
+ import {getAuthCode, getDefaultStorage, getTokenFromLocation, getTokenFromStorage} from './utils'
11
+
12
+ /**
13
+ * Resolves the initial auth state for Standalone mode.
14
+ *
15
+ * Token discovery order:
16
+ * 1. Provided token (`auth.token` in config)
17
+ * 2. Auth code or token from location (OAuth callback)
18
+ * 3. localStorage: `__sanity_auth_token`
19
+ * 4. Falls back to `LOGGED_OUT`
20
+ *
21
+ * @internal
22
+ */
23
+ export function getStandaloneInitialState(options: AuthStrategyOptions): AuthStrategyResult {
24
+ const {authConfig, initialLocationHref} = options
25
+ const providedToken = authConfig.token
26
+ const callbackUrl = authConfig.callbackUrl
27
+ const storageKey = '__sanity_auth_token'
28
+ const storageArea = authConfig.storageArea ?? getDefaultStorage()
29
+
30
+ // Provided token always wins
31
+ if (providedToken) {
32
+ return {
33
+ authState: {type: AuthStateType.LOGGED_IN, token: providedToken, currentUser: null},
34
+ storageKey,
35
+ storageArea,
36
+ authMethod: undefined,
37
+ dashboardContext: {},
38
+ }
39
+ }
40
+
41
+ // Check for auth code or token-from-location (OAuth callback)
42
+ if (getAuthCode(callbackUrl, initialLocationHref) || getTokenFromLocation(initialLocationHref)) {
43
+ return {
44
+ authState: {type: AuthStateType.LOGGING_IN, isExchangingToken: false},
45
+ storageKey,
46
+ storageArea,
47
+ authMethod: undefined,
48
+ dashboardContext: {},
49
+ }
50
+ }
51
+
52
+ // Try localStorage
53
+ const token = getTokenFromStorage(storageArea, storageKey)
54
+ if (token) {
55
+ return {
56
+ authState: {type: AuthStateType.LOGGED_IN, token, currentUser: null},
57
+ storageKey,
58
+ storageArea,
59
+ authMethod: 'localstorage',
60
+ dashboardContext: {},
61
+ }
62
+ }
63
+
64
+ // No token found
65
+ return {
66
+ authState: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
67
+ storageKey,
68
+ storageArea,
69
+ authMethod: undefined,
70
+ dashboardContext: {},
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Initialize Standalone auth subscriptions:
76
+ * - Subscribe to state changes and fetch current user
77
+ * - Subscribe to storage events for token key
78
+ * - Start stamped token refresh
79
+ *
80
+ * @internal
81
+ */
82
+ export function initializeStandaloneAuth(
83
+ context: StoreContext<AuthStoreState>,
84
+ tokenRefresherRunning: boolean,
85
+ ): {dispose: () => void; tokenRefresherStarted: boolean} {
86
+ const subscriptions: Subscription[] = []
87
+ let startedRefresher = false
88
+
89
+ subscriptions.push(subscribeToStateAndFetchCurrentUser(context, {useProjectHostname: false}))
90
+
91
+ const storageArea = context.state.get().options?.storageArea
92
+ if (storageArea) {
93
+ subscriptions.push(subscribeToStorageEventsAndSetToken(context))
94
+ }
95
+
96
+ if (!tokenRefresherRunning) {
97
+ startedRefresher = true
98
+ subscriptions.push(refreshStampedToken(context))
99
+ }
100
+
101
+ return {
102
+ dispose: () => {
103
+ for (const subscription of subscriptions) {
104
+ subscription.unsubscribe()
105
+ }
106
+ },
107
+ tokenRefresherStarted: startedRefresher,
108
+ }
109
+ }
@@ -0,0 +1,217 @@
1
+ import {type Subscription} from 'rxjs'
2
+
3
+ import {type TokenSource} from '../config/sanityConfig'
4
+ import {type StoreContext} from '../store/defineStore'
5
+ import {AuthStateType} from './authStateType'
6
+ import {type AuthStoreState, type LoggedInAuthState} from './authStore'
7
+ import {type AuthStrategyOptions, type AuthStrategyResult} from './authStrategy'
8
+ import {refreshStampedToken} from './refreshStampedToken'
9
+ import {checkForCookieAuth, getStudioTokenFromLocalStorage} from './studioModeAuth'
10
+ import {subscribeToStateAndFetchCurrentUser} from './subscribeToStateAndFetchCurrentUser'
11
+ import {subscribeToStorageEventsAndSetToken} from './subscribeToStorageEventsAndSetToken'
12
+ import {getDefaultStorage} from './utils'
13
+
14
+ /**
15
+ * Resolves the initial auth state for Studio mode.
16
+ *
17
+ * When a `tokenSource` is provided (via `config.studio.auth.token`), the
18
+ * initial state is `LOGGING_IN` — the actual token arrives asynchronously
19
+ * via the subscription set up in `initializeStudioAuth`.
20
+ *
21
+ * Fallback token discovery order (no tokenSource):
22
+ * 1. Provided token (`auth.token` in config)
23
+ * 2. localStorage: `__studio_auth_token_${projectId}`
24
+ * 3. Falls back to `LOGGED_OUT` (cookie auth is checked async in `initializeStudioAuth`)
25
+ *
26
+ * @internal
27
+ */
28
+ export function getStudioInitialState(options: AuthStrategyOptions): AuthStrategyResult {
29
+ const {authConfig, projectId, tokenSource} = options
30
+ const storageArea = authConfig.storageArea ?? getDefaultStorage()
31
+ const studioStorageKey = `__studio_auth_token_${projectId ?? ''}`
32
+
33
+ // When a reactive token source is available, start in LOGGING_IN state.
34
+ // The actual token will arrive via subscription in initializeStudioAuth.
35
+ if (tokenSource) {
36
+ return {
37
+ authState: {type: AuthStateType.LOGGING_IN, isExchangingToken: false},
38
+ storageKey: studioStorageKey,
39
+ storageArea,
40
+ authMethod: undefined,
41
+ dashboardContext: {},
42
+ }
43
+ }
44
+
45
+ // Fallback: discover token from config or localStorage
46
+ const providedToken = authConfig.token
47
+
48
+ // Check localStorage first — mirrors original authStore behavior where
49
+ // the localStorage read always runs before the providedToken check.
50
+ let authMethod: AuthStrategyResult['authMethod'] = undefined
51
+ const token = getStudioTokenFromLocalStorage(storageArea, studioStorageKey)
52
+ if (token) {
53
+ authMethod = 'localstorage'
54
+ }
55
+
56
+ if (providedToken) {
57
+ return {
58
+ authState: {type: AuthStateType.LOGGED_IN, token: providedToken, currentUser: null},
59
+ storageKey: studioStorageKey,
60
+ storageArea,
61
+ authMethod,
62
+ dashboardContext: {},
63
+ }
64
+ }
65
+
66
+ if (token) {
67
+ return {
68
+ authState: {type: AuthStateType.LOGGED_IN, token, currentUser: null},
69
+ storageKey: studioStorageKey,
70
+ storageArea,
71
+ authMethod: 'localstorage',
72
+ dashboardContext: {},
73
+ }
74
+ }
75
+
76
+ // No token found — start logged out, cookie auth will be checked asynchronously
77
+ return {
78
+ authState: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
79
+ storageKey: studioStorageKey,
80
+ storageArea,
81
+ authMethod: undefined,
82
+ dashboardContext: {},
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Initialize Studio auth subscriptions.
88
+ *
89
+ * When a `tokenSource` is available (reactive path), subscribes to it for
90
+ * ongoing token sync. The Studio is the token authority — no independent
91
+ * token refresh or cookie auth probing is needed.
92
+ *
93
+ * Fallback path (no tokenSource):
94
+ * - Subscribe to state changes and fetch current user
95
+ * - Subscribe to storage events for studio token key
96
+ * - Check for cookie auth asynchronously if no token was found
97
+ * - Start stamped token refresh
98
+ *
99
+ * @internal
100
+ */
101
+ export function initializeStudioAuth(
102
+ context: StoreContext<AuthStoreState>,
103
+ tokenRefresherRunning: boolean,
104
+ ): {dispose: () => void; tokenRefresherStarted: boolean} {
105
+ const tokenSource = context.instance.config.studio?.auth?.token
106
+
107
+ // Reactive token path — Studio provides the token
108
+ if (tokenSource) {
109
+ return initializeWithTokenSource(context, tokenSource)
110
+ }
111
+
112
+ // Fallback path — discover token from localStorage/cookies
113
+ return initializeWithFallback(context, tokenRefresherRunning)
114
+ }
115
+
116
+ /**
117
+ * Subscribe to a reactive token source from the Studio workspace.
118
+ * The Studio is the single authority for auth — the SDK does not run
119
+ * its own token refresher or cookie auth checks.
120
+ */
121
+ function initializeWithTokenSource(
122
+ context: StoreContext<AuthStoreState>,
123
+ tokenSource: TokenSource,
124
+ ): {dispose: () => void; tokenRefresherStarted: boolean} {
125
+ const subscriptions: Subscription[] = []
126
+
127
+ // Subscribe to the current user fetcher — runs whenever auth state changes
128
+ subscriptions.push(subscribeToStateAndFetchCurrentUser(context, {useProjectHostname: true}))
129
+
130
+ // Subscribe to the Studio's token stream
131
+ const tokenSub = tokenSource.subscribe({
132
+ next: (token) => {
133
+ const {state} = context
134
+ if (token) {
135
+ state.set('studioTokenSource', (prev) => ({
136
+ options: {...prev.options, authMethod: undefined},
137
+ authState: {type: AuthStateType.LOGGED_IN, token, currentUser: null},
138
+ }))
139
+ } else {
140
+ state.set('studioTokenSourceLoggedOut', (prev) => ({
141
+ options: {...prev.options, authMethod: undefined},
142
+ authState: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
143
+ }))
144
+ }
145
+ },
146
+ })
147
+
148
+ return {
149
+ dispose: () => {
150
+ tokenSub.unsubscribe()
151
+ for (const subscription of subscriptions) {
152
+ subscription.unsubscribe()
153
+ }
154
+ },
155
+ // Studio handles token refresh — do not start the SDK's refresher
156
+ tokenRefresherStarted: false,
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Fallback initialization when no reactive token source is available.
162
+ * Uses localStorage/cookie discovery (existing behavior).
163
+ */
164
+ function initializeWithFallback(
165
+ context: StoreContext<AuthStoreState>,
166
+ tokenRefresherRunning: boolean,
167
+ ): {dispose: () => void; tokenRefresherStarted: boolean} {
168
+ const subscriptions: Subscription[] = []
169
+ let startedRefresher = false
170
+
171
+ subscriptions.push(subscribeToStateAndFetchCurrentUser(context, {useProjectHostname: true}))
172
+
173
+ const storageArea = context.state.get().options?.storageArea
174
+ if (storageArea) {
175
+ subscriptions.push(subscribeToStorageEventsAndSetToken(context))
176
+ }
177
+
178
+ // If no token found during getInitialState, try cookie auth asynchronously
179
+ try {
180
+ const {instance, state} = context
181
+ const token: string | null =
182
+ state.get().authState?.type === AuthStateType.LOGGED_IN
183
+ ? (state.get().authState as LoggedInAuthState).token
184
+ : null
185
+
186
+ if (!token) {
187
+ const projectIdValue = instance.config.projectId
188
+ const clientFactory = state.get().options.clientFactory
189
+ checkForCookieAuth(projectIdValue, clientFactory).then((isCookieAuthEnabled) => {
190
+ if (!isCookieAuthEnabled) return
191
+ state.set('enableCookieAuth', (prev) => ({
192
+ options: {...prev.options, authMethod: 'cookie'},
193
+ authState:
194
+ prev.authState.type === AuthStateType.LOGGED_IN
195
+ ? prev.authState
196
+ : {type: AuthStateType.LOGGED_IN, token: '', currentUser: null},
197
+ }))
198
+ })
199
+ }
200
+ } catch {
201
+ // best-effort cookie detection
202
+ }
203
+
204
+ if (!tokenRefresherRunning) {
205
+ startedRefresher = true
206
+ subscriptions.push(refreshStampedToken(context))
207
+ }
208
+
209
+ return {
210
+ dispose: () => {
211
+ for (const subscription of subscriptions) {
212
+ subscription.unsubscribe()
213
+ }
214
+ },
215
+ tokenRefresherStarted: startedRefresher,
216
+ }
217
+ }
@@ -21,7 +21,7 @@ describe('checkForCookieAuth', () => {
21
21
 
22
22
  await checkForCookieAuth(projectId, clientFactory)
23
23
 
24
- expect(clientFactory).toHaveBeenCalledWith({projectId, useCdn: false})
24
+ expect(clientFactory).toHaveBeenCalledWith(expect.objectContaining({projectId, useCdn: false}))
25
25
  })
26
26
 
27
27
  it('should return true if client request returns a user with id', async () => {
@@ -64,6 +64,48 @@ describe('checkForCookieAuth', () => {
64
64
 
65
65
  expect(result).toBe(false)
66
66
  })
67
+
68
+ it('should return false if client request returns null', async () => {
69
+ const projectId = 'test-project'
70
+ const mockClient = {
71
+ request: vi.fn().mockResolvedValue(null),
72
+ } as unknown as SanityClient
73
+ const clientFactory = vi.fn().mockReturnValue(mockClient)
74
+
75
+ const result = await checkForCookieAuth(projectId, clientFactory)
76
+
77
+ expect(result).toBe(false)
78
+ })
79
+
80
+ it('should return false if client request returns a non-object', async () => {
81
+ const projectId = 'test-project'
82
+ const mockClient = {
83
+ request: vi.fn().mockResolvedValue('unexpected-string'),
84
+ } as unknown as SanityClient
85
+ const clientFactory = vi.fn().mockReturnValue(mockClient)
86
+
87
+ const result = await checkForCookieAuth(projectId, clientFactory)
88
+
89
+ expect(result).toBe(false)
90
+ })
91
+
92
+ it('should pass timeout to client factory', async () => {
93
+ const projectId = 'test-project'
94
+ const mockClient = {
95
+ request: vi.fn().mockResolvedValue({id: 'user-id'}),
96
+ } as unknown as SanityClient
97
+ const clientFactory = vi.fn().mockReturnValue(mockClient)
98
+
99
+ await checkForCookieAuth(projectId, clientFactory)
100
+
101
+ expect(clientFactory).toHaveBeenCalledWith(
102
+ expect.objectContaining({
103
+ projectId,
104
+ useCdn: false,
105
+ timeout: 10_000,
106
+ }),
107
+ )
108
+ })
67
109
  })
68
110
 
69
111
  describe('getStudioTokenFromLocalStorage', () => {
@@ -2,6 +2,13 @@ import {type ClientConfig, type SanityClient} from '@sanity/client'
2
2
 
3
3
  import {getTokenFromStorage} from './utils'
4
4
 
5
+ /**
6
+ * Cookie auth is a best-effort probe — if the API doesn't respond quickly,
7
+ * the user likely isn't cookie-authenticated and we should move on rather
8
+ * than block the auth flow indefinitely.
9
+ */
10
+ const COOKIE_AUTH_TIMEOUT_MS = 10_000
11
+
5
12
  /**
6
13
  * Attempts to check for cookie auth by making a withCredentials request to the users endpoint.
7
14
  * @param projectId - The project ID to check for cookie auth.
@@ -18,13 +25,15 @@ export async function checkForCookieAuth(
18
25
  const client = clientFactory({
19
26
  projectId,
20
27
  useCdn: false,
28
+ requestTagPrefix: 'sdk',
29
+ timeout: COOKIE_AUTH_TIMEOUT_MS,
21
30
  })
22
31
  const user = await client.request({
23
32
  uri: '/users/me',
24
33
  withCredentials: true,
25
34
  tag: 'users.get-current',
26
35
  })
27
- return typeof user?.id === 'string'
36
+ return user != null && typeof user === 'object' && typeof user.id === 'string'
28
37
  } catch {
29
38
  return false
30
39
  }
@@ -3,20 +3,35 @@ import {distinctUntilChanged, filter, map, type Subscription, switchMap} from 'r
3
3
 
4
4
  import {type StoreContext} from '../store/defineStore'
5
5
  import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
6
+ import {isStudioConfig} from './authMode'
6
7
  import {AuthStateType} from './authStateType'
7
8
  import {type AuthMethodOptions, type AuthState, type AuthStoreState} from './authStore'
8
9
 
9
- export const subscribeToStateAndFetchCurrentUser = ({
10
- state,
11
- instance,
12
- }: StoreContext<AuthStoreState>): Subscription => {
10
+ /**
11
+ * Subscribes to auth state changes and fetches the current user when
12
+ * the state transitions to `LOGGED_IN` without a `currentUser`.
13
+ *
14
+ * @param context - The store context for the auth store.
15
+ * @param fetchOptions - Configuration options. When `useProjectHostname` is
16
+ * `true`, requests use the project-specific hostname (required for Studio
17
+ * cookie auth). Set to `true` for studio mode, `false` otherwise.
18
+ *
19
+ * @internal
20
+ */
21
+ export const subscribeToStateAndFetchCurrentUser = (
22
+ {state, instance}: StoreContext<AuthStoreState>,
23
+ fetchOptions?: {useProjectHostname?: boolean},
24
+ ): Subscription => {
13
25
  const {clientFactory, apiHost} = state.get().options
14
- const useProjectHostname = !!instance.config.studioMode?.enabled
26
+ const useProjectHostname = fetchOptions?.useProjectHostname ?? isStudioConfig(instance.config)
15
27
  const projectId = instance.config.projectId
16
28
 
17
29
  const currentUser$ = state.observable
18
30
  .pipe(
19
- map(({authState, options}) => ({authState, authMethod: options.authMethod})),
31
+ map(({authState, options: storeOptions}) => ({
32
+ authState,
33
+ authMethod: storeOptions.authMethod,
34
+ })),
20
35
  filter(
21
36
  (
22
37
  value,