@sanity/sdk 2.3.0 → 2.3.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "private": false,
5
5
  "description": "Sanity SDK",
6
6
  "keywords": [
@@ -71,10 +71,10 @@
71
71
  "vite": "^6.3.4",
72
72
  "vitest": "^3.1.2",
73
73
  "@repo/config-eslint": "0.0.0",
74
- "@repo/config-test": "0.0.1",
75
74
  "@repo/package.bundle": "3.82.0",
75
+ "@repo/package.config": "0.0.1",
76
76
  "@repo/tsconfig": "0.0.1",
77
- "@repo/package.config": "0.0.1"
77
+ "@repo/config-test": "0.0.1"
78
78
  },
79
79
  "engines": {
80
80
  "node": ">=20.0.0"
@@ -237,6 +237,7 @@ describe('authStore', () => {
237
237
  it('sets to logged in using studio token when studio mode is enabled and token exists', () => {
238
238
  const studioToken = 'studio-token'
239
239
  const projectId = 'studio-project'
240
+ const studioStorageKey = `__studio_auth_token_${projectId}`
240
241
  const mockStorage = {
241
242
  getItem: vi.fn(),
242
243
  setItem: vi.fn(),
@@ -252,40 +253,38 @@ describe('authStore', () => {
252
253
  })
253
254
 
254
255
  const {authState, options} = authStore.getInitialState(instance)
255
- expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage, projectId)
256
+ expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage, studioStorageKey)
256
257
  expect(authState).toMatchObject({type: AuthStateType.LOGGED_IN, token: studioToken})
257
258
  expect(options.authMethod).toBe('localstorage')
258
259
  })
259
260
 
260
- it('checks for cookie auth when studio mode is enabled and no studio token exists', async () => {
261
- vi.useFakeTimers()
261
+ it('checks for cookie auth during initialize when studio mode is enabled and no studio token exists', () => {
262
262
  const projectId = 'studio-project'
263
+ const studioStorageKey = `__studio_auth_token_${projectId}`
263
264
  const mockStorage = {
264
265
  getItem: vi.fn(),
265
266
  setItem: vi.fn(),
266
267
  removeItem: vi.fn(),
267
268
  } as unknown as Storage // Mock storage
268
269
  vi.mocked(getStudioTokenFromLocalStorage).mockReturnValue(null)
269
- // Mock cookie check to return true asynchronously
270
270
  vi.mocked(checkForCookieAuth).mockResolvedValue(true)
271
271
 
272
272
  instance = createSanityInstance({
273
273
  projectId,
274
274
  dataset: 'd',
275
275
  studioMode: {enabled: true},
276
- auth: {storageArea: mockStorage}, // Provide mock storage
276
+ auth: {storageArea: mockStorage},
277
277
  })
278
278
 
279
- // Initial state might be logged out before the async check completes
279
+ // Verify initial state without async cookie probe
280
280
  const {authState: initialAuthState} = authStore.getInitialState(instance)
281
- expect(initialAuthState.type).toBe(AuthStateType.LOGGED_OUT) // Or potentially logging in depending on other factors
282
- expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage, projectId)
283
- expect(checkForCookieAuth).toHaveBeenCalledWith(projectId, expect.any(Function))
281
+ expect(initialAuthState.type).toBe(AuthStateType.LOGGED_OUT)
282
+ expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage, studioStorageKey)
284
283
 
285
- // Wait for the promise in getInitialState to resolve
286
- await vi.runAllTimersAsync()
284
+ // Trigger store creation + initialize
285
+ getAuthState(instance)
287
286
 
288
- vi.useRealTimers()
287
+ expect(checkForCookieAuth).toHaveBeenCalledWith(projectId, expect.any(Function))
289
288
  })
290
289
 
291
290
  it('falls back to default auth (storage token) when studio mode is disabled', () => {
@@ -573,14 +572,7 @@ describe('authStore', () => {
573
572
  expect(initialOrgId.getCurrent()).toBe('initial-org-id')
574
573
 
575
574
  // Call handleCallback with the callback URL
576
- await handleAuthCallback(instance, callbackUrl) // Use await as handleAuthCallback is async
577
-
578
- // Wait for the state update to be reflected in the selector
579
- await vi.waitUntil(
580
- () => getDashboardOrganizationId(instance).getCurrent() === 'callback-org-id',
581
- )
582
- // Add a microtask yield just in case
583
- await new Promise((resolve) => setTimeout(resolve, 0))
575
+ await handleAuthCallback(instance, callbackUrl)
584
576
 
585
577
  // Check that the orgId from the callback context is now set
586
578
  const finalOrgId = getDashboardOrganizationId(instance)
@@ -67,7 +67,11 @@ export interface DashboardContext {
67
67
  orgId?: string
68
68
  }
69
69
 
70
- type AuthMethodOptions = 'localstorage' | 'cookie' | undefined
70
+ /**
71
+ * The method of authentication used.
72
+ * @internal
73
+ */
74
+ export type AuthMethodOptions = 'localstorage' | 'cookie' | undefined
71
75
 
72
76
  let tokenRefresherRunning = false
73
77
 
@@ -105,7 +109,8 @@ export const authStore = defineStore<AuthStoreState>({
105
109
  } = instance.config.auth ?? {}
106
110
  let storageArea = instance.config.auth?.storageArea
107
111
 
108
- const storageKey = `__sanity_auth_token`
112
+ let storageKey = `__sanity_auth_token`
113
+ const studioModeEnabled = instance.config.studioMode?.enabled
109
114
 
110
115
  // This login URL will only be used for local development
111
116
  let loginDomain = 'https://www.sanity.io'
@@ -149,23 +154,21 @@ export const authStore = defineStore<AuthStoreState>({
149
154
  console.error('Failed to parse dashboard context from initial location:', err)
150
155
  }
151
156
 
152
- if (!isInDashboard) {
157
+ if (!isInDashboard || studioModeEnabled) {
153
158
  // If not in dashboard, use the storage area from the config
159
+ // If studio mode is enabled, use the local storage area (default)
154
160
  storageArea = storageArea ?? getDefaultStorage()
155
161
  }
156
162
 
157
163
  let token: string | null
158
164
  let authMethod: AuthMethodOptions
159
- if (instance.config.studioMode?.enabled) {
160
- token = getStudioTokenFromLocalStorage(storageArea, instance.config.projectId)
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)
161
170
  if (token) {
162
171
  authMethod = 'localstorage'
163
- } else {
164
- checkForCookieAuth(instance.config.projectId, clientFactory).then((isCookieAuthEnabled) => {
165
- if (isCookieAuthEnabled) {
166
- authMethod = 'cookie'
167
- }
168
- })
169
172
  }
170
173
  } else {
171
174
  token = getTokenFromStorage(storageArea, storageKey)
@@ -177,14 +180,16 @@ export const authStore = defineStore<AuthStoreState>({
177
180
  let authState: AuthState
178
181
  if (providedToken) {
179
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}
180
185
  } else if (
181
186
  getAuthCode(callbackUrl, initialLocationHref) ||
182
187
  getTokenFromLocation(initialLocationHref)
183
188
  ) {
184
189
  authState = {type: AuthStateType.LOGGING_IN, isExchangingToken: false}
185
190
  // Note: dashboardContext from the callback URL can be set later in handleAuthCallback too
186
- } else if (token && !isInDashboard) {
187
- // Only use token from storage if NOT running in dashboard
191
+ } else if (token && !isInDashboard && !studioModeEnabled) {
192
+ // Only use token from storage if NOT running in dashboard and studio mode is not enabled
188
193
  authState = {type: AuthStateType.LOGGED_IN, token, currentUser: null}
189
194
  } else {
190
195
  // Default to logged out if no provided token, not handling callback,
@@ -212,11 +217,37 @@ export const authStore = defineStore<AuthStoreState>({
212
217
  initialize(context) {
213
218
  const subscriptions: Subscription[] = []
214
219
  subscriptions.push(subscribeToStateAndFetchCurrentUser(context))
215
-
216
- if (context.state.get().options?.storageArea) {
220
+ const storageArea = context.state.get().options?.storageArea
221
+ if (storageArea) {
217
222
  subscriptions.push(subscribeToStorageEventsAndSetToken(context))
218
223
  }
219
224
 
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
249
+ }
250
+
220
251
  if (!tokenRefresherRunning) {
221
252
  tokenRefresherRunning = true
222
253
  subscriptions.push(refreshStampedToken(context))
@@ -96,7 +96,7 @@ describe('getStudioTokenFromLocalStorage', () => {
96
96
  expect(getTokenFromStorageSpy).not.toHaveBeenCalled()
97
97
  })
98
98
 
99
- it('should return null if projectId is undefined', () => {
99
+ it('should return null if storageKey is undefined', () => {
100
100
  const result = getStudioTokenFromLocalStorage(storageArea, undefined)
101
101
  expect(result).toBeNull()
102
102
  expect(getTokenFromStorageSpy).not.toHaveBeenCalled()
@@ -104,19 +104,19 @@ describe('getStudioTokenFromLocalStorage', () => {
104
104
 
105
105
  it('should call getTokenFromStorage with correct key', () => {
106
106
  getTokenFromStorageSpy.mockReturnValue(null) // Assume token not found for this test
107
- getStudioTokenFromLocalStorage(storageArea, projectId)
107
+ getStudioTokenFromLocalStorage(storageArea, studioStorageKey)
108
108
  expect(getTokenFromStorageSpy).toHaveBeenCalledWith(storageArea, studioStorageKey)
109
109
  })
110
110
 
111
111
  it('should return the token if found in storage', () => {
112
112
  getTokenFromStorageSpy.mockReturnValue(mockToken)
113
- const result = getStudioTokenFromLocalStorage(storageArea, projectId)
113
+ const result = getStudioTokenFromLocalStorage(storageArea, studioStorageKey)
114
114
  expect(result).toBe(mockToken)
115
115
  })
116
116
 
117
117
  it('should return null if token is not found in storage', () => {
118
118
  getTokenFromStorageSpy.mockReturnValue(null)
119
- const result = getStudioTokenFromLocalStorage(storageArea, projectId)
119
+ const result = getStudioTokenFromLocalStorage(storageArea, studioStorageKey)
120
120
  expect(result).toBeNull()
121
121
  })
122
122
  })
@@ -33,17 +33,16 @@ export async function checkForCookieAuth(
33
33
  /**
34
34
  * Attempts to retrieve a studio token from local storage.
35
35
  * @param storageArea - The storage area to retrieve the token from.
36
- * @param projectId - The project ID to retrieve the token for.
36
+ * @param storageKey - The storage key to retrieve the token from.
37
37
  * @returns The studio token or null if it does not exist.
38
38
  * @internal
39
39
  */
40
40
  export function getStudioTokenFromLocalStorage(
41
41
  storageArea: Storage | undefined,
42
- projectId: string | undefined,
42
+ storageKey: string | undefined,
43
43
  ): string | null {
44
- if (!storageArea || !projectId) return null
45
- const studioStorageKey = `__studio_auth_token_${projectId}`
46
- const token = getTokenFromStorage(storageArea, studioStorageKey)
44
+ if (!storageArea || !storageKey) return null
45
+ const token = getTokenFromStorage(storageArea, storageKey)
47
46
  if (token) {
48
47
  return token
49
48
  }
@@ -4,32 +4,43 @@ import {distinctUntilChanged, filter, map, type Subscription, switchMap} from 'r
4
4
  import {type StoreContext} from '../store/defineStore'
5
5
  import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
6
6
  import {AuthStateType} from './authStateType'
7
- import {type AuthState, type AuthStoreState} from './authStore'
7
+ import {type AuthMethodOptions, type AuthState, type AuthStoreState} from './authStore'
8
8
 
9
9
  export const subscribeToStateAndFetchCurrentUser = ({
10
10
  state,
11
+ instance,
11
12
  }: StoreContext<AuthStoreState>): Subscription => {
12
13
  const {clientFactory, apiHost} = state.get().options
14
+ const useProjectHostname = !!instance.config.studioMode?.enabled
15
+ const projectId = instance.config.projectId
13
16
 
14
17
  const currentUser$ = state.observable
15
18
  .pipe(
16
- map(({authState}) => authState),
19
+ map(({authState, options}) => ({authState, authMethod: options.authMethod})),
17
20
  filter(
18
- (authState): authState is Extract<AuthState, {type: AuthStateType.LOGGED_IN}> =>
19
- authState.type === AuthStateType.LOGGED_IN && !authState.currentUser,
21
+ (
22
+ value,
23
+ ): value is {
24
+ authState: Extract<AuthState, {type: AuthStateType.LOGGED_IN}>
25
+ authMethod: AuthMethodOptions
26
+ } => value.authState.type === AuthStateType.LOGGED_IN && !value.authState.currentUser,
27
+ ),
28
+ map((value) => ({token: value.authState.token, authMethod: value.authMethod})),
29
+ distinctUntilChanged(
30
+ (prev, curr) => prev.token === curr.token && prev.authMethod === curr.authMethod,
20
31
  ),
21
- map((authState) => authState.token),
22
- distinctUntilChanged(),
23
32
  )
24
33
  .pipe(
25
- map((token) =>
34
+ map(({token, authMethod}) =>
26
35
  clientFactory({
27
36
  apiVersion: DEFAULT_API_VERSION,
28
37
  requestTagPrefix: REQUEST_TAG_PREFIX,
29
- token,
38
+ token: authMethod === 'cookie' ? undefined : token,
30
39
  ignoreBrowserTokenWarning: true,
31
- useProjectHostname: false,
40
+ useProjectHostname,
32
41
  useCdn: false,
42
+ ...(authMethod === 'cookie' ? {withCredentials: true} : {}),
43
+ ...(useProjectHostname && projectId ? {projectId} : {}),
33
44
  ...(apiHost && {apiHost}),
34
45
  }),
35
46
  ),