@sanity/sdk 2.7.0 → 3.0.0-rc.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 +228 -239
- package/dist/index.js +287 -454
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/_exports/index.ts +16 -17
- package/src/agent/agentActions.test.ts +60 -16
- package/src/agent/agentActions.ts +29 -20
- package/src/auth/authMode.test.ts +0 -25
- package/src/auth/authMode.ts +3 -6
- package/src/auth/authStore.test.ts +129 -66
- package/src/auth/authStore.ts +9 -11
- package/src/auth/dashboardAuth.ts +2 -2
- package/src/auth/getOrganizationVerificationState.test.ts +10 -11
- package/src/auth/handleAuthCallback.test.ts +0 -12
- package/src/auth/handleAuthCallback.ts +9 -3
- package/src/auth/logout.test.ts +0 -6
- package/src/auth/refreshStampedToken.test.ts +121 -17
- package/src/auth/standaloneAuth.ts +9 -3
- package/src/auth/studioAuth.ts +35 -8
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +9 -3
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +1 -1
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +0 -2
- package/src/auth/subscribeToStorageEventsAndSetToken.ts +2 -2
- package/src/auth/utils.ts +33 -0
- package/src/client/clientStore.test.ts +14 -61
- package/src/client/clientStore.ts +52 -28
- package/src/comlink/controller/actions/destroyController.test.ts +1 -4
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +1 -4
- package/src/comlink/controller/actions/getOrCreateController.test.ts +1 -4
- package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
- package/src/comlink/controller/comlinkControllerStore.test.ts +1 -4
- package/src/comlink/node/actions/getOrCreateNode.test.ts +1 -4
- package/src/comlink/node/actions/releaseNode.test.ts +1 -4
- package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
- package/src/comlink/node/getNodeState.test.ts +1 -1
- package/src/config/__tests__/handles.test.ts +12 -18
- package/src/config/handles.ts +7 -25
- package/src/config/sanityConfig.ts +99 -52
- package/src/datasets/datasets.test.ts +2 -2
- package/src/datasets/datasets.ts +4 -10
- package/src/document/actions.test.ts +33 -4
- package/src/document/actions.ts +3 -10
- package/src/document/applyDocumentActions.test.ts +17 -18
- package/src/document/applyDocumentActions.ts +9 -12
- package/src/document/documentStore.test.ts +303 -133
- package/src/document/documentStore.ts +70 -61
- package/src/document/permissions.test.ts +44 -8
- package/src/document/processActions.test.ts +77 -7
- package/src/document/reducers.test.ts +35 -3
- package/src/document/sharedListener.test.ts +13 -13
- package/src/document/sharedListener.ts +8 -3
- package/src/favorites/favorites.test.ts +10 -2
- package/src/presence/presenceStore.test.ts +34 -9
- package/src/presence/presenceStore.ts +29 -13
- package/src/preview/previewProjectionUtils.test.ts +192 -0
- package/src/preview/previewProjectionUtils.ts +88 -0
- package/src/preview/{previewStore.ts → types.ts} +6 -25
- package/src/project/project.test.ts +1 -1
- package/src/project/project.ts +14 -20
- package/src/projection/getProjectionState.test.ts +4 -2
- package/src/projection/getProjectionState.ts +2 -21
- package/src/projection/projectionQuery.ts +2 -3
- package/src/projection/projectionStore.test.ts +3 -3
- package/src/projection/resolveProjection.test.ts +2 -1
- package/src/projection/resolveProjection.ts +2 -18
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +2 -2
- package/src/projection/subscribeToStateAndFetchBatches.ts +23 -36
- package/src/projection/types.ts +1 -9
- package/src/projects/projects.test.ts +1 -1
- package/src/query/queryStore.test.ts +86 -28
- package/src/query/queryStore.ts +23 -38
- package/src/releases/getPerspectiveState.test.ts +14 -13
- package/src/releases/getPerspectiveState.ts +6 -6
- package/src/releases/releasesStore.test.ts +21 -6
- package/src/releases/releasesStore.ts +18 -8
- package/src/store/createActionBinder.test.ts +114 -111
- package/src/store/createActionBinder.ts +52 -101
- package/src/store/createSanityInstance.test.ts +13 -83
- package/src/store/createSanityInstance.ts +2 -78
- package/src/store/createStateSourceAction.test.ts +2 -2
- package/src/store/createStateSourceAction.ts +5 -5
- package/src/store/createStoreInstance.test.ts +2 -4
- package/src/users/reducers.test.ts +1 -6
- package/src/users/reducers.ts +2 -2
- package/src/users/types.ts +4 -4
- package/src/users/usersStore.test.ts +12 -15
- package/src/utils/createFetcherStore.test.ts +1 -1
- package/src/utils/logger.test.ts +0 -12
- package/src/utils/logger.ts +3 -8
- package/src/preview/getPreviewState.test.ts +0 -120
- package/src/preview/getPreviewState.ts +0 -91
- package/src/preview/previewQuery.test.ts +0 -236
- package/src/preview/previewQuery.ts +0 -153
- package/src/preview/previewStore.test.ts +0 -36
- package/src/preview/resolvePreview.test.ts +0 -47
- package/src/preview/resolvePreview.ts +0 -20
- package/src/preview/subscribeToStateAndFetchBatches.test.ts +0 -221
- package/src/preview/subscribeToStateAndFetchBatches.ts +0 -112
- package/src/preview/util.ts +0 -13
|
@@ -7,13 +7,13 @@ import {createSanityInstance} from '../store/createSanityInstance'
|
|
|
7
7
|
import {createStoreState} from '../store/createStoreState'
|
|
8
8
|
import {AuthStateType} from './authStateType'
|
|
9
9
|
import {type AuthState, authStore} from './authStore'
|
|
10
|
-
// Import only the public function
|
|
11
10
|
import {
|
|
12
11
|
getLastRefreshTime,
|
|
13
12
|
getNextRefreshDelay,
|
|
14
13
|
refreshStampedToken,
|
|
15
14
|
setLastRefreshTime,
|
|
16
15
|
} from './refreshStampedToken'
|
|
16
|
+
import {createLoggedInAuthState} from './utils'
|
|
17
17
|
|
|
18
18
|
// Type definitions for Web Locks (can be kept if needed for context)
|
|
19
19
|
// ... (Lock, LockOptions, LockGrantedCallback types)
|
|
@@ -118,8 +118,6 @@ describe('refreshStampedToken', () => {
|
|
|
118
118
|
}
|
|
119
119
|
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
120
120
|
const instance = createSanityInstance({
|
|
121
|
-
projectId: 'p',
|
|
122
|
-
dataset: 'd',
|
|
123
121
|
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
124
122
|
})
|
|
125
123
|
const initialState = authStore.getInitialState(instance, null)
|
|
@@ -147,6 +145,126 @@ describe('refreshStampedToken', () => {
|
|
|
147
145
|
expect(locksRequest).not.toHaveBeenCalled()
|
|
148
146
|
})
|
|
149
147
|
|
|
148
|
+
it('does not refresh on visibility change when lastTokenRefresh is recent', async () => {
|
|
149
|
+
const mockClient = {
|
|
150
|
+
observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
|
|
151
|
+
}
|
|
152
|
+
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
153
|
+
const instance = createSanityInstance({
|
|
154
|
+
defaultResource: {projectId: 'p', dataset: 'd'},
|
|
155
|
+
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
156
|
+
})
|
|
157
|
+
const initialState = authStore.getInitialState(instance, null)
|
|
158
|
+
initialState.authState = createLoggedInAuthState('sk-initial-token-st123', null)
|
|
159
|
+
initialState.dashboardContext = {mode: 'test'}
|
|
160
|
+
const state = createStoreState(initialState)
|
|
161
|
+
|
|
162
|
+
const subscription = refreshStampedToken({state, instance, key: null})
|
|
163
|
+
subscriptions.push(subscription)
|
|
164
|
+
|
|
165
|
+
const addEventListenerMock = global.document.addEventListener as ReturnType<typeof vi.fn>
|
|
166
|
+
expect(addEventListenerMock).toHaveBeenCalledWith('visibilitychange', expect.any(Function))
|
|
167
|
+
const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
|
|
168
|
+
|
|
169
|
+
Object.defineProperty(global.document, 'visibilityState', {
|
|
170
|
+
value: 'visible',
|
|
171
|
+
writable: true,
|
|
172
|
+
configurable: true,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
visibilityHandler()
|
|
176
|
+
await vi.advanceTimersByTimeAsync(100)
|
|
177
|
+
|
|
178
|
+
expect(mockClient.observable.request).not.toHaveBeenCalled()
|
|
179
|
+
const finalAuthState = state.get().authState
|
|
180
|
+
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
181
|
+
expect(finalAuthState.token).toBe('sk-initial-token-st123')
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('refreshes on visibility change when lastTokenRefresh is stale', async () => {
|
|
186
|
+
const REFRESH_INTERVAL = 12 * 60 * 60 * 1000
|
|
187
|
+
const mockClient = {
|
|
188
|
+
observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
|
|
189
|
+
}
|
|
190
|
+
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
191
|
+
const instance = createSanityInstance({
|
|
192
|
+
defaultResource: {projectId: 'p', dataset: 'd'},
|
|
193
|
+
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
194
|
+
})
|
|
195
|
+
const initialState = authStore.getInitialState(instance, null)
|
|
196
|
+
const staleTimestamp = Date.now() - REFRESH_INTERVAL - 1000
|
|
197
|
+
initialState.authState = {
|
|
198
|
+
type: AuthStateType.LOGGED_IN,
|
|
199
|
+
token: 'sk-initial-token-st123',
|
|
200
|
+
currentUser: null,
|
|
201
|
+
lastTokenRefresh: staleTimestamp,
|
|
202
|
+
}
|
|
203
|
+
initialState.dashboardContext = {mode: 'test'}
|
|
204
|
+
const state = createStoreState(initialState)
|
|
205
|
+
|
|
206
|
+
const subscription = refreshStampedToken({state, instance, key: null})
|
|
207
|
+
subscriptions.push(subscription)
|
|
208
|
+
|
|
209
|
+
const addEventListenerMock = global.document.addEventListener as ReturnType<typeof vi.fn>
|
|
210
|
+
expect(addEventListenerMock).toHaveBeenCalledWith('visibilitychange', expect.any(Function))
|
|
211
|
+
const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
|
|
212
|
+
|
|
213
|
+
Object.defineProperty(global.document, 'visibilityState', {
|
|
214
|
+
value: 'visible',
|
|
215
|
+
writable: true,
|
|
216
|
+
configurable: true,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
visibilityHandler()
|
|
220
|
+
await vi.advanceTimersToNextTimerAsync()
|
|
221
|
+
|
|
222
|
+
expect(mockClient.observable.request).toHaveBeenCalled()
|
|
223
|
+
const finalAuthState = state.get().authState
|
|
224
|
+
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
225
|
+
expect(finalAuthState.token).toBe('sk-refreshed-token-st123')
|
|
226
|
+
expect(finalAuthState.lastTokenRefresh).toBeGreaterThan(staleTimestamp)
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('refreshes on visibility change when lastTokenRefresh is undefined (pre-fix behavior)', async () => {
|
|
231
|
+
const mockClient = {
|
|
232
|
+
observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
|
|
233
|
+
}
|
|
234
|
+
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
235
|
+
const instance = createSanityInstance({
|
|
236
|
+
defaultResource: {projectId: 'p', dataset: 'd'},
|
|
237
|
+
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
238
|
+
})
|
|
239
|
+
const initialState = authStore.getInitialState(instance, null)
|
|
240
|
+
initialState.authState = {
|
|
241
|
+
type: AuthStateType.LOGGED_IN,
|
|
242
|
+
token: 'sk-initial-token-st123',
|
|
243
|
+
currentUser: null,
|
|
244
|
+
// lastTokenRefresh intentionally omitted to demonstrate the old bug
|
|
245
|
+
}
|
|
246
|
+
initialState.dashboardContext = {mode: 'test'}
|
|
247
|
+
const state = createStoreState(initialState)
|
|
248
|
+
|
|
249
|
+
const subscription = refreshStampedToken({state, instance, key: null})
|
|
250
|
+
subscriptions.push(subscription)
|
|
251
|
+
|
|
252
|
+
const addEventListenerMock = global.document.addEventListener as ReturnType<typeof vi.fn>
|
|
253
|
+
const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
|
|
254
|
+
|
|
255
|
+
Object.defineProperty(global.document, 'visibilityState', {
|
|
256
|
+
value: 'visible',
|
|
257
|
+
writable: true,
|
|
258
|
+
configurable: true,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
visibilityHandler()
|
|
262
|
+
await vi.advanceTimersToNextTimerAsync()
|
|
263
|
+
|
|
264
|
+
// Without lastTokenRefresh, shouldRefreshToken returns true — this is the bug
|
|
265
|
+
expect(mockClient.observable.request).toHaveBeenCalled()
|
|
266
|
+
})
|
|
267
|
+
|
|
150
268
|
it('does not refresh when tab is not visible', async () => {
|
|
151
269
|
// Set visibility to hidden
|
|
152
270
|
Object.defineProperty(global, 'document', {
|
|
@@ -164,8 +282,6 @@ describe('refreshStampedToken', () => {
|
|
|
164
282
|
}
|
|
165
283
|
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
166
284
|
const instance = createSanityInstance({
|
|
167
|
-
projectId: 'p',
|
|
168
|
-
dataset: 'd',
|
|
169
285
|
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
170
286
|
})
|
|
171
287
|
const initialState = authStore.getInitialState(instance, null)
|
|
@@ -198,8 +314,6 @@ describe('refreshStampedToken', () => {
|
|
|
198
314
|
const mockClient = {observable: {request: vi.fn()}}
|
|
199
315
|
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
200
316
|
const instance = createSanityInstance({
|
|
201
|
-
projectId: 'p',
|
|
202
|
-
dataset: 'd',
|
|
203
317
|
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
204
318
|
})
|
|
205
319
|
const initialState = authStore.getInitialState(instance, null)
|
|
@@ -249,8 +363,6 @@ describe('refreshStampedToken', () => {
|
|
|
249
363
|
}
|
|
250
364
|
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
251
365
|
const instance = createSanityInstance({
|
|
252
|
-
projectId: 'p',
|
|
253
|
-
dataset: 'd',
|
|
254
366
|
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
255
367
|
})
|
|
256
368
|
const initialState = authStore.getInitialState(instance, null)
|
|
@@ -299,8 +411,6 @@ describe('refreshStampedToken', () => {
|
|
|
299
411
|
}
|
|
300
412
|
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
301
413
|
const instance = createSanityInstance({
|
|
302
|
-
projectId: 'p',
|
|
303
|
-
dataset: 'd',
|
|
304
414
|
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
305
415
|
})
|
|
306
416
|
const initialState = authStore.getInitialState(instance, null)
|
|
@@ -345,8 +455,6 @@ describe('refreshStampedToken', () => {
|
|
|
345
455
|
const mockClient = {observable: {request: vi.fn(() => throwError(() => error))}}
|
|
346
456
|
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
347
457
|
const instance = createSanityInstance({
|
|
348
|
-
projectId: 'p',
|
|
349
|
-
dataset: 'd',
|
|
350
458
|
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
351
459
|
})
|
|
352
460
|
const initialState = authStore.getInitialState(instance, null)
|
|
@@ -374,8 +482,6 @@ describe('refreshStampedToken', () => {
|
|
|
374
482
|
it('does nothing if user is not logged in', async () => {
|
|
375
483
|
const mockClientFactory = vi.fn()
|
|
376
484
|
const instance = createSanityInstance({
|
|
377
|
-
projectId: 'p',
|
|
378
|
-
dataset: 'd',
|
|
379
485
|
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
380
486
|
})
|
|
381
487
|
const initialState = authStore.getInitialState(instance, null)
|
|
@@ -400,8 +506,6 @@ describe('refreshStampedToken', () => {
|
|
|
400
506
|
const mockClient = {observable: {request: vi.fn()}}
|
|
401
507
|
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
402
508
|
const instance = createSanityInstance({
|
|
403
|
-
projectId: 'p',
|
|
404
|
-
dataset: 'd',
|
|
405
509
|
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
406
510
|
})
|
|
407
511
|
const initialState = authStore.getInitialState(instance, null)
|
|
@@ -7,7 +7,13 @@ import {type AuthStrategyOptions, type AuthStrategyResult} from './authStrategy'
|
|
|
7
7
|
import {refreshStampedToken} from './refreshStampedToken'
|
|
8
8
|
import {subscribeToStateAndFetchCurrentUser} from './subscribeToStateAndFetchCurrentUser'
|
|
9
9
|
import {subscribeToStorageEventsAndSetToken} from './subscribeToStorageEventsAndSetToken'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
createLoggedInAuthState,
|
|
12
|
+
getAuthCode,
|
|
13
|
+
getDefaultStorage,
|
|
14
|
+
getTokenFromLocation,
|
|
15
|
+
getTokenFromStorage,
|
|
16
|
+
} from './utils'
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Resolves the initial auth state for Standalone mode.
|
|
@@ -30,7 +36,7 @@ export function getStandaloneInitialState(options: AuthStrategyOptions): AuthStr
|
|
|
30
36
|
// Provided token always wins
|
|
31
37
|
if (providedToken) {
|
|
32
38
|
return {
|
|
33
|
-
authState:
|
|
39
|
+
authState: createLoggedInAuthState(providedToken, null),
|
|
34
40
|
storageKey,
|
|
35
41
|
storageArea,
|
|
36
42
|
authMethod: undefined,
|
|
@@ -53,7 +59,7 @@ export function getStandaloneInitialState(options: AuthStrategyOptions): AuthStr
|
|
|
53
59
|
const token = getTokenFromStorage(storageArea, storageKey)
|
|
54
60
|
if (token) {
|
|
55
61
|
return {
|
|
56
|
-
authState:
|
|
62
|
+
authState: createLoggedInAuthState(token, null),
|
|
57
63
|
storageKey,
|
|
58
64
|
storageArea,
|
|
59
65
|
authMethod: 'localstorage',
|
package/src/auth/studioAuth.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {refreshStampedToken} from './refreshStampedToken'
|
|
|
9
9
|
import {checkForCookieAuth, getStudioTokenFromLocalStorage} from './studioModeAuth'
|
|
10
10
|
import {subscribeToStateAndFetchCurrentUser} from './subscribeToStateAndFetchCurrentUser'
|
|
11
11
|
import {subscribeToStorageEventsAndSetToken} from './subscribeToStorageEventsAndSetToken'
|
|
12
|
-
import {getDefaultStorage} from './utils'
|
|
12
|
+
import {createLoggedInAuthState, getDefaultStorage} from './utils'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Resolves the initial auth state for Studio mode.
|
|
@@ -55,7 +55,7 @@ export function getStudioInitialState(options: AuthStrategyOptions): AuthStrateg
|
|
|
55
55
|
|
|
56
56
|
if (providedToken) {
|
|
57
57
|
return {
|
|
58
|
-
authState:
|
|
58
|
+
authState: createLoggedInAuthState(providedToken, null),
|
|
59
59
|
storageKey: studioStorageKey,
|
|
60
60
|
storageArea,
|
|
61
61
|
authMethod,
|
|
@@ -65,7 +65,7 @@ export function getStudioInitialState(options: AuthStrategyOptions): AuthStrateg
|
|
|
65
65
|
|
|
66
66
|
if (token) {
|
|
67
67
|
return {
|
|
68
|
-
authState:
|
|
68
|
+
authState: createLoggedInAuthState(token, null),
|
|
69
69
|
storageKey: studioStorageKey,
|
|
70
70
|
storageArea,
|
|
71
71
|
authMethod: 'localstorage',
|
|
@@ -115,14 +115,29 @@ export function initializeStudioAuth(
|
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
117
|
* Subscribe to a reactive token source from the Studio workspace.
|
|
118
|
-
*
|
|
119
|
-
*
|
|
118
|
+
*
|
|
119
|
+
* When the token source emits a non-null token, the SDK uses it directly.
|
|
120
|
+
* When it emits `null`, the behavior depends on the `authenticated` flag
|
|
121
|
+
* from the Studio's workspace config:
|
|
122
|
+
*
|
|
123
|
+
* - `authenticated: true` — the Studio has already verified the user is
|
|
124
|
+
* logged in (e.g. via cookie auth). The SDK treats the null token as
|
|
125
|
+
* cookie-based auth and stays in the LOGGED_IN state.
|
|
126
|
+
*
|
|
127
|
+
* - `authenticated` absent/false — the user is genuinely not authenticated;
|
|
128
|
+
* transition to LOGGED_OUT.
|
|
129
|
+
*
|
|
130
|
+
* No async cookie probing is needed here because this code path only runs
|
|
131
|
+
* when a Studio provides SDKStudioContext, and the Studio's Workspace type
|
|
132
|
+
* always includes `authenticated`. The async `checkForCookieAuth` fallback
|
|
133
|
+
* remains in `initializeWithFallback` for the non-Studio path.
|
|
120
134
|
*/
|
|
121
135
|
function initializeWithTokenSource(
|
|
122
136
|
context: StoreContext<AuthStoreState>,
|
|
123
137
|
tokenSource: TokenSource,
|
|
124
138
|
): {dispose: () => void; tokenRefresherStarted: boolean} {
|
|
125
139
|
const subscriptions: Subscription[] = []
|
|
140
|
+
const studioAuthenticated = context.instance.config.studio?.authenticated === true
|
|
126
141
|
|
|
127
142
|
// Subscribe to the current user fetcher — runs whenever auth state changes
|
|
128
143
|
subscriptions.push(subscribeToStateAndFetchCurrentUser(context, {useProjectHostname: true}))
|
|
@@ -132,11 +147,23 @@ function initializeWithTokenSource(
|
|
|
132
147
|
next: (token) => {
|
|
133
148
|
const {state} = context
|
|
134
149
|
if (token) {
|
|
150
|
+
// Studio provided a real token — use it directly
|
|
135
151
|
state.set('studioTokenSource', (prev) => ({
|
|
136
152
|
options: {...prev.options, authMethod: undefined},
|
|
137
|
-
authState:
|
|
153
|
+
authState: createLoggedInAuthState(token, null),
|
|
154
|
+
}))
|
|
155
|
+
} else if (studioAuthenticated) {
|
|
156
|
+
// The Studio says the user is authenticated — null token means
|
|
157
|
+
// cookie-based auth is in use. Stay logged in with cookie method.
|
|
158
|
+
state.set('studioTokenSourceCookieAuth', (prev) => ({
|
|
159
|
+
options: {...prev.options, authMethod: 'cookie'},
|
|
160
|
+
authState:
|
|
161
|
+
prev.authState.type === AuthStateType.LOGGED_IN
|
|
162
|
+
? prev.authState
|
|
163
|
+
: createLoggedInAuthState('', null),
|
|
138
164
|
}))
|
|
139
165
|
} else {
|
|
166
|
+
// No token and Studio doesn't confirm authentication — logged out
|
|
140
167
|
state.set('studioTokenSourceLoggedOut', (prev) => ({
|
|
141
168
|
options: {...prev.options, authMethod: undefined},
|
|
142
169
|
authState: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
|
|
@@ -184,7 +211,7 @@ function initializeWithFallback(
|
|
|
184
211
|
: null
|
|
185
212
|
|
|
186
213
|
if (!token) {
|
|
187
|
-
const projectIdValue = instance.config.projectId
|
|
214
|
+
const projectIdValue = instance.config.studio?.projectId
|
|
188
215
|
const clientFactory = state.get().options.clientFactory
|
|
189
216
|
checkForCookieAuth(projectIdValue, clientFactory).then((isCookieAuthEnabled) => {
|
|
190
217
|
if (!isCookieAuthEnabled) return
|
|
@@ -193,7 +220,7 @@ function initializeWithFallback(
|
|
|
193
220
|
authState:
|
|
194
221
|
prev.authState.type === AuthStateType.LOGGED_IN
|
|
195
222
|
? prev.authState
|
|
196
|
-
:
|
|
223
|
+
: createLoggedInAuthState('', null),
|
|
197
224
|
}))
|
|
198
225
|
})
|
|
199
226
|
}
|
|
@@ -18,7 +18,9 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
|
|
|
18
18
|
const mockRequest = vi.fn().mockReturnValue(of(mockUser))
|
|
19
19
|
const mockClient = {observable: {request: mockRequest}}
|
|
20
20
|
const clientFactory = vi.fn().mockReturnValue(mockClient)
|
|
21
|
-
const instance = createSanityInstance({
|
|
21
|
+
const instance = createSanityInstance({
|
|
22
|
+
auth: {clientFactory},
|
|
23
|
+
})
|
|
22
24
|
|
|
23
25
|
const state = createStoreState(authStore.getInitialState(instance, null))
|
|
24
26
|
const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
|
|
@@ -50,7 +52,9 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
|
|
|
50
52
|
const mockRequest = vi.fn().mockReturnValue(of(mockUser).pipe(delay(0)))
|
|
51
53
|
const mockClient = {observable: {request: mockRequest}}
|
|
52
54
|
const clientFactory = vi.fn().mockReturnValue(mockClient)
|
|
53
|
-
const instance = createSanityInstance({
|
|
55
|
+
const instance = createSanityInstance({
|
|
56
|
+
auth: {clientFactory},
|
|
57
|
+
})
|
|
54
58
|
|
|
55
59
|
const state = createStoreState(authStore.getInitialState(instance, null))
|
|
56
60
|
const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
|
|
@@ -86,7 +90,9 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
|
|
|
86
90
|
const mockRequest = vi.fn().mockReturnValue(throwError(() => error))
|
|
87
91
|
const mockClient = {observable: {request: mockRequest}}
|
|
88
92
|
const clientFactory = vi.fn().mockReturnValue(mockClient)
|
|
89
|
-
const instance = createSanityInstance({
|
|
93
|
+
const instance = createSanityInstance({
|
|
94
|
+
auth: {clientFactory},
|
|
95
|
+
})
|
|
90
96
|
|
|
91
97
|
const state = createStoreState(authStore.getInitialState(instance, null))
|
|
92
98
|
const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
|
|
@@ -24,7 +24,7 @@ export const subscribeToStateAndFetchCurrentUser = (
|
|
|
24
24
|
): Subscription => {
|
|
25
25
|
const {clientFactory, apiHost} = state.get().options
|
|
26
26
|
const useProjectHostname = fetchOptions?.useProjectHostname ?? isStudioConfig(instance.config)
|
|
27
|
-
const projectId = instance.config.projectId
|
|
27
|
+
const projectId = instance.config.studio?.projectId
|
|
28
28
|
|
|
29
29
|
const currentUser$ = state.observable
|
|
30
30
|
.pipe(
|
|
@@ -3,7 +3,7 @@ import {defer, distinctUntilChanged, filter, map, type Subscription} from 'rxjs'
|
|
|
3
3
|
import {type StoreContext} from '../store/defineStore'
|
|
4
4
|
import {AuthStateType} from './authStateType'
|
|
5
5
|
import {type AuthStoreState} from './authStore'
|
|
6
|
-
import {getStorageEvents, getTokenFromStorage} from './utils'
|
|
6
|
+
import {createLoggedInAuthState, getStorageEvents, getTokenFromStorage} from './utils'
|
|
7
7
|
|
|
8
8
|
export const subscribeToStorageEventsAndSetToken = ({
|
|
9
9
|
state,
|
|
@@ -22,7 +22,7 @@ export const subscribeToStorageEventsAndSetToken = ({
|
|
|
22
22
|
return tokenFromStorage$.subscribe((token) => {
|
|
23
23
|
state.set('updateTokenFromStorageEvent', {
|
|
24
24
|
authState: token
|
|
25
|
-
?
|
|
25
|
+
? createLoggedInAuthState(token, null)
|
|
26
26
|
: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
|
|
27
27
|
})
|
|
28
28
|
})
|
package/src/auth/utils.ts
CHANGED
|
@@ -1,7 +1,40 @@
|
|
|
1
1
|
import {type ClientError} from '@sanity/client'
|
|
2
|
+
import {type CurrentUser} from '@sanity/types'
|
|
2
3
|
import {EMPTY, fromEvent, Observable} from 'rxjs'
|
|
3
4
|
|
|
4
5
|
import {AUTH_CODE_PARAM, DEFAULT_BASE} from './authConstants'
|
|
6
|
+
import {AuthStateType} from './authStateType'
|
|
7
|
+
import {type LoggedInAuthState} from './authStore'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates a properly initialized {@link LoggedInAuthState}.
|
|
11
|
+
*
|
|
12
|
+
* For stamped tokens (containing `"-st"`), `lastTokenRefresh` is set to
|
|
13
|
+
* `Date.now()` so that the visibility-change handler in
|
|
14
|
+
* {@link refreshStampedToken} does not trigger an unnecessary refresh the
|
|
15
|
+
* first time the tab becomes visible.
|
|
16
|
+
*
|
|
17
|
+
* @param token - The auth token.
|
|
18
|
+
* @param currentUser - The current user, or `null` if not yet fetched.
|
|
19
|
+
* @param existingLastTokenRefresh - An existing timestamp to preserve
|
|
20
|
+
* (e.g. when updating a token while keeping the previous refresh time).
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export function createLoggedInAuthState(
|
|
24
|
+
token: string,
|
|
25
|
+
currentUser: CurrentUser | null,
|
|
26
|
+
existingLastTokenRefresh?: number,
|
|
27
|
+
): LoggedInAuthState {
|
|
28
|
+
const isStampedToken = token.includes('-st')
|
|
29
|
+
const lastTokenRefresh = existingLastTokenRefresh ?? (isStampedToken ? Date.now() : undefined)
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
type: AuthStateType.LOGGED_IN,
|
|
33
|
+
token,
|
|
34
|
+
currentUser,
|
|
35
|
+
...(lastTokenRefresh !== undefined && {lastTokenRefresh}),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
5
38
|
|
|
6
39
|
export function getAuthCode(callbackUrl: string | undefined, locationHref: string): string | null {
|
|
7
40
|
const loc = new URL(locationHref, DEFAULT_BASE)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {createClient, type SanityClient} from '@sanity/client'
|
|
2
2
|
import {Subject} from 'rxjs'
|
|
3
|
-
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
4
|
|
|
5
5
|
import {getAuthMethodState, getTokenState} from '../auth/authStore'
|
|
6
6
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
@@ -33,10 +33,7 @@ beforeEach(() => {
|
|
|
33
33
|
vi.mocked(createClient).mockImplementation(
|
|
34
34
|
(clientConfig) => ({config: () => clientConfig}) as SanityClient,
|
|
35
35
|
)
|
|
36
|
-
instance = createSanityInstance(
|
|
37
|
-
projectId: 'test-project',
|
|
38
|
-
dataset: 'test-dataset',
|
|
39
|
-
})
|
|
36
|
+
instance = createSanityInstance()
|
|
40
37
|
})
|
|
41
38
|
|
|
42
39
|
afterEach(() => {
|
|
@@ -45,29 +42,6 @@ afterEach(() => {
|
|
|
45
42
|
|
|
46
43
|
describe('clientStore', () => {
|
|
47
44
|
describe('getClient', () => {
|
|
48
|
-
it('should create a client with default configuration', () => {
|
|
49
|
-
const client = getClient(instance, {apiVersion: '2024-11-12'})
|
|
50
|
-
|
|
51
|
-
const defaultConfiguration = {
|
|
52
|
-
useCdn: false,
|
|
53
|
-
ignoreBrowserTokenWarning: true,
|
|
54
|
-
allowReconfigure: false,
|
|
55
|
-
requestTagPrefix: 'sanity.sdk',
|
|
56
|
-
projectId: 'test-project',
|
|
57
|
-
dataset: 'test-dataset',
|
|
58
|
-
token: 'initial-token',
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
expect(vi.mocked(createClient)).toHaveBeenCalledWith({
|
|
62
|
-
...defaultConfiguration,
|
|
63
|
-
apiVersion: '2024-11-12',
|
|
64
|
-
})
|
|
65
|
-
expect(client.config()).toEqual({
|
|
66
|
-
...defaultConfiguration,
|
|
67
|
-
apiVersion: '2024-11-12',
|
|
68
|
-
})
|
|
69
|
-
})
|
|
70
|
-
|
|
71
45
|
it('should throw when using disallowed configuration keys', () => {
|
|
72
46
|
expect(() =>
|
|
73
47
|
getClient(instance, {
|
|
@@ -180,35 +154,13 @@ describe('clientStore', () => {
|
|
|
180
154
|
it('should create client when source is provided', () => {
|
|
181
155
|
const client = getClient(instance, {
|
|
182
156
|
apiVersion: '2024-11-12',
|
|
183
|
-
|
|
157
|
+
resource: {mediaLibraryId: 'media-lib-123'},
|
|
184
158
|
})
|
|
185
159
|
|
|
186
160
|
expect(vi.mocked(createClient)).toHaveBeenCalledWith(
|
|
187
161
|
expect.objectContaining({
|
|
188
162
|
'apiVersion': '2024-11-12',
|
|
189
|
-
'~experimental_resource': {type: 'dataset', id: 'source-project.source-dataset'},
|
|
190
|
-
}),
|
|
191
|
-
)
|
|
192
|
-
// Client should be projectless - no projectId/dataset in config
|
|
193
|
-
expect(client.config()).not.toHaveProperty('projectId')
|
|
194
|
-
expect(client.config()).not.toHaveProperty('dataset')
|
|
195
|
-
expect(client.config()).toEqual(
|
|
196
|
-
expect.objectContaining({
|
|
197
|
-
'~experimental_resource': {type: 'dataset', id: 'source-project.source-dataset'},
|
|
198
|
-
}),
|
|
199
|
-
)
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it('should create resource when source has array sourceId and be projectless', () => {
|
|
203
|
-
const client = getClient(instance, {
|
|
204
|
-
apiVersion: '2024-11-12',
|
|
205
|
-
source: {mediaLibraryId: 'media-lib-123'},
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
expect(vi.mocked(createClient)).toHaveBeenCalledWith(
|
|
209
|
-
expect.objectContaining({
|
|
210
163
|
'~experimental_resource': {type: 'media-library', id: 'media-lib-123'},
|
|
211
|
-
'apiVersion': '2024-11-12',
|
|
212
164
|
}),
|
|
213
165
|
)
|
|
214
166
|
// Client should be projectless - no projectId/dataset in config
|
|
@@ -224,7 +176,7 @@ describe('clientStore', () => {
|
|
|
224
176
|
it('should create resource when canvas source is provided and be projectless', () => {
|
|
225
177
|
const client = getClient(instance, {
|
|
226
178
|
apiVersion: '2024-11-12',
|
|
227
|
-
|
|
179
|
+
resource: {canvasId: 'canvas-123'},
|
|
228
180
|
})
|
|
229
181
|
|
|
230
182
|
expect(vi.mocked(createClient)).toHaveBeenCalledWith(
|
|
@@ -243,10 +195,11 @@ describe('clientStore', () => {
|
|
|
243
195
|
)
|
|
244
196
|
})
|
|
245
197
|
|
|
246
|
-
|
|
198
|
+
// skipped until we migrate to using source for project and dataset
|
|
199
|
+
it.skip('should create projectless client when source is provided, ignoring instance config', () => {
|
|
247
200
|
const client = getClient(instance, {
|
|
248
201
|
apiVersion: '2024-11-12',
|
|
249
|
-
|
|
202
|
+
resource: {projectId: 'source-project', dataset: 'source-dataset'},
|
|
250
203
|
})
|
|
251
204
|
|
|
252
205
|
// Client should be projectless - source takes precedence, instance config is ignored
|
|
@@ -263,13 +216,13 @@ describe('clientStore', () => {
|
|
|
263
216
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
264
217
|
const client = getClient(instance, {
|
|
265
218
|
apiVersion: '2024-11-12',
|
|
266
|
-
|
|
219
|
+
resource: {mediaLibraryId: 'media-lib-123'},
|
|
267
220
|
projectId: 'explicit-project',
|
|
268
221
|
dataset: 'explicit-dataset',
|
|
269
222
|
})
|
|
270
223
|
|
|
271
224
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
272
|
-
'Both
|
|
225
|
+
'Both resource and explicit projectId/dataset are provided. The resource will be used and projectId/dataset will be ignored.',
|
|
273
226
|
)
|
|
274
227
|
// Client should still be projectless despite explicit projectId/dataset
|
|
275
228
|
expect(client.config()).not.toHaveProperty('projectId')
|
|
@@ -280,15 +233,15 @@ describe('clientStore', () => {
|
|
|
280
233
|
it('should create different clients for different sources', () => {
|
|
281
234
|
const client1 = getClient(instance, {
|
|
282
235
|
apiVersion: '2024-11-12',
|
|
283
|
-
|
|
236
|
+
resource: {projectId: 'source-project', dataset: 'source-dataset'},
|
|
284
237
|
})
|
|
285
238
|
const client2 = getClient(instance, {
|
|
286
239
|
apiVersion: '2024-11-12',
|
|
287
|
-
|
|
240
|
+
resource: {mediaLibraryId: 'media-lib-123'},
|
|
288
241
|
})
|
|
289
242
|
const client3 = getClient(instance, {
|
|
290
243
|
apiVersion: '2024-11-12',
|
|
291
|
-
|
|
244
|
+
resource: {canvasId: 'canvas-123'},
|
|
292
245
|
})
|
|
293
246
|
|
|
294
247
|
expect(client1).not.toBe(client2)
|
|
@@ -300,11 +253,11 @@ describe('clientStore', () => {
|
|
|
300
253
|
it('should reuse clients with identical source configurations', () => {
|
|
301
254
|
const client1 = getClient(instance, {
|
|
302
255
|
apiVersion: '2024-11-12',
|
|
303
|
-
|
|
256
|
+
resource: {projectId: 'source-project', dataset: 'source-dataset'},
|
|
304
257
|
})
|
|
305
258
|
const client2 = getClient(instance, {
|
|
306
259
|
apiVersion: '2024-11-12',
|
|
307
|
-
|
|
260
|
+
resource: {projectId: 'source-project', dataset: 'source-dataset'},
|
|
308
261
|
})
|
|
309
262
|
|
|
310
263
|
expect(client1).toBe(client2)
|