@sanity/sdk 2.8.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.
Files changed (99) hide show
  1. package/dist/index.d.ts +228 -239
  2. package/dist/index.js +287 -454
  3. package/dist/index.js.map +1 -1
  4. package/package.json +4 -4
  5. package/src/_exports/index.ts +16 -17
  6. package/src/agent/agentActions.test.ts +60 -16
  7. package/src/agent/agentActions.ts +29 -20
  8. package/src/auth/authMode.test.ts +0 -25
  9. package/src/auth/authMode.ts +3 -6
  10. package/src/auth/authStore.test.ts +129 -66
  11. package/src/auth/authStore.ts +9 -11
  12. package/src/auth/dashboardAuth.ts +2 -2
  13. package/src/auth/getOrganizationVerificationState.test.ts +10 -11
  14. package/src/auth/handleAuthCallback.test.ts +0 -12
  15. package/src/auth/handleAuthCallback.ts +9 -3
  16. package/src/auth/logout.test.ts +0 -6
  17. package/src/auth/refreshStampedToken.test.ts +121 -17
  18. package/src/auth/standaloneAuth.ts +9 -3
  19. package/src/auth/studioAuth.ts +35 -8
  20. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +9 -3
  21. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +1 -1
  22. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +0 -2
  23. package/src/auth/subscribeToStorageEventsAndSetToken.ts +2 -2
  24. package/src/auth/utils.ts +33 -0
  25. package/src/client/clientStore.test.ts +14 -61
  26. package/src/client/clientStore.ts +52 -28
  27. package/src/comlink/controller/actions/destroyController.test.ts +1 -4
  28. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +1 -4
  29. package/src/comlink/controller/actions/getOrCreateController.test.ts +1 -4
  30. package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
  31. package/src/comlink/controller/comlinkControllerStore.test.ts +1 -4
  32. package/src/comlink/node/actions/getOrCreateNode.test.ts +1 -4
  33. package/src/comlink/node/actions/releaseNode.test.ts +1 -4
  34. package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
  35. package/src/comlink/node/getNodeState.test.ts +1 -1
  36. package/src/config/__tests__/handles.test.ts +12 -18
  37. package/src/config/handles.ts +7 -25
  38. package/src/config/sanityConfig.ts +99 -52
  39. package/src/datasets/datasets.test.ts +2 -2
  40. package/src/datasets/datasets.ts +4 -10
  41. package/src/document/actions.test.ts +33 -4
  42. package/src/document/actions.ts +3 -10
  43. package/src/document/applyDocumentActions.test.ts +17 -18
  44. package/src/document/applyDocumentActions.ts +9 -12
  45. package/src/document/documentStore.test.ts +303 -133
  46. package/src/document/documentStore.ts +70 -61
  47. package/src/document/permissions.test.ts +44 -8
  48. package/src/document/processActions.test.ts +77 -7
  49. package/src/document/reducers.test.ts +35 -3
  50. package/src/document/sharedListener.test.ts +13 -13
  51. package/src/document/sharedListener.ts +8 -3
  52. package/src/favorites/favorites.test.ts +10 -2
  53. package/src/presence/presenceStore.test.ts +34 -9
  54. package/src/presence/presenceStore.ts +29 -13
  55. package/src/preview/previewProjectionUtils.test.ts +192 -0
  56. package/src/preview/previewProjectionUtils.ts +88 -0
  57. package/src/preview/{previewStore.ts → types.ts} +6 -25
  58. package/src/project/project.test.ts +1 -1
  59. package/src/project/project.ts +14 -20
  60. package/src/projection/getProjectionState.test.ts +4 -2
  61. package/src/projection/getProjectionState.ts +2 -21
  62. package/src/projection/projectionQuery.ts +2 -3
  63. package/src/projection/projectionStore.test.ts +3 -3
  64. package/src/projection/resolveProjection.test.ts +2 -1
  65. package/src/projection/resolveProjection.ts +2 -18
  66. package/src/projection/subscribeToStateAndFetchBatches.test.ts +2 -2
  67. package/src/projection/subscribeToStateAndFetchBatches.ts +23 -36
  68. package/src/projection/types.ts +1 -9
  69. package/src/projects/projects.test.ts +1 -1
  70. package/src/query/queryStore.test.ts +86 -28
  71. package/src/query/queryStore.ts +23 -38
  72. package/src/releases/getPerspectiveState.test.ts +14 -13
  73. package/src/releases/getPerspectiveState.ts +6 -6
  74. package/src/releases/releasesStore.test.ts +21 -6
  75. package/src/releases/releasesStore.ts +18 -8
  76. package/src/store/createActionBinder.test.ts +114 -111
  77. package/src/store/createActionBinder.ts +52 -101
  78. package/src/store/createSanityInstance.test.ts +13 -83
  79. package/src/store/createSanityInstance.ts +2 -78
  80. package/src/store/createStateSourceAction.test.ts +2 -2
  81. package/src/store/createStateSourceAction.ts +5 -5
  82. package/src/store/createStoreInstance.test.ts +2 -4
  83. package/src/users/reducers.test.ts +1 -6
  84. package/src/users/reducers.ts +2 -2
  85. package/src/users/types.ts +4 -4
  86. package/src/users/usersStore.test.ts +12 -15
  87. package/src/utils/createFetcherStore.test.ts +1 -1
  88. package/src/utils/logger.test.ts +0 -12
  89. package/src/utils/logger.ts +3 -8
  90. package/src/preview/getPreviewState.test.ts +0 -120
  91. package/src/preview/getPreviewState.ts +0 -91
  92. package/src/preview/previewQuery.test.ts +0 -236
  93. package/src/preview/previewQuery.ts +0 -153
  94. package/src/preview/previewStore.test.ts +0 -36
  95. package/src/preview/resolvePreview.test.ts +0 -47
  96. package/src/preview/resolvePreview.ts +0 -20
  97. package/src/preview/subscribeToStateAndFetchBatches.test.ts +0 -221
  98. package/src/preview/subscribeToStateAndFetchBatches.ts +0 -112
  99. 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 {getAuthCode, getDefaultStorage, getTokenFromLocation, getTokenFromStorage} from './utils'
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: {type: AuthStateType.LOGGED_IN, token: providedToken, currentUser: null},
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: {type: AuthStateType.LOGGED_IN, token, currentUser: null},
62
+ authState: createLoggedInAuthState(token, null),
57
63
  storageKey,
58
64
  storageArea,
59
65
  authMethod: 'localstorage',
@@ -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: {type: AuthStateType.LOGGED_IN, token: providedToken, currentUser: null},
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: {type: AuthStateType.LOGGED_IN, token, currentUser: null},
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
- * The Studio is the single authority for auth — the SDK does not run
119
- * its own token refresher or cookie auth checks.
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: {type: AuthStateType.LOGGED_IN, token, currentUser: null},
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
- : {type: AuthStateType.LOGGED_IN, token: '', currentUser: null},
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({projectId: 'p', dataset: 'd', auth: {clientFactory}})
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({projectId: 'p', dataset: 'd', auth: {clientFactory}})
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({projectId: 'p', dataset: 'd', auth: {clientFactory}})
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(
@@ -23,8 +23,6 @@ describe('subscribeToStorageEventsAndSetToken', () => {
23
23
  vi.clearAllMocks()
24
24
 
25
25
  instance = createSanityInstance({
26
- projectId: 'p',
27
- dataset: 'd',
28
26
  auth: {storageArea: storageArea},
29
27
  })
30
28
 
@@ -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
- ? {type: AuthStateType.LOGGED_IN, token, currentUser: null}
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
- source: {projectId: 'source-project', dataset: 'source-dataset'},
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
- source: {canvasId: 'canvas-123'},
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
- it('should create projectless client when source is provided, ignoring instance config', () => {
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
- source: {projectId: 'source-project', dataset: 'source-dataset'},
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
- source: {projectId: 'source-project', dataset: 'source-dataset'},
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 source and explicit projectId/dataset are provided. The source will be used and projectId/dataset will be ignored.',
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
- source: {projectId: 'source-project', dataset: 'source-dataset'},
236
+ resource: {projectId: 'source-project', dataset: 'source-dataset'},
284
237
  })
285
238
  const client2 = getClient(instance, {
286
239
  apiVersion: '2024-11-12',
287
- source: {mediaLibraryId: 'media-lib-123'},
240
+ resource: {mediaLibraryId: 'media-lib-123'},
288
241
  })
289
242
  const client3 = getClient(instance, {
290
243
  apiVersion: '2024-11-12',
291
- source: {canvasId: 'canvas-123'},
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
- source: {projectId: 'source-project', dataset: 'source-dataset'},
256
+ resource: {projectId: 'source-project', dataset: 'source-dataset'},
304
257
  })
305
258
  const client2 = getClient(instance, {
306
259
  apiVersion: '2024-11-12',
307
- source: {projectId: 'source-project', dataset: 'source-dataset'},
260
+ resource: {projectId: 'source-project', dataset: 'source-dataset'},
308
261
  })
309
262
 
310
263
  expect(client1).toBe(client2)