@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/dist/index.d.ts +144 -17
- package/dist/index.js +171 -46
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/_exports/index.ts +30 -0
- package/src/agent/agentActions.test.ts +81 -0
- package/src/agent/agentActions.ts +139 -0
- package/src/auth/authStore.test.ts +12 -20
- package/src/auth/authStore.ts +46 -15
- package/src/auth/studioModeAuth.test.ts +4 -4
- package/src/auth/studioModeAuth.ts +4 -5
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +20 -9
- package/src/auth/utils.ts +36 -0
- package/src/client/clientStore.test.ts +151 -0
- package/src/client/clientStore.ts +39 -1
- package/src/config/sanityConfig.ts +41 -0
- package/src/document/actions.test.ts +34 -0
- package/src/document/actions.ts +20 -0
- package/src/document/documentStore.test.ts +28 -0
- package/src/document/processActions.test.ts +97 -0
- package/src/document/processActions.ts +12 -2
- package/src/query/queryStore.ts +7 -4
- package/src/store/createActionBinder.ts +27 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk",
|
|
3
|
-
"version": "2.
|
|
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.
|
|
64
|
+
"@sanity/prettier-config": "^1.0.6",
|
|
65
65
|
"@types/lodash-es": "^4.17.12",
|
|
66
|
-
"@vitest/coverage-v8": "3.
|
|
66
|
+
"@vitest/coverage-v8": "3.2.4",
|
|
67
67
|
"eslint": "^9.22.0",
|
|
68
|
-
"prettier": "^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.
|
|
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
|
},
|
package/src/_exports/index.ts
CHANGED
|
@@ -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,
|
|
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',
|
|
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},
|
|
276
|
+
auth: {storageArea: mockStorage},
|
|
277
277
|
})
|
|
278
278
|
|
|
279
|
-
//
|
|
279
|
+
// Verify initial state without async cookie probe
|
|
280
280
|
const {authState: initialAuthState} = authStore.getInitialState(instance)
|
|
281
|
-
expect(initialAuthState.type).toBe(AuthStateType.LOGGED_OUT)
|
|
282
|
-
expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage,
|
|
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
|
-
//
|
|
286
|
-
|
|
284
|
+
// Trigger store creation + initialize
|
|
285
|
+
getAuthState(instance)
|
|
287
286
|
|
|
288
|
-
|
|
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)
|
|
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)
|
package/src/auth/authStore.ts
CHANGED
|
@@ -67,7 +67,11 @@ export interface DashboardContext {
|
|
|
67
67
|
orgId?: string
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
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
|
-
|
|
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 (
|
|
160
|
-
|
|
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 (
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
42
|
+
storageKey: string | undefined,
|
|
43
43
|
): string | null {
|
|
44
|
-
if (!storageArea || !
|
|
45
|
-
const
|
|
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
|
-
(
|
|
19
|
-
|
|
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
|
|
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
|
+
}
|