@sanity/sdk 2.3.0 → 2.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "private": false,
5
5
  "description": "Sanity SDK",
6
6
  "keywords": [
@@ -61,18 +61,18 @@
61
61
  "devDependencies": {
62
62
  "@sanity/browserslist-config": "^1.0.5",
63
63
  "@sanity/pkg-utils": "^7.9.6",
64
- "@sanity/prettier-config": "^1.0.3",
64
+ "@sanity/prettier-config": "^1.0.6",
65
65
  "@types/lodash-es": "^4.17.12",
66
- "@vitest/coverage-v8": "3.1.2",
66
+ "@vitest/coverage-v8": "3.2.4",
67
67
  "eslint": "^9.22.0",
68
- "prettier": "^3.5.3",
68
+ "prettier": "^3.7.3",
69
69
  "rollup-plugin-visualizer": "^5.14.0",
70
70
  "typescript": "^5.8.3",
71
71
  "vite": "^6.3.4",
72
- "vitest": "^3.1.2",
73
- "@repo/config-eslint": "0.0.0",
72
+ "vitest": "^3.2.4",
74
73
  "@repo/config-test": "0.0.1",
75
74
  "@repo/package.bundle": "3.82.0",
75
+ "@repo/config-eslint": "0.0.0",
76
76
  "@repo/tsconfig": "0.0.1",
77
77
  "@repo/package.config": "0.0.1"
78
78
  },
@@ -5,6 +5,25 @@ import {type SanityProject as _SanityProject} from '@sanity/client'
5
5
  */
6
6
  export type SanityProject = _SanityProject
7
7
 
8
+ export type {
9
+ AgentGenerateOptions,
10
+ AgentGenerateResult,
11
+ AgentPatchOptions,
12
+ AgentPatchResult,
13
+ AgentPromptOptions,
14
+ AgentPromptResult,
15
+ AgentTransformOptions,
16
+ AgentTransformResult,
17
+ AgentTranslateOptions,
18
+ AgentTranslateResult,
19
+ } from '../agent/agentActions'
20
+ export {
21
+ agentGenerate,
22
+ agentPatch,
23
+ agentPrompt,
24
+ agentTransform,
25
+ agentTranslate,
26
+ } from '../agent/agentActions'
8
27
  export {AuthStateType} from '../auth/authStateType'
9
28
  export {
10
29
  type AuthState,
@@ -24,6 +43,13 @@ export {
24
43
  export {observeOrganizationVerificationState} from '../auth/getOrganizationVerificationState'
25
44
  export {handleAuthCallback} from '../auth/handleAuthCallback'
26
45
  export {logout} from '../auth/logout'
46
+ export {
47
+ type ApiErrorBody,
48
+ getClientErrorApiBody,
49
+ getClientErrorApiDescription,
50
+ getClientErrorApiType,
51
+ isProjectUserNotFoundClientError,
52
+ } from '../auth/utils'
27
53
  export type {ClientStoreState as ClientState} from '../client/clientStore'
28
54
  export {type ClientOptions, getClient, getClientState} from '../client/clientStore'
29
55
  export {
@@ -50,9 +76,13 @@ export {
50
76
  createProjectHandle,
51
77
  } from '../config/handles'
52
78
  export {
79
+ canvasSource,
53
80
  type DatasetHandle,
81
+ datasetSource,
54
82
  type DocumentHandle,
83
+ type DocumentSource,
55
84
  type DocumentTypeHandle,
85
+ mediaLibrarySource,
56
86
  type PerspectiveHandle,
57
87
  type ProjectHandle,
58
88
  type ReleasePerspective,
@@ -0,0 +1,81 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import {firstValueFrom, of} from 'rxjs'
3
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
4
+
5
+ import {
6
+ agentGenerate,
7
+ agentPatch,
8
+ agentPrompt,
9
+ agentTransform,
10
+ agentTranslate,
11
+ } from './agentActions'
12
+
13
+ let mockClient: any
14
+
15
+ vi.mock('../client/clientStore', () => {
16
+ return {
17
+ getClientState: () => ({observable: of(mockClient)}),
18
+ }
19
+ })
20
+
21
+ describe('agent actions', () => {
22
+ beforeEach(() => {
23
+ mockClient = {
24
+ observable: {
25
+ agent: {
26
+ action: {
27
+ generate: vi.fn(),
28
+ transform: vi.fn(),
29
+ translate: vi.fn(),
30
+ },
31
+ },
32
+ },
33
+ agent: {
34
+ action: {
35
+ prompt: vi.fn(),
36
+ patch: vi.fn(),
37
+ },
38
+ },
39
+ }
40
+ })
41
+
42
+ it('agentGenerate returns observable from client', async () => {
43
+ mockClient.observable.agent.action.generate.mockReturnValue(of('gen'))
44
+ const instance = {config: {projectId: 'p', dataset: 'd'}} as any
45
+ const value = await firstValueFrom(agentGenerate(instance, {foo: 'bar'} as any))
46
+ expect(value).toBe('gen')
47
+ expect(mockClient.observable.agent.action.generate).toHaveBeenCalledWith({foo: 'bar'})
48
+ })
49
+
50
+ it('agentTransform returns observable from client', async () => {
51
+ mockClient.observable.agent.action.transform.mockReturnValue(of('xform'))
52
+ const instance = {config: {projectId: 'p', dataset: 'd'}} as any
53
+ const value = await firstValueFrom(agentTransform(instance, {a: 1} as any))
54
+ expect(value).toBe('xform')
55
+ expect(mockClient.observable.agent.action.transform).toHaveBeenCalledWith({a: 1})
56
+ })
57
+
58
+ it('agentTranslate returns observable from client', async () => {
59
+ mockClient.observable.agent.action.translate.mockReturnValue(of('xlate'))
60
+ const instance = {config: {projectId: 'p', dataset: 'd'}} as any
61
+ const value = await firstValueFrom(agentTranslate(instance, {b: 2} as any))
62
+ expect(value).toBe('xlate')
63
+ expect(mockClient.observable.agent.action.translate).toHaveBeenCalledWith({b: 2})
64
+ })
65
+
66
+ it('agentPrompt wraps promise into observable', async () => {
67
+ mockClient.agent.action.prompt.mockResolvedValue('prompted')
68
+ const instance = {config: {projectId: 'p', dataset: 'd'}} as any
69
+ const value = await firstValueFrom(agentPrompt(instance, {p: true} as any))
70
+ expect(value).toBe('prompted')
71
+ expect(mockClient.agent.action.prompt).toHaveBeenCalledWith({p: true})
72
+ })
73
+
74
+ it('agentPatch wraps promise into observable', async () => {
75
+ mockClient.agent.action.patch.mockResolvedValue('patched')
76
+ const instance = {config: {projectId: 'p', dataset: 'd'}} as any
77
+ const value = await firstValueFrom(agentPatch(instance, {q: false} as any))
78
+ expect(value).toBe('patched')
79
+ expect(mockClient.agent.action.patch).toHaveBeenCalledWith({q: false})
80
+ })
81
+ })
@@ -0,0 +1,139 @@
1
+ import {type SanityClient} from '@sanity/client'
2
+ import {from, Observable, switchMap} from 'rxjs'
3
+
4
+ import {getClientState} from '../client/clientStore'
5
+ import {type SanityInstance} from '../store/createSanityInstance'
6
+
7
+ const API_VERSION = 'vX'
8
+
9
+ /** @alpha */
10
+ export type AgentGenerateOptions = Parameters<
11
+ SanityClient['observable']['agent']['action']['generate']
12
+ >[0]
13
+
14
+ /** @alpha */
15
+ export type AgentTransformOptions = Parameters<
16
+ SanityClient['observable']['agent']['action']['transform']
17
+ >[0]
18
+
19
+ /** @alpha */
20
+ export type AgentTranslateOptions = Parameters<
21
+ SanityClient['observable']['agent']['action']['translate']
22
+ >[0]
23
+
24
+ /** @alpha */
25
+ export type AgentPromptOptions = Parameters<SanityClient['agent']['action']['prompt']>[0]
26
+
27
+ /** @alpha */
28
+ export type AgentPatchOptions = Parameters<SanityClient['agent']['action']['patch']>[0]
29
+
30
+ /** @alpha */
31
+ export type AgentGenerateResult = Awaited<
32
+ ReturnType<SanityClient['observable']['agent']['action']['generate']>
33
+ >
34
+
35
+ /** @alpha */
36
+ export type AgentTransformResult = Awaited<
37
+ ReturnType<SanityClient['observable']['agent']['action']['transform']>
38
+ >
39
+
40
+ /** @alpha */
41
+ export type AgentTranslateResult = Awaited<
42
+ ReturnType<SanityClient['observable']['agent']['action']['translate']>
43
+ >
44
+
45
+ /** @alpha */
46
+ export type AgentPromptResult = Awaited<ReturnType<SanityClient['agent']['action']['prompt']>>
47
+
48
+ /** @alpha */
49
+ export type AgentPatchResult = Awaited<ReturnType<SanityClient['agent']['action']['patch']>>
50
+
51
+ /**
52
+ * Generates a new document using the agent.
53
+ * @param instance - The Sanity instance.
54
+ * @param options - The options for the agent generate action. See the [Agent Actions API](https://www.sanity.io/docs/agent-actions/introduction) for more details.
55
+ * @returns An Observable emitting the result of the agent generate action.
56
+ * @alpha
57
+ */
58
+ export function agentGenerate(
59
+ instance: SanityInstance,
60
+ options: AgentGenerateOptions,
61
+ ): AgentGenerateResult {
62
+ return getClientState(instance, {
63
+ apiVersion: API_VERSION,
64
+ projectId: instance.config.projectId,
65
+ dataset: instance.config.dataset,
66
+ }).observable.pipe(switchMap((client) => client.observable.agent.action.generate(options)))
67
+ }
68
+
69
+ /**
70
+ * Transforms a document using the agent.
71
+ * @param instance - The Sanity instance.
72
+ * @param options - The options for the agent transform action. See the [Agent Actions API](https://www.sanity.io/docs/agent-actions/introduction) for more details.
73
+ * @returns An Observable emitting the result of the agent transform action.
74
+ * @alpha
75
+ */
76
+ export function agentTransform(
77
+ instance: SanityInstance,
78
+ options: AgentTransformOptions,
79
+ ): AgentTransformResult {
80
+ return getClientState(instance, {
81
+ apiVersion: API_VERSION,
82
+ projectId: instance.config.projectId,
83
+ dataset: instance.config.dataset,
84
+ }).observable.pipe(switchMap((client) => client.observable.agent.action.transform(options)))
85
+ }
86
+
87
+ /**
88
+ * Translates a document using the agent.
89
+ * @param instance - The Sanity instance.
90
+ * @param options - The options for the agent translate action. See the [Agent Actions API](https://www.sanity.io/docs/agent-actions/introduction) for more details.
91
+ * @returns An Observable emitting the result of the agent translate action.
92
+ * @alpha
93
+ */
94
+ export function agentTranslate(
95
+ instance: SanityInstance,
96
+ options: AgentTranslateOptions,
97
+ ): AgentTranslateResult {
98
+ return getClientState(instance, {
99
+ apiVersion: API_VERSION,
100
+ projectId: instance.config.projectId,
101
+ dataset: instance.config.dataset,
102
+ }).observable.pipe(switchMap((client) => client.observable.agent.action.translate(options)))
103
+ }
104
+
105
+ /**
106
+ * Prompts the agent using the same instruction template format as the other actions, but returns text or json instead of acting on a document.
107
+ * @param instance - The Sanity instance.
108
+ * @param options - The options for the agent prompt action. See the [Agent Actions API](https://www.sanity.io/docs/agent-actions/introduction) for more details.
109
+ * @returns An Observable emitting the result of the agent prompt action.
110
+ * @alpha
111
+ */
112
+ export function agentPrompt(
113
+ instance: SanityInstance,
114
+ options: AgentPromptOptions,
115
+ ): Observable<AgentPromptResult> {
116
+ return getClientState(instance, {
117
+ apiVersion: API_VERSION,
118
+ projectId: instance.config.projectId,
119
+ dataset: instance.config.dataset,
120
+ }).observable.pipe(switchMap((client) => from(client.agent.action.prompt(options))))
121
+ }
122
+
123
+ /**
124
+ * Patches a document using the agent.
125
+ * @param instance - The Sanity instance.
126
+ * @param options - The options for the agent patch action. See the [Agent Actions API](https://www.sanity.io/docs/agent-actions/introduction) for more details.
127
+ * @returns An Observable emitting the result of the agent patch action.
128
+ * @alpha
129
+ */
130
+ export function agentPatch(
131
+ instance: SanityInstance,
132
+ options: AgentPatchOptions,
133
+ ): Observable<AgentPatchResult> {
134
+ return getClientState(instance, {
135
+ apiVersion: API_VERSION,
136
+ projectId: instance.config.projectId,
137
+ dataset: instance.config.dataset,
138
+ }).observable.pipe(switchMap((client) => from(client.agent.action.patch(options))))
139
+ }
@@ -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
  ),
package/src/auth/utils.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import {type ClientError} from '@sanity/client'
1
2
  import {EMPTY, fromEvent, Observable} from 'rxjs'
2
3
 
3
4
  import {AUTH_CODE_PARAM, DEFAULT_BASE} from './authConstants'
@@ -134,3 +135,38 @@ export function getCleanedUrl(locationUrl: string): string {
134
135
  loc.searchParams.delete('url')
135
136
  return loc.toString()
136
137
  }
138
+
139
+ // -----------------------------------------------------------------------------
140
+ // ClientError helpers (shared)
141
+ // -----------------------------------------------------------------------------
142
+
143
+ /** @internal */
144
+ export type ApiErrorBody = {
145
+ error?: {type?: string; description?: string}
146
+ type?: string
147
+ description?: string
148
+ message?: string
149
+ }
150
+
151
+ /** @internal Extracts the structured API error body from a ClientError, if present. */
152
+ export function getClientErrorApiBody(error: ClientError): ApiErrorBody | undefined {
153
+ const body: unknown = (error as ClientError).response?.body
154
+ return body && typeof body === 'object' ? (body as ApiErrorBody) : undefined
155
+ }
156
+
157
+ /** @internal Returns the error type string from an API error body, if available. */
158
+ export function getClientErrorApiType(error: ClientError): string | undefined {
159
+ const body = getClientErrorApiBody(error)
160
+ return body?.error?.type ?? body?.type
161
+ }
162
+
163
+ /** @internal Returns the error description string from an API error body, if available. */
164
+ export function getClientErrorApiDescription(error: ClientError): string | undefined {
165
+ const body = getClientErrorApiBody(error)
166
+ return body?.error?.description ?? body?.description
167
+ }
168
+
169
+ /** @internal True if the error represents a projectUserNotFoundError. */
170
+ export function isProjectUserNotFoundClientError(error: ClientError): boolean {
171
+ return getClientErrorApiType(error) === 'projectUserNotFoundError'
172
+ }