@sanity/sdk 2.6.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 (39) hide show
  1. package/dist/index.d.ts +124 -13
  2. package/dist/index.js +468 -243
  3. package/dist/index.js.map +1 -1
  4. package/package.json +5 -4
  5. package/src/_exports/index.ts +3 -0
  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/config/sanityConfig.ts +48 -7
  18. package/src/projection/getProjectionState.ts +6 -5
  19. package/src/projection/projectionQuery.test.ts +38 -55
  20. package/src/projection/projectionQuery.ts +27 -31
  21. package/src/projection/projectionStore.test.ts +4 -4
  22. package/src/projection/projectionStore.ts +3 -2
  23. package/src/projection/resolveProjection.ts +2 -2
  24. package/src/projection/statusQuery.test.ts +35 -0
  25. package/src/projection/statusQuery.ts +71 -0
  26. package/src/projection/subscribeToStateAndFetchBatches.test.ts +63 -50
  27. package/src/projection/subscribeToStateAndFetchBatches.ts +106 -27
  28. package/src/projection/types.ts +12 -0
  29. package/src/projection/util.ts +0 -1
  30. package/src/query/queryStore.test.ts +64 -0
  31. package/src/query/queryStore.ts +30 -10
  32. package/src/releases/getPerspectiveState.test.ts +17 -14
  33. package/src/releases/getPerspectiveState.ts +58 -38
  34. package/src/releases/releasesStore.test.ts +59 -61
  35. package/src/releases/releasesStore.ts +21 -35
  36. package/src/releases/utils/isReleasePerspective.ts +7 -0
  37. package/src/store/createActionBinder.test.ts +211 -1
  38. package/src/store/createActionBinder.ts +95 -17
  39. package/src/store/createSanityInstance.ts +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "private": false,
5
5
  "description": "Sanity SDK",
6
6
  "keywords": [
@@ -44,10 +44,11 @@
44
44
  "prettier": "@sanity/prettier-config",
45
45
  "dependencies": {
46
46
  "@sanity/bifur-client": "^0.4.1",
47
- "@sanity/client": "^7.12.0",
47
+ "@sanity/client": "^7.14.1",
48
48
  "@sanity/comlink": "^3.0.4",
49
49
  "@sanity/diff-match-patch": "^3.2.0",
50
50
  "@sanity/diff-patch": "^6.0.0",
51
+ "@sanity/id-utils": "^1.0.0",
51
52
  "@sanity/json-match": "^1.0.5",
52
53
  "@sanity/message-protocol": "^0.18.0",
53
54
  "@sanity/mutate": "^0.12.4",
@@ -72,10 +73,10 @@
72
73
  "vite": "^6.3.4",
73
74
  "vitest": "^3.2.4",
74
75
  "@repo/config-eslint": "0.0.0",
76
+ "@repo/config-test": "0.0.1",
75
77
  "@repo/package.bundle": "3.82.0",
76
- "@repo/tsconfig": "0.0.1",
77
78
  "@repo/package.config": "0.0.1",
78
- "@repo/config-test": "0.0.1"
79
+ "@repo/tsconfig": "0.0.1"
79
80
  },
80
81
  "engines": {
81
82
  "node": ">=20.19"
@@ -24,6 +24,7 @@ export {
24
24
  agentTransform,
25
25
  agentTranslate,
26
26
  } from '../agent/agentActions'
27
+ export {isStudioConfig} from '../auth/authMode'
27
28
  export {AuthStateType} from '../auth/authStateType'
28
29
  export {
29
30
  type AuthState,
@@ -99,6 +100,8 @@ export {
99
100
  type ProjectHandle,
100
101
  type ReleasePerspective,
101
102
  type SanityConfig,
103
+ type StudioConfig,
104
+ type TokenSource,
102
105
  } from '../config/sanityConfig'
103
106
  export {getDatasetsState, resolveDatasets} from '../datasets/datasets'
104
107
  export {
@@ -0,0 +1,56 @@
1
+ import {describe, expect, it} from 'vitest'
2
+
3
+ import {type SanityConfig} from '../config/sanityConfig'
4
+ import {isStudioConfig, resolveAuthMode} from './authMode'
5
+
6
+ describe('resolveAuthMode', () => {
7
+ it('returns "studio" when studio config is provided', () => {
8
+ const config: SanityConfig = {studio: {}}
9
+ expect(resolveAuthMode(config, 'https://example.com')).toBe('studio')
10
+ })
11
+
12
+ it('returns "studio" when deprecated studioMode.enabled is true', () => {
13
+ const config: SanityConfig = {studioMode: {enabled: true}}
14
+ expect(resolveAuthMode(config, 'https://example.com')).toBe('studio')
15
+ })
16
+
17
+ it('returns "dashboard" when _context URL param has a non-empty JSON object', () => {
18
+ const context = encodeURIComponent(JSON.stringify({orgId: '123'}))
19
+ const href = `https://example.com?_context=${context}`
20
+ expect(resolveAuthMode({}, href)).toBe('dashboard')
21
+ })
22
+
23
+ it('returns "standalone" by default', () => {
24
+ expect(resolveAuthMode({}, 'https://example.com')).toBe('standalone')
25
+ })
26
+
27
+ it('prefers studio config over studioMode', () => {
28
+ const config: SanityConfig = {
29
+ studio: {},
30
+ studioMode: {enabled: true},
31
+ }
32
+ expect(resolveAuthMode(config, 'https://example.com')).toBe('studio')
33
+ })
34
+ })
35
+
36
+ describe('isStudioConfig', () => {
37
+ it('returns true when studio config is provided', () => {
38
+ expect(isStudioConfig({studio: {}})).toBe(true)
39
+ })
40
+
41
+ it('returns true when deprecated studioMode.enabled is true', () => {
42
+ expect(isStudioConfig({studioMode: {enabled: true}})).toBe(true)
43
+ })
44
+
45
+ it('returns false when studioMode.enabled is false', () => {
46
+ expect(isStudioConfig({studioMode: {enabled: false}})).toBe(false)
47
+ })
48
+
49
+ it('returns false for empty config', () => {
50
+ expect(isStudioConfig({})).toBe(false)
51
+ })
52
+
53
+ it('returns true when both studio and studioMode are present', () => {
54
+ expect(isStudioConfig({studio: {}, studioMode: {enabled: true}})).toBe(true)
55
+ })
56
+ })
@@ -0,0 +1,71 @@
1
+ import {type SanityConfig} from '../config/sanityConfig'
2
+ import {DEFAULT_BASE} from './authConstants'
3
+
4
+ /**
5
+ * The runtime auth mode determines which authentication strategy the SDK uses.
6
+ *
7
+ * - `studio` — Running inside Sanity Studio. Token is discovered from
8
+ * Studio's localStorage entry or via cookie auth.
9
+ * - `dashboard` — Running inside the Sanity Dashboard iframe. Token is
10
+ * provided by the parent frame via Comlink.
11
+ * - `standalone` — Running as an independent app. Token comes from
12
+ * localStorage or the OAuth login flow.
13
+ *
14
+ * @internal
15
+ */
16
+ type AuthMode = 'studio' | 'dashboard' | 'standalone'
17
+
18
+ /**
19
+ * Determines the auth mode from instance config and environment.
20
+ *
21
+ * Priority:
22
+ * 1. `studio` config provided → `'studio'`
23
+ * 2. `studioMode.enabled` in config (deprecated) → `'studio'`
24
+ * 3. Dashboard context detected (`_context` URL param with content) → `'dashboard'`
25
+ * 4. Otherwise → `'standalone'`
26
+ *
27
+ * @internal
28
+ */
29
+ export function resolveAuthMode(config: SanityConfig, locationHref: string): AuthMode {
30
+ if (isStudioConfig(config)) return 'studio'
31
+ if (detectDashboardContext(locationHref)) return 'dashboard'
32
+ return 'standalone'
33
+ }
34
+
35
+ /**
36
+ * Returns `true` when the config indicates the SDK is running inside a Studio.
37
+ * Checks the new `studio` field first, then falls back to the deprecated
38
+ * `studioMode.enabled` for backwards compatibility.
39
+ *
40
+ * @internal
41
+ */
42
+ export function isStudioConfig(config: SanityConfig): boolean {
43
+ return !!config.studio || !!config.studioMode?.enabled
44
+ }
45
+
46
+ /**
47
+ * Checks whether the given location href contains a `_context` URL parameter
48
+ * with a non-empty JSON object, indicating the SDK is running inside the
49
+ * Sanity Dashboard.
50
+ *
51
+ * @internal
52
+ */
53
+ function detectDashboardContext(locationHref: string): boolean {
54
+ try {
55
+ const parsedUrl = new URL(locationHref, DEFAULT_BASE)
56
+ const contextParam = parsedUrl.searchParams.get('_context')
57
+ if (!contextParam) return false
58
+
59
+ const parsed: unknown = JSON.parse(contextParam)
60
+ return (
61
+ typeof parsed === 'object' &&
62
+ parsed !== null &&
63
+ !Array.isArray(parsed) &&
64
+ Object.keys(parsed as Record<string, unknown>).length > 0
65
+ )
66
+ } catch (err) {
67
+ // eslint-disable-next-line no-console
68
+ console.error('Failed to parse dashboard context from initial location:', err)
69
+ return false
70
+ }
71
+ }
@@ -153,6 +153,34 @@ describe('authStore', () => {
153
153
  errorSpy.mockRestore()
154
154
  })
155
155
 
156
+ it('rejects array _context and falls back to standalone mode', () => {
157
+ const initialLocationHref = `https://example.com/?_context=${encodeURIComponent(JSON.stringify(['a', 'b']))}`
158
+ instance = createSanityInstance({
159
+ projectId: 'p',
160
+ dataset: 'd',
161
+ auth: {initialLocationHref},
162
+ })
163
+
164
+ const {dashboardContext, authState} = authStore.getInitialState(instance, null)
165
+ // Array should not be treated as dashboard context — falls through to standalone
166
+ expect(dashboardContext).toStrictEqual({})
167
+ expect(authState.type).toBe(AuthStateType.LOGGED_OUT)
168
+ })
169
+
170
+ it('rejects empty object _context and falls back to standalone mode', () => {
171
+ const initialLocationHref = `https://example.com/?_context=${encodeURIComponent(JSON.stringify({}))}`
172
+ instance = createSanityInstance({
173
+ projectId: 'p',
174
+ dataset: 'd',
175
+ auth: {initialLocationHref},
176
+ })
177
+
178
+ const {dashboardContext, authState} = authStore.getInitialState(instance, null)
179
+ // Empty object not treated as dashboard — falls through to standalone
180
+ expect(dashboardContext).toStrictEqual({})
181
+ expect(authState.type).toBe(AuthStateType.LOGGED_OUT)
182
+ })
183
+
156
184
  it('sets to logged in if provided token is present (even in dashboard)', () => {
157
185
  const token = 'provided-token'
158
186
  const context = {mode: 'dashboard'}
@@ -234,7 +262,7 @@ describe('authStore', () => {
234
262
  expect(dashboardContext).toStrictEqual({})
235
263
  })
236
264
 
237
- it('sets to logged in using studio token when studio mode is enabled and token exists', () => {
265
+ it('sets to logged in using studio token when studio config is provided and token exists', () => {
238
266
  const studioToken = 'studio-token'
239
267
  const projectId = 'studio-project'
240
268
  const studioStorageKey = `__studio_auth_token_${projectId}`
@@ -248,7 +276,7 @@ describe('authStore', () => {
248
276
  instance = createSanityInstance({
249
277
  projectId,
250
278
  dataset: 'd',
251
- studioMode: {enabled: true},
279
+ studio: {},
252
280
  auth: {storageArea: mockStorage}, // Provide mock storage
253
281
  })
254
282
 
@@ -258,7 +286,7 @@ describe('authStore', () => {
258
286
  expect(options.authMethod).toBe('localstorage')
259
287
  })
260
288
 
261
- it('checks for cookie auth during initialize when studio mode is enabled and no studio token exists', () => {
289
+ it('checks for cookie auth during initialize when studio config is provided and no studio token exists', () => {
262
290
  const projectId = 'studio-project'
263
291
  const studioStorageKey = `__studio_auth_token_${projectId}`
264
292
  const mockStorage = {
@@ -272,7 +300,7 @@ describe('authStore', () => {
272
300
  instance = createSanityInstance({
273
301
  projectId,
274
302
  dataset: 'd',
275
- studioMode: {enabled: true},
303
+ studio: {},
276
304
  auth: {storageArea: mockStorage},
277
305
  })
278
306
 
@@ -287,6 +315,59 @@ describe('authStore', () => {
287
315
  expect(checkForCookieAuth).toHaveBeenCalledWith(projectId, expect.any(Function))
288
316
  })
289
317
 
318
+ it('starts in LOGGING_IN state when studio config with tokenSource is provided', () => {
319
+ const mockTokenSource = {
320
+ subscribe: vi.fn(() => ({unsubscribe: vi.fn()})),
321
+ }
322
+
323
+ instance = createSanityInstance({
324
+ projectId: 'studio-project',
325
+ dataset: 'production',
326
+ studio: {
327
+ auth: {token: mockTokenSource},
328
+ },
329
+ })
330
+
331
+ const {authState} = authStore.getInitialState(instance, null)
332
+ expect(authState.type).toBe(AuthStateType.LOGGING_IN)
333
+ // Should not try localStorage or cookie auth
334
+ expect(getStudioTokenFromLocalStorage).not.toHaveBeenCalled()
335
+ })
336
+
337
+ it('resolves to studio mode when studio config is provided (without studioMode flag)', () => {
338
+ instance = createSanityInstance({
339
+ projectId: 'studio-project',
340
+ dataset: 'production',
341
+ studio: {},
342
+ })
343
+
344
+ const {authState} = authStore.getInitialState(instance, null)
345
+ // Without tokenSource, falls back to localStorage discovery like studioMode
346
+ expect(authState.type).toBe(AuthStateType.LOGGED_OUT)
347
+ expect(getStudioTokenFromLocalStorage).toHaveBeenCalled()
348
+ })
349
+
350
+ it('subscribes to tokenSource during initialize when studio config is provided', () => {
351
+ const mockUnsubscribe = vi.fn()
352
+ const mockSubscribe = vi.fn(() => ({unsubscribe: mockUnsubscribe}))
353
+ const mockTokenSource = {subscribe: mockSubscribe}
354
+
355
+ instance = createSanityInstance({
356
+ projectId: 'studio-project',
357
+ dataset: 'production',
358
+ studio: {
359
+ auth: {token: mockTokenSource},
360
+ },
361
+ })
362
+
363
+ // Trigger store creation + initialize
364
+ getAuthState(instance)
365
+
366
+ expect(mockSubscribe).toHaveBeenCalledWith({next: expect.any(Function)})
367
+ // Should NOT start cookie auth or storage event subscriptions
368
+ expect(checkForCookieAuth).not.toHaveBeenCalled()
369
+ })
370
+
290
371
  it('falls back to default auth (storage token) when studio mode is disabled', () => {
291
372
  const storageToken = 'regular-storage-token'
292
373
  vi.mocked(getTokenFromStorage).mockReturnValue(storageToken)
@@ -1,24 +1,21 @@
1
1
  import {type ClientConfig, createClient, type SanityClient} from '@sanity/client'
2
2
  import {type CurrentUser} from '@sanity/types'
3
- import {type Subscription} from 'rxjs'
4
3
 
5
4
  import {type AuthConfig, type AuthProvider} from '../config/authConfig'
6
5
  import {bindActionGlobally} from '../store/createActionBinder'
7
6
  import {createStateSourceAction} from '../store/createStateSourceAction'
8
7
  import {defineStore} from '../store/defineStore'
8
+ import {resolveAuthMode} from './authMode'
9
9
  import {AuthStateType} from './authStateType'
10
- import {refreshStampedToken} from './refreshStampedToken'
11
- import {checkForCookieAuth, getStudioTokenFromLocalStorage} from './studioModeAuth'
12
- import {subscribeToStateAndFetchCurrentUser} from './subscribeToStateAndFetchCurrentUser'
13
- import {subscribeToStorageEventsAndSetToken} from './subscribeToStorageEventsAndSetToken'
14
- import {
15
- getAuthCode,
16
- getCleanedUrl,
17
- getDefaultLocation,
18
- getDefaultStorage,
19
- getTokenFromLocation,
20
- getTokenFromStorage,
21
- } from './utils'
10
+ import {type AuthStrategyOptions} from './authStrategy'
11
+ import {getDashboardInitialState, initializeDashboardAuth} from './dashboardAuth'
12
+ import {getStandaloneInitialState, initializeStandaloneAuth} from './standaloneAuth'
13
+ import {getStudioInitialState, initializeStudioAuth} from './studioAuth'
14
+ import {getCleanedUrl, getDefaultLocation} from './utils'
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Public types
18
+ // ---------------------------------------------------------------------------
22
19
 
23
20
  /**
24
21
  * Represents the various states the authentication can be in.
@@ -96,8 +93,13 @@ export interface AuthStoreState {
96
93
  dashboardContext?: DashboardContext
97
94
  }
98
95
 
96
+ // ---------------------------------------------------------------------------
97
+ // Store definition — thin orchestrator
98
+ // ---------------------------------------------------------------------------
99
+
99
100
  export const authStore = defineStore<AuthStoreState>({
100
101
  name: 'Auth',
102
+
101
103
  getInitialState(instance) {
102
104
  const {
103
105
  apiHost,
@@ -107,12 +109,11 @@ export const authStore = defineStore<AuthStoreState>({
107
109
  clientFactory = createClient,
108
110
  initialLocationHref = getDefaultLocation(),
109
111
  } = instance.config.auth ?? {}
110
- let storageArea = instance.config.auth?.storageArea
111
112
 
112
- let storageKey = `__sanity_auth_token`
113
- const studioModeEnabled = instance.config.studioMode?.enabled
113
+ const authConfig = instance.config.auth ?? {}
114
114
 
115
- // This login URL will only be used for local development
115
+ // Build login URL (used by standalone mode, but always computed for the
116
+ // public `getLoginUrlState` accessor)
116
117
  let loginDomain = 'https://www.sanity.io'
117
118
  try {
118
119
  if (apiHost && new URL(apiHost).hostname.endsWith('.sanity.work')) {
@@ -126,80 +127,33 @@ export const authStore = defineStore<AuthStoreState>({
126
127
  loginUrl.searchParams.set('type', 'stampedToken') // Token must be stamped to have an sid passed back
127
128
  loginUrl.searchParams.set('withSid', 'true')
128
129
 
129
- // Check if running in dashboard context by parsing initialLocationHref
130
- let dashboardContext: DashboardContext = {}
131
- let isInDashboard = false
132
- try {
133
- const parsedUrl = new URL(initialLocationHref)
134
- const contextParam = parsedUrl.searchParams.get('_context')
135
- if (contextParam) {
136
- const parsedContext = JSON.parse(contextParam)
137
-
138
- // Consider it in dashboard if context is present and an object
139
- if (
140
- parsedContext &&
141
- typeof parsedContext === 'object' &&
142
- Object.keys(parsedContext).length > 0
143
- ) {
144
- // Explicitly remove the 'sid' property from the parsed object *before* assigning
145
- delete parsedContext.sid
146
-
147
- // Now assign the potentially modified object to dashboardContext
148
- dashboardContext = parsedContext
149
- isInDashboard = true
150
- }
151
- }
152
- } catch (err) {
153
- // eslint-disable-next-line no-console
154
- console.error('Failed to parse dashboard context from initial location:', err)
155
- }
156
-
157
- if (!isInDashboard || studioModeEnabled) {
158
- // If not in dashboard, use the storage area from the config
159
- // If studio mode is enabled, use the local storage area (default)
160
- storageArea = storageArea ?? getDefaultStorage()
161
- }
130
+ // Resolve auth mode and delegate to the appropriate strategy
131
+ const mode = resolveAuthMode(instance.config, initialLocationHref)
162
132
 
163
- let token: string | null
164
- let authMethod: AuthMethodOptions
165
- if (studioModeEnabled) {
166
- // In studio mode, always use the studio-specific storage key and subscribe to it
167
- const studioStorageKey = `__studio_auth_token_${instance.config.projectId ?? ''}`
168
- storageKey = studioStorageKey
169
- token = getStudioTokenFromLocalStorage(storageArea, studioStorageKey)
170
- if (token) {
171
- authMethod = 'localstorage'
172
- }
173
- } else {
174
- token = getTokenFromStorage(storageArea, storageKey)
175
- if (token) {
176
- authMethod = 'localstorage'
177
- }
133
+ const strategyOptions: AuthStrategyOptions = {
134
+ authConfig,
135
+ projectId: instance.config.projectId,
136
+ initialLocationHref,
137
+ clientFactory,
138
+ tokenSource: instance.config.studio?.auth?.token,
178
139
  }
179
140
 
180
- let authState: AuthState
181
- if (providedToken) {
182
- authState = {type: AuthStateType.LOGGED_IN, token: providedToken, currentUser: null}
183
- } else if (token && studioModeEnabled) {
184
- authState = {type: AuthStateType.LOGGED_IN, token: token ?? '', currentUser: null}
185
- } else if (
186
- getAuthCode(callbackUrl, initialLocationHref) ||
187
- getTokenFromLocation(initialLocationHref)
188
- ) {
189
- authState = {type: AuthStateType.LOGGING_IN, isExchangingToken: false}
190
- // Note: dashboardContext from the callback URL can be set later in handleAuthCallback too
191
- } else if (token && !isInDashboard && !studioModeEnabled) {
192
- // Only use token from storage if NOT running in dashboard and studio mode is not enabled
193
- authState = {type: AuthStateType.LOGGED_IN, token, currentUser: null}
194
- } else {
195
- // Default to logged out if no provided token, not handling callback,
196
- // or if token exists but we ARE in dashboard mode.
197
- authState = {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false}
141
+ let result
142
+ switch (mode) {
143
+ case 'studio':
144
+ result = getStudioInitialState(strategyOptions)
145
+ break
146
+ case 'dashboard':
147
+ result = getDashboardInitialState(strategyOptions)
148
+ break
149
+ case 'standalone':
150
+ result = getStandaloneInitialState(strategyOptions)
151
+ break
198
152
  }
199
153
 
200
154
  return {
201
- authState,
202
- dashboardContext,
155
+ authState: result.authState,
156
+ dashboardContext: result.dashboardContext,
203
157
  options: {
204
158
  apiHost,
205
159
  loginUrl: loginUrl.toString(),
@@ -208,59 +162,43 @@ export const authStore = defineStore<AuthStoreState>({
208
162
  providedToken,
209
163
  clientFactory,
210
164
  initialLocationHref,
211
- storageKey,
212
- storageArea,
213
- authMethod,
165
+ storageKey: result.storageKey,
166
+ storageArea: result.storageArea,
167
+ authMethod: result.authMethod,
214
168
  },
215
169
  }
216
170
  },
171
+
217
172
  initialize(context) {
218
- const subscriptions: Subscription[] = []
219
- subscriptions.push(subscribeToStateAndFetchCurrentUser(context))
220
- const storageArea = context.state.get().options?.storageArea
221
- if (storageArea) {
222
- subscriptions.push(subscribeToStorageEventsAndSetToken(context))
223
- }
173
+ const initialLocationHref =
174
+ context.state.get().options?.initialLocationHref ?? getDefaultLocation()
175
+ const mode = resolveAuthMode(context.instance.config, initialLocationHref)
224
176
 
225
- // If in Studio mode with no local token, resolve cookie auth asynchronously
226
- try {
227
- const {instance, state} = context
228
- const studioModeEnabled = !!instance.config.studioMode?.enabled
229
- const token: string | null =
230
- state.get().authState?.type === AuthStateType.LOGGED_IN
231
- ? (state.get().authState as LoggedInAuthState).token
232
- : null
233
- if (studioModeEnabled && !token) {
234
- const projectId = instance.config.projectId
235
- const clientFactory = state.get().options.clientFactory
236
- checkForCookieAuth(projectId, clientFactory).then((isCookieAuthEnabled) => {
237
- if (!isCookieAuthEnabled) return
238
- state.set('enableCookieAuth', (prev) => ({
239
- options: {...prev.options, authMethod: 'cookie'},
240
- authState:
241
- prev.authState.type === AuthStateType.LOGGED_IN
242
- ? prev.authState
243
- : {type: AuthStateType.LOGGED_IN, token: '', currentUser: null},
244
- }))
245
- })
246
- }
247
- } catch {
248
- // best-effort cookie detection
177
+ let initResult
178
+ switch (mode) {
179
+ case 'studio':
180
+ initResult = initializeStudioAuth(context, tokenRefresherRunning)
181
+ break
182
+ case 'dashboard':
183
+ initResult = initializeDashboardAuth(context, tokenRefresherRunning)
184
+ break
185
+ case 'standalone':
186
+ initResult = initializeStandaloneAuth(context, tokenRefresherRunning)
187
+ break
249
188
  }
250
189
 
251
- if (!tokenRefresherRunning) {
190
+ if (initResult.tokenRefresherStarted) {
252
191
  tokenRefresherRunning = true
253
- subscriptions.push(refreshStampedToken(context))
254
192
  }
255
193
 
256
- return () => {
257
- for (const subscription of subscriptions) {
258
- subscription.unsubscribe()
259
- }
260
- }
194
+ return initResult.dispose
261
195
  },
262
196
  })
263
197
 
198
+ // ---------------------------------------------------------------------------
199
+ // Public bound actions
200
+ // ---------------------------------------------------------------------------
201
+
264
202
  /**
265
203
  * @public
266
204
  */
@@ -0,0 +1,39 @@
1
+ import {type ClientConfig, type SanityClient} from '@sanity/client'
2
+
3
+ import {type AuthConfig} from '../config/authConfig'
4
+ import {type TokenSource} from '../config/sanityConfig'
5
+ import {type AuthMethodOptions, type AuthState, type DashboardContext} from './authStore'
6
+
7
+ /**
8
+ * The result returned by each auth strategy's `getInitialState` function.
9
+ * Provides the auth store with the initial auth state and the options needed
10
+ * to run the store.
11
+ *
12
+ * @internal
13
+ */
14
+ export interface AuthStrategyResult {
15
+ authState: AuthState
16
+ storageKey: string
17
+ storageArea: Storage | undefined
18
+ authMethod: AuthMethodOptions
19
+ dashboardContext: DashboardContext
20
+ }
21
+
22
+ /**
23
+ * Shared options that every auth strategy receives.
24
+ * Extracted from the instance config once and passed through.
25
+ *
26
+ * @internal
27
+ */
28
+ export interface AuthStrategyOptions {
29
+ authConfig: AuthConfig
30
+ projectId: string | undefined
31
+ initialLocationHref: string
32
+ clientFactory: (config: ClientConfig) => SanityClient
33
+ /**
34
+ * Reactive token source from a Studio workspace. When provided, the studio
35
+ * auth strategy subscribes to it for ongoing token sync instead of
36
+ * discovering tokens from localStorage or cookies.
37
+ */
38
+ tokenSource?: TokenSource
39
+ }