@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.2

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 (241) hide show
  1. package/README.md +12 -0
  2. package/dist/_exports/_internal.d.ts +163 -0
  3. package/dist/_exports/_internal.d.ts.map +1 -0
  4. package/dist/_exports/_internal.js +332 -0
  5. package/dist/_exports/_internal.js.map +1 -0
  6. package/dist/_exports/agent.d.ts +65 -0
  7. package/dist/_exports/agent.d.ts.map +1 -0
  8. package/dist/_exports/agent.js +71 -0
  9. package/dist/_exports/agent.js.map +1 -0
  10. package/dist/_exports/comlink.d.ts +118 -0
  11. package/dist/_exports/comlink.d.ts.map +1 -0
  12. package/dist/_exports/comlink.js +69 -0
  13. package/dist/_exports/comlink.js.map +1 -0
  14. package/dist/authStore-CtE9bLHW.js +754 -0
  15. package/dist/authStore-CtE9bLHW.js.map +1 -0
  16. package/dist/authStore-DKFX8fn7.d.ts +3179 -0
  17. package/dist/authStore-DKFX8fn7.d.ts.map +1 -0
  18. package/dist/clientStore-BtE2-q2i.js +90 -0
  19. package/dist/clientStore-BtE2-q2i.js.map +1 -0
  20. package/dist/createSanityInstance-DUI1bGIg.d.ts +278 -0
  21. package/dist/createSanityInstance-DUI1bGIg.d.ts.map +1 -0
  22. package/dist/createStateSourceAction-BOKtqFNm.js +300 -0
  23. package/dist/createStateSourceAction-BOKtqFNm.js.map +1 -0
  24. package/dist/getEnv-D5RGkQLu.js +8 -0
  25. package/dist/getEnv-D5RGkQLu.js.map +1 -0
  26. package/dist/getNodeState-CR3__B2D.js +67 -0
  27. package/dist/getNodeState-CR3__B2D.js.map +1 -0
  28. package/dist/index.d.ts +3 -2332
  29. package/dist/index.js +4171 -4332
  30. package/dist/index.js.map +1 -1
  31. package/dist/logger-DAtJb5RQ.js +173 -0
  32. package/dist/logger-DAtJb5RQ.js.map +1 -0
  33. package/dist/object-CxXlSVg7.js +83 -0
  34. package/dist/object-CxXlSVg7.js.map +1 -0
  35. package/dist/queryStore-DPf9sN3V.js +1346 -0
  36. package/dist/queryStore-DPf9sN3V.js.map +1 -0
  37. package/dist/rolldown-runtime-vyAXikos.js +9 -0
  38. package/dist/setCleanupTimeout-BiM6q1KF.js +31 -0
  39. package/dist/setCleanupTimeout-BiM6q1KF.js.map +1 -0
  40. package/dist/telemetryManager-Dq_qCkeI.js +127 -0
  41. package/dist/telemetryManager-Dq_qCkeI.js.map +1 -0
  42. package/dist/version-BChjsi4u.js +9 -0
  43. package/dist/version-BChjsi4u.js.map +1 -0
  44. package/package.json +47 -52
  45. package/src/_exports/_internal.ts +27 -0
  46. package/src/_exports/agent.ts +19 -0
  47. package/src/_exports/comlink.ts +16 -0
  48. package/src/_exports/index.ts +125 -64
  49. package/src/access/checkPermissions.test.ts +55 -0
  50. package/src/access/checkPermissions.ts +54 -0
  51. package/src/agent/agentActions.test.ts +16 -60
  52. package/src/agent/agentActions.ts +25 -38
  53. package/src/applications/applications.test-d.ts +88 -0
  54. package/src/applications/applications.test.ts +130 -0
  55. package/src/applications/applications.ts +367 -0
  56. package/src/auth/authLogger.ts +30 -0
  57. package/src/auth/authStore.test.ts +194 -13
  58. package/src/auth/authStore.ts +58 -26
  59. package/src/auth/getOrganizationVerificationState.test.ts +18 -17
  60. package/src/auth/getOrganizationVerificationState.ts +4 -4
  61. package/src/auth/handleAuthCallback.test.ts +35 -1
  62. package/src/auth/handleAuthCallback.ts +25 -6
  63. package/src/auth/logout.test.ts +75 -2
  64. package/src/auth/logout.ts +23 -4
  65. package/src/auth/refreshStampedToken.test.ts +219 -469
  66. package/src/auth/refreshStampedToken.ts +79 -195
  67. package/src/auth/studioAuth.ts +1 -1
  68. package/src/auth/studioModeAuth.ts +2 -1
  69. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
  70. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
  71. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
  72. package/src/client/clientStore.test.ts +62 -37
  73. package/src/client/clientStore.ts +45 -75
  74. package/src/client/liveEvents.test.ts +151 -0
  75. package/src/client/liveEvents.ts +107 -0
  76. package/src/comlink/controller/actions/destroyController.test.ts +4 -1
  77. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
  78. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  79. package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
  80. package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
  81. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
  82. package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
  83. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  84. package/src/comlink/node/actions/releaseNode.test.ts +7 -4
  85. package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
  86. package/src/comlink/node/getNodeState.test.ts +1 -1
  87. package/src/comlink/node/getNodeState.ts +2 -1
  88. package/src/comments/addonDatasetStore.test.ts +215 -0
  89. package/src/comments/addonDatasetStore.ts +249 -0
  90. package/src/comments/buildCommentThreads.test.ts +83 -0
  91. package/src/comments/buildCommentThreads.ts +54 -0
  92. package/src/comments/commentActions.test.ts +540 -0
  93. package/src/comments/commentActions.ts +555 -0
  94. package/src/comments/commentFieldPath.test.ts +40 -0
  95. package/src/comments/commentFieldPath.ts +44 -0
  96. package/src/comments/commentsConstants.ts +80 -0
  97. package/src/comments/commentsStore.test.ts +419 -0
  98. package/src/comments/commentsStore.ts +483 -0
  99. package/src/comments/normalizeComment.test.ts +116 -0
  100. package/src/comments/normalizeComment.ts +45 -0
  101. package/src/comments/observeComments.test.ts +193 -0
  102. package/src/comments/observeComments.ts +113 -0
  103. package/src/comments/reducers.test.ts +277 -0
  104. package/src/comments/reducers.ts +316 -0
  105. package/src/comments/types.ts +249 -0
  106. package/src/comments/weakenReferences.test.ts +47 -0
  107. package/src/comments/weakenReferences.ts +29 -0
  108. package/src/config/__tests__/handles.test.ts +18 -12
  109. package/src/config/handles.ts +25 -7
  110. package/src/config/sanityConfig.ts +40 -82
  111. package/src/datasets/datasets.test.ts +2 -2
  112. package/src/datasets/datasets.ts +10 -4
  113. package/src/document/actions.test.ts +115 -33
  114. package/src/document/actions.ts +212 -15
  115. package/src/document/applyDocumentActions.test.ts +35 -13
  116. package/src/document/applyDocumentActions.ts +30 -9
  117. package/src/document/documentConstants.ts +15 -0
  118. package/src/document/documentStore.concurrency.test.ts +1306 -0
  119. package/src/document/documentStore.test.ts +753 -273
  120. package/src/document/documentStore.ts +197 -75
  121. package/src/document/events.test.ts +57 -2
  122. package/src/document/events.ts +82 -26
  123. package/src/document/listen.test.ts +56 -0
  124. package/src/document/listen.ts +74 -21
  125. package/src/document/listenerEventOperators.test.ts +311 -0
  126. package/src/document/listenerEventOperators.ts +217 -0
  127. package/src/document/patchOperations.test.ts +76 -2
  128. package/src/document/patchOperations.ts +60 -1
  129. package/src/document/permissions.test.ts +107 -60
  130. package/src/document/permissions.ts +44 -19
  131. package/src/document/processActions/create.ts +138 -0
  132. package/src/document/processActions/delete.ts +100 -0
  133. package/src/document/processActions/discard.ts +63 -0
  134. package/src/document/processActions/edit.ts +154 -0
  135. package/src/document/processActions/processActions.ts +218 -0
  136. package/src/document/processActions/publish.ts +120 -0
  137. package/src/document/processActions/releaseArchive.ts +77 -0
  138. package/src/document/processActions/releaseCreate.ts +59 -0
  139. package/src/document/processActions/releaseDelete.ts +65 -0
  140. package/src/document/processActions/releaseEdit.ts +37 -0
  141. package/src/document/processActions/releasePublish.ts +45 -0
  142. package/src/document/processActions/releaseSchedule.ts +87 -0
  143. package/src/document/processActions/releaseUtil.ts +31 -0
  144. package/src/document/processActions/shared.ts +308 -0
  145. package/src/document/processActions/unpublish.ts +85 -0
  146. package/src/document/processActions.test.ts +1129 -121
  147. package/src/document/reducers.test.ts +772 -64
  148. package/src/document/reducers.ts +205 -51
  149. package/src/document/resourceRules.test.ts +178 -0
  150. package/src/document/resourceRules.ts +117 -0
  151. package/src/document/sharedListener.test.ts +16 -14
  152. package/src/document/sharedListener.ts +24 -8
  153. package/src/document/util.test.ts +46 -0
  154. package/src/document/util.ts +50 -0
  155. package/src/favorites/favorites.test.ts +5 -6
  156. package/src/installations/installations.test-d.ts +73 -0
  157. package/src/installations/installations.test.ts +62 -0
  158. package/src/installations/installations.ts +206 -0
  159. package/src/organization/organization.test-d.ts +36 -0
  160. package/src/organization/organization.test.ts +138 -0
  161. package/src/organization/organization.ts +138 -0
  162. package/src/organizations/organizations.test-d.ts +56 -0
  163. package/src/organizations/organizations.test.ts +150 -0
  164. package/src/organizations/organizations.ts +107 -0
  165. package/src/presence/bifurTransport.test.ts +201 -9
  166. package/src/presence/bifurTransport.ts +98 -14
  167. package/src/presence/paths.test.ts +70 -0
  168. package/src/presence/paths.ts +41 -0
  169. package/src/presence/presenceStore.test.ts +915 -24
  170. package/src/presence/presenceStore.ts +576 -104
  171. package/src/presence/types.ts +175 -3
  172. package/src/preview/previewProjectionUtils.test.ts +9 -22
  173. package/src/preview/previewProjectionUtils.ts +7 -3
  174. package/src/preview/types.ts +0 -15
  175. package/src/project/project.test-d.ts +37 -0
  176. package/src/project/project.test.ts +157 -12
  177. package/src/project/project.ts +138 -25
  178. package/src/projection/getProjectionState.test.ts +18 -20
  179. package/src/projection/getProjectionState.ts +23 -3
  180. package/src/projection/projectionQuery.ts +5 -5
  181. package/src/projection/projectionStore.test.ts +1 -9
  182. package/src/projection/resolveProjection.test.ts +1 -2
  183. package/src/projection/resolveProjection.ts +16 -0
  184. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  185. package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
  186. package/src/projects/projects.test-d.ts +32 -0
  187. package/src/projects/projects.test.ts +108 -42
  188. package/src/projects/projects.ts +63 -27
  189. package/src/query/queryStore.test.ts +259 -123
  190. package/src/query/queryStore.ts +85 -69
  191. package/src/query/reducers.test.ts +0 -42
  192. package/src/query/reducers.ts +2 -18
  193. package/src/releases/getPerspectiveState.test.ts +17 -23
  194. package/src/releases/getPerspectiveState.ts +8 -20
  195. package/src/releases/observeReleases.test.ts +127 -0
  196. package/src/releases/observeReleases.ts +102 -0
  197. package/src/releases/releasesStore.test.ts +100 -53
  198. package/src/releases/releasesStore.ts +69 -44
  199. package/src/releases/utils/sortReleases.test.ts +38 -2
  200. package/src/releases/utils/sortReleases.ts +15 -10
  201. package/src/store/createActionBinder.test.ts +109 -90
  202. package/src/store/createActionBinder.ts +36 -28
  203. package/src/store/createSanityInstance.test.ts +15 -13
  204. package/src/store/createSanityInstance.ts +14 -2
  205. package/src/store/createStateSourceAction.test.ts +64 -2
  206. package/src/store/createStateSourceAction.ts +38 -43
  207. package/src/store/createStoreInstance.test.ts +4 -2
  208. package/src/store/fetcherStore.test-d.ts +121 -0
  209. package/src/store/fetcherStore.test.ts +299 -0
  210. package/src/store/fetcherStore.ts +801 -0
  211. package/src/telemetry/environment.test.ts +119 -0
  212. package/src/telemetry/environment.ts +92 -0
  213. package/src/telemetry/events.ts +42 -0
  214. package/src/telemetry/initTelemetry.test.ts +432 -0
  215. package/src/telemetry/initTelemetry.ts +218 -0
  216. package/src/telemetry/telemetryManager.test.ts +327 -0
  217. package/src/telemetry/telemetryManager.ts +199 -0
  218. package/src/users/reducers.test.ts +3 -1
  219. package/src/users/reducers.ts +5 -6
  220. package/src/users/types.ts +4 -4
  221. package/src/users/usersStore.test.ts +16 -12
  222. package/src/users/usersStore.ts +5 -1
  223. package/src/utils/buildQuery.test.ts +14 -0
  224. package/src/utils/buildQuery.ts +17 -0
  225. package/src/utils/createFetcherStore.test.ts +7 -5
  226. package/src/utils/createFetcherStore.ts +8 -5
  227. package/src/utils/getStagingApiHost.test.ts +21 -0
  228. package/src/utils/getStagingApiHost.ts +14 -0
  229. package/src/utils/ids.test.ts +1 -29
  230. package/src/utils/ids.ts +0 -10
  231. package/src/utils/includeShape.ts +36 -0
  232. package/src/utils/isImportError.test.ts +72 -0
  233. package/src/utils/isImportError.ts +34 -0
  234. package/src/utils/logger.test.ts +12 -0
  235. package/src/utils/logger.ts +8 -3
  236. package/src/utils/object.test.ts +95 -0
  237. package/src/utils/object.ts +142 -0
  238. package/src/utils/setCleanupTimeout.ts +37 -0
  239. package/src/document/processActions.ts +0 -707
  240. package/src/utils/defineIntent.test.ts +0 -477
  241. package/src/utils/defineIntent.ts +0 -244
@@ -2,8 +2,6 @@ import {
2
2
  distinctUntilChanged,
3
3
  exhaustMap,
4
4
  filter,
5
- firstValueFrom,
6
- from,
7
5
  map,
8
6
  Observable,
9
7
  type Subscription,
@@ -13,42 +11,12 @@ import {
13
11
  } from 'rxjs'
14
12
 
15
13
  import {type StoreContext} from '../store/defineStore'
16
- import {DEFAULT_API_VERSION} from './authConstants'
14
+ import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
15
+ import {getAuthLogger} from './authLogger'
17
16
  import {AuthStateType} from './authStateType'
18
17
  import {type AuthState, type AuthStoreState} from './authStore'
19
18
 
20
19
  const REFRESH_INTERVAL = 12 * 60 * 60 * 1000 // 12 hours in milliseconds
21
- const LOCK_NAME = 'sanity-token-refresh-lock'
22
-
23
- /** @internal */
24
- export function getLastRefreshTime(storageArea: Storage | undefined, storageKey: string): number {
25
- try {
26
- const data = storageArea?.getItem(`${storageKey}_last_refresh`)
27
- const parsed = data ? parseInt(data, 10) : 0
28
- return isNaN(parsed) ? 0 : parsed
29
- } catch {
30
- return 0
31
- }
32
- }
33
-
34
- /** @internal */
35
- export function setLastRefreshTime(storageArea: Storage | undefined, storageKey: string): void {
36
- try {
37
- storageArea?.setItem(`${storageKey}_last_refresh`, Date.now().toString())
38
- } catch {
39
- // Ignore storage errors
40
- }
41
- }
42
-
43
- /** @internal */
44
- export function getNextRefreshDelay(storageArea: Storage | undefined, storageKey: string): number {
45
- const lastRefresh = getLastRefreshTime(storageArea, storageKey)
46
- if (!lastRefresh) return 0
47
-
48
- const now = Date.now()
49
- const nextRefreshTime = lastRefresh + REFRESH_INTERVAL
50
- return Math.max(0, nextRefreshTime - now)
51
- }
52
20
 
53
21
  function createTokenRefreshStream(
54
22
  token: string,
@@ -58,7 +26,7 @@ function createTokenRefreshStream(
58
26
  return new Observable((subscriber) => {
59
27
  const client = clientFactory({
60
28
  apiVersion: DEFAULT_API_VERSION,
61
- requestTagPrefix: 'token-refresh',
29
+ requestTagPrefix: REQUEST_TAG_PREFIX,
62
30
  useProjectHostname: false,
63
31
  useCdn: false,
64
32
  token,
@@ -70,6 +38,7 @@ function createTokenRefreshStream(
70
38
  .request<{token: string}>({
71
39
  uri: 'auth/refresh-token',
72
40
  method: 'POST',
41
+ tag: 'refresh-token',
73
42
  body: {
74
43
  token,
75
44
  },
@@ -80,57 +49,6 @@ function createTokenRefreshStream(
80
49
  })
81
50
  }
82
51
 
83
- async function acquireTokenRefreshLock(
84
- refreshFn: () => Promise<void>,
85
- storageArea: Storage | undefined,
86
- storageKey: string,
87
- ): Promise<boolean> {
88
- if (!navigator.locks) {
89
- // If Web Locks API is not supported, perform an immediate, uncoordinated refresh.
90
- // eslint-disable-next-line no-console
91
- console.warn('Web Locks API not supported. Proceeding with uncoordinated refresh.')
92
- await refreshFn()
93
- setLastRefreshTime(storageArea, storageKey)
94
- return true // Indicate success to allow stream processing, though it won't loop.
95
- }
96
-
97
- try {
98
- // Attempt to acquire an exclusive lock for token refresh coordination.
99
- // The callback handles the continuous refresh cycle while the lock is held.
100
- const result = await navigator.locks.request(LOCK_NAME, {mode: 'exclusive'}, async (lock) => {
101
- if (!lock) return false // Lock not granted
102
-
103
- // Problematic infinite loop - needs redesign for graceful termination.
104
- // This loop continuously refreshes the token at REFRESH_INTERVAL.
105
- while (true) {
106
- const delay = getNextRefreshDelay(storageArea, storageKey)
107
- if (delay > 0) {
108
- await new Promise((resolve) => setTimeout(resolve, delay))
109
- }
110
- try {
111
- await refreshFn()
112
- setLastRefreshTime(storageArea, storageKey)
113
- } catch (error) {
114
- // eslint-disable-next-line no-console
115
- console.error('Token refresh failed within lock:', error)
116
- // Decide how to handle errors - break, retry, etc.? Currently logs and continues.
117
- }
118
- // Wait for the next interval
119
- await new Promise((resolve) => setTimeout(resolve, REFRESH_INTERVAL))
120
- }
121
- // Unreachable due to while(true)
122
- })
123
- // The promise from navigator.locks.request resolves with the callback's return value,
124
- // but only if the callback finishes. The infinite loop prevents this.
125
- return result === true
126
- } catch (error) {
127
- // Handle potential errors during the initial lock request itself.
128
- // eslint-disable-next-line no-console
129
- console.error('Failed to request token refresh lock:', error)
130
- return false // Indicate lock acquisition failure.
131
- }
132
- }
133
-
134
52
  function shouldRefreshToken(lastRefresh: number | undefined): boolean {
135
53
  if (!lastRefresh) return true
136
54
  const timeSinceLastRefresh = Date.now() - lastRefresh
@@ -138,99 +56,46 @@ function shouldRefreshToken(lastRefresh: number | undefined): boolean {
138
56
  }
139
57
 
140
58
  /**
59
+ * Periodically refreshes stamped auth tokens (those containing `-st`).
60
+ *
61
+ * Two triggers cause a refresh while the user stays logged in:
62
+ * - a 12-hour interval timer (only fires while the tab is visible)
63
+ * - the tab becoming visible again after `lastTokenRefresh` has gone stale
64
+ *
65
+ * Non-stamped tokens (e.g. personal access tokens) are never refreshed.
66
+ *
141
67
  * @internal
142
68
  */
143
- export const refreshStampedToken = ({state}: StoreContext<AuthStoreState>): Subscription => {
69
+ export const refreshStampedToken = ({
70
+ state,
71
+ instance,
72
+ }: StoreContext<AuthStoreState>): Subscription => {
73
+ const logger = getAuthLogger(instance)
74
+
144
75
  const {clientFactory, apiHost, storageArea, storageKey} = state.get().options
145
76
 
146
77
  const refreshToken$ = state.observable.pipe(
147
- map((storeState) => ({
148
- authState: storeState.authState,
149
- dashboardContext: storeState.dashboardContext,
150
- })),
78
+ map((storeState) => storeState.authState),
151
79
  filter(
152
- (
153
- storeState,
154
- ): storeState is {
155
- authState: Extract<AuthState, {type: AuthStateType.LOGGED_IN}>
156
- dashboardContext: AuthStoreState['dashboardContext']
157
- } => storeState.authState.type === AuthStateType.LOGGED_IN,
80
+ (authState): authState is Extract<AuthState, {type: AuthStateType.LOGGED_IN}> =>
81
+ authState.type === AuthStateType.LOGGED_IN,
158
82
  ),
159
- distinctUntilChanged(
160
- (prev, curr) =>
161
- prev.authState.type === curr.authState.type &&
162
- prev.authState.token === curr.authState.token && // Only care about token for distinctness here
163
- prev.dashboardContext === curr.dashboardContext,
164
- ), // Make distinctness check explicit
165
- filter((storeState) => storeState.authState.token.includes('-st')), // Ensure we only try to refresh stamped tokens
166
- exhaustMap((storeState) => {
167
- // USE exhaustMap instead of switchMap
168
- // Create a function that performs a single refresh and updates state/storage
169
- const performRefresh = async () => {
170
- // Read the latest token directly from state inside refresh
171
- const currentState = state.get()
172
- if (currentState.authState.type !== AuthStateType.LOGGED_IN) {
173
- throw new Error('User logged out before refresh could complete') // Abort refresh
174
- }
175
- const currentToken = currentState.authState.token
176
-
177
- const response = await firstValueFrom(
178
- createTokenRefreshStream(currentToken, clientFactory, apiHost),
179
- )
180
-
181
- state.set('setRefreshStampedToken', (prev) => ({
182
- authState:
183
- prev.authState.type === AuthStateType.LOGGED_IN
184
- ? {...prev.authState, token: response.token}
185
- : prev.authState,
186
- }))
187
- storageArea?.setItem(storageKey, JSON.stringify({token: response.token}))
188
- }
189
-
190
- if (storeState.dashboardContext) {
191
- return new Observable<{token: string}>((subscriber) => {
192
- const visibilityHandler = () => {
193
- const currentState = state.get()
194
- if (
195
- document.visibilityState === 'visible' &&
196
- currentState.authState.type === AuthStateType.LOGGED_IN &&
197
- shouldRefreshToken(currentState.authState.lastTokenRefresh)
198
- ) {
199
- createTokenRefreshStream(
200
- currentState.authState.token,
201
- clientFactory,
202
- apiHost,
203
- ).subscribe({
204
- next: (response) => {
205
- state.set('setRefreshStampedToken', (prev) => ({
206
- authState:
207
- prev.authState.type === AuthStateType.LOGGED_IN
208
- ? {
209
- ...prev.authState,
210
- token: response.token,
211
- lastTokenRefresh: Date.now(),
212
- }
213
- : prev.authState,
214
- }))
215
- subscriber.next(response)
216
- },
217
- error: (error) => subscriber.error(error),
218
- })
219
- }
220
- }
221
-
222
- const timerSubscription = timer(REFRESH_INTERVAL, REFRESH_INTERVAL)
223
- .pipe(
224
- filter(() => document.visibilityState === 'visible'),
225
- switchMap(() => {
226
- const currentState = state.get().authState
227
- if (currentState.type !== AuthStateType.LOGGED_IN) {
228
- throw new Error('User logged out before refresh could complete')
229
- }
230
- return createTokenRefreshStream(currentState.token, clientFactory, apiHost)
231
- }),
232
- )
233
- .subscribe({
83
+ distinctUntilChanged((prev, curr) => prev.token === curr.token),
84
+ filter((authState) => authState.token.includes('-st')), // Ensure we only try to refresh stamped tokens
85
+ exhaustMap(() =>
86
+ new Observable<{token: string}>((subscriber) => {
87
+ const visibilityHandler = () => {
88
+ const currentState = state.get()
89
+ if (
90
+ document.visibilityState === 'visible' &&
91
+ currentState.authState.type === AuthStateType.LOGGED_IN &&
92
+ shouldRefreshToken(currentState.authState.lastTokenRefresh)
93
+ ) {
94
+ createTokenRefreshStream(
95
+ currentState.authState.token,
96
+ clientFactory,
97
+ apiHost,
98
+ ).subscribe({
234
99
  next: (response) => {
235
100
  state.set('setRefreshStampedToken', (prev) => ({
236
101
  authState:
@@ -246,35 +111,53 @@ export const refreshStampedToken = ({state}: StoreContext<AuthStoreState>): Subs
246
111
  },
247
112
  error: (error) => subscriber.error(error),
248
113
  })
249
-
250
- document.addEventListener('visibilitychange', visibilityHandler)
251
-
252
- return () => {
253
- document.removeEventListener('visibilitychange', visibilityHandler)
254
- timerSubscription.unsubscribe()
255
114
  }
256
- }).pipe(
257
- takeWhile(() => state.get().authState.type === AuthStateType.LOGGED_IN),
258
- map((response: {token: string}) => ({token: response.token})),
259
- )
260
- }
115
+ }
261
116
 
262
- // If not in dashboard context, use lock-based refresh
263
- return from(acquireTokenRefreshLock(performRefresh, storageArea, storageKey)).pipe(
264
- filter((hasLock) => hasLock),
265
- map(() => {
266
- const currentState = state.get().authState
267
- if (currentState.type !== AuthStateType.LOGGED_IN) {
268
- throw new Error('User logged out before refresh could complete')
269
- }
270
- return {token: currentState.token} as const
271
- }),
272
- )
273
- }),
117
+ const timerSubscription = timer(REFRESH_INTERVAL, REFRESH_INTERVAL)
118
+ .pipe(
119
+ filter(() => document.visibilityState === 'visible'),
120
+ switchMap(() => {
121
+ const currentState = state.get().authState
122
+ if (currentState.type !== AuthStateType.LOGGED_IN) {
123
+ throw new Error('User logged out before refresh could complete')
124
+ }
125
+ return createTokenRefreshStream(currentState.token, clientFactory, apiHost)
126
+ }),
127
+ )
128
+ .subscribe({
129
+ next: (response) => {
130
+ state.set('setRefreshStampedToken', (prev) => ({
131
+ authState:
132
+ prev.authState.type === AuthStateType.LOGGED_IN
133
+ ? {
134
+ ...prev.authState,
135
+ token: response.token,
136
+ lastTokenRefresh: Date.now(),
137
+ }
138
+ : prev.authState,
139
+ }))
140
+ subscriber.next(response)
141
+ },
142
+ error: (error) => subscriber.error(error),
143
+ })
144
+
145
+ document.addEventListener('visibilitychange', visibilityHandler)
146
+
147
+ return () => {
148
+ document.removeEventListener('visibilitychange', visibilityHandler)
149
+ timerSubscription.unsubscribe()
150
+ }
151
+ }).pipe(
152
+ takeWhile(() => state.get().authState.type === AuthStateType.LOGGED_IN),
153
+ map((response: {token: string}) => ({token: response.token})),
154
+ ),
155
+ ),
274
156
  )
275
157
 
276
158
  return refreshToken$.subscribe({
277
159
  next: (response: {token: string}) => {
160
+ logger.debug('Token refresh completed, updating state')
278
161
  state.set('setRefreshStampedToken', (prev) => ({
279
162
  authState:
280
163
  prev.authState.type === AuthStateType.LOGGED_IN
@@ -288,6 +171,7 @@ export const refreshStampedToken = ({state}: StoreContext<AuthStoreState>): Subs
288
171
  storageArea?.setItem(storageKey, JSON.stringify({token: response.token}))
289
172
  },
290
173
  error: (error) => {
174
+ logger.error('Token refresh failed', {error})
291
175
  state.set('setRefreshStampedTokenError', {authState: {type: AuthStateType.ERROR, error}})
292
176
  },
293
177
  })
@@ -211,7 +211,7 @@ function initializeWithFallback(
211
211
  : null
212
212
 
213
213
  if (!token) {
214
- const projectIdValue = instance.config.studio?.projectId
214
+ const projectIdValue = instance.config.projectId
215
215
  const clientFactory = state.get().options.clientFactory
216
216
  checkForCookieAuth(projectIdValue, clientFactory).then((isCookieAuthEnabled) => {
217
217
  if (!isCookieAuthEnabled) return
@@ -1,5 +1,6 @@
1
1
  import {type ClientConfig, type SanityClient} from '@sanity/client'
2
2
 
3
+ import {REQUEST_TAG_PREFIX} from './authConstants'
3
4
  import {getTokenFromStorage} from './utils'
4
5
 
5
6
  /**
@@ -25,7 +26,7 @@ export async function checkForCookieAuth(
25
26
  const client = clientFactory({
26
27
  projectId,
27
28
  useCdn: false,
28
- requestTagPrefix: 'sdk',
29
+ requestTagPrefix: REQUEST_TAG_PREFIX,
29
30
  timeout: COOKIE_AUTH_TIMEOUT_MS,
30
31
  })
31
32
  const user = await client.request({
@@ -1,6 +1,6 @@
1
1
  import {type CurrentUser} from '@sanity/types'
2
2
  import {delay, of, throwError} from 'rxjs'
3
- import {beforeEach, describe, it} from 'vitest'
3
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
4
4
 
5
5
  import {createSanityInstance} from '../store/createSanityInstance'
6
6
  import {createStoreState} from '../store/createStoreState'
@@ -8,19 +8,32 @@ import {AuthStateType} from './authStateType'
8
8
  import {authStore} from './authStore'
9
9
  import {subscribeToStateAndFetchCurrentUser} from './subscribeToStateAndFetchCurrentUser'
10
10
 
11
+ // Mock logger to prevent actual logging during tests
12
+ vi.mock('../utils/logger', async (importOriginal) => {
13
+ const original = await importOriginal<typeof import('../utils/logger')>()
14
+ return {
15
+ ...original,
16
+ createLogger: vi.fn(() => ({
17
+ info: vi.fn(),
18
+ debug: vi.fn(),
19
+ warn: vi.fn(),
20
+ error: vi.fn(),
21
+ trace: vi.fn(),
22
+ })),
23
+ }
24
+ })
25
+
11
26
  describe('subscribeToStateAndFetchCurrentUser', () => {
12
27
  beforeEach(() => {
13
28
  vi.clearAllMocks()
14
29
  })
15
30
 
16
31
  it('fetches the current user if the is logged in without a current user present', () => {
17
- const mockUser = {id: 'example-user'} as CurrentUser
32
+ const mockUser = {id: 'example-user', email: 'user@example.com'} as CurrentUser
18
33
  const mockRequest = vi.fn().mockReturnValue(of(mockUser))
19
34
  const mockClient = {observable: {request: mockRequest}}
20
35
  const clientFactory = vi.fn().mockReturnValue(mockClient)
21
- const instance = createSanityInstance({
22
- auth: {clientFactory},
23
- })
36
+ const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
24
37
 
25
38
  const state = createStoreState(authStore.getInitialState(instance, null))
26
39
  const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
@@ -42,7 +55,11 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
42
55
  useProjectHostname: false,
43
56
  useCdn: false,
44
57
  })
45
- expect(mockRequest).toHaveBeenCalledWith({method: 'GET', uri: '/users/me'})
58
+ expect(mockRequest).toHaveBeenCalledWith({
59
+ method: 'GET',
60
+ uri: '/users/me',
61
+ tag: 'users.get-current',
62
+ })
46
63
 
47
64
  subscription.unsubscribe()
48
65
  })
@@ -52,9 +69,7 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
52
69
  const mockRequest = vi.fn().mockReturnValue(of(mockUser).pipe(delay(0)))
53
70
  const mockClient = {observable: {request: mockRequest}}
54
71
  const clientFactory = vi.fn().mockReturnValue(mockClient)
55
- const instance = createSanityInstance({
56
- auth: {clientFactory},
57
- })
72
+ const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
58
73
 
59
74
  const state = createStoreState(authStore.getInitialState(instance, null))
60
75
  const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
@@ -80,7 +95,11 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
80
95
  useProjectHostname: false,
81
96
  useCdn: false,
82
97
  })
83
- expect(mockRequest).toHaveBeenCalledWith({method: 'GET', uri: '/users/me'})
98
+ expect(mockRequest).toHaveBeenCalledWith({
99
+ method: 'GET',
100
+ uri: '/users/me',
101
+ tag: 'users.get-current',
102
+ })
84
103
 
85
104
  subscription.unsubscribe()
86
105
  })
@@ -90,9 +109,7 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
90
109
  const mockRequest = vi.fn().mockReturnValue(throwError(() => error))
91
110
  const mockClient = {observable: {request: mockRequest}}
92
111
  const clientFactory = vi.fn().mockReturnValue(mockClient)
93
- const instance = createSanityInstance({
94
- auth: {clientFactory},
95
- })
112
+ const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
96
113
 
97
114
  const state = createStoreState(authStore.getInitialState(instance, null))
98
115
  const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
@@ -110,4 +127,120 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
110
127
 
111
128
  subscription.unsubscribe()
112
129
  })
130
+
131
+ it('recovers from a fetch error when a new token is set', () => {
132
+ const error = new Error('Unauthorized')
133
+ const mockUser = {id: 'recovered-user'} as CurrentUser
134
+ const mockRequest = vi
135
+ .fn()
136
+ .mockReturnValueOnce(throwError(() => error))
137
+ .mockReturnValueOnce(of(mockUser))
138
+ const mockClient = {observable: {request: mockRequest}}
139
+ const clientFactory = vi.fn().mockReturnValue(mockClient)
140
+ const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
141
+
142
+ const state = createStoreState(authStore.getInitialState(instance, null))
143
+ const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
144
+
145
+ // First token causes a 401 — state should transition to ERROR
146
+ state.set('setLoggedIn', {
147
+ authState: {type: AuthStateType.LOGGED_IN, token: 'expired-token', currentUser: null},
148
+ })
149
+ expect(state.get()).toMatchObject({authState: {type: AuthStateType.ERROR, error}})
150
+
151
+ // Simulate comlink providing a fresh token (setAuthToken sets LOGGED_IN with new token)
152
+ state.set('setNewToken', {
153
+ authState: {type: AuthStateType.LOGGED_IN, token: 'fresh-token', currentUser: null},
154
+ })
155
+
156
+ // Subscription should still be alive — re-fetches /users/me with the new token
157
+ expect(state.get()).toMatchObject({
158
+ authState: {type: AuthStateType.LOGGED_IN, token: 'fresh-token', currentUser: mockUser},
159
+ })
160
+ expect(mockRequest).toHaveBeenCalledTimes(2)
161
+
162
+ subscription.unsubscribe()
163
+ })
164
+
165
+ it('recovers from multiple consecutive fetch errors', () => {
166
+ const error1 = new Error('Unauthorized')
167
+ const error2 = new Error('Unauthorized again')
168
+ const mockUser = {id: 'finally-recovered'} as CurrentUser
169
+ const mockRequest = vi
170
+ .fn()
171
+ .mockReturnValueOnce(throwError(() => error1))
172
+ .mockReturnValueOnce(throwError(() => error2))
173
+ .mockReturnValueOnce(of(mockUser))
174
+ const mockClient = {observable: {request: mockRequest}}
175
+ const clientFactory = vi.fn().mockReturnValue(mockClient)
176
+ const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
177
+
178
+ const state = createStoreState(authStore.getInitialState(instance, null))
179
+ const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
180
+
181
+ // First attempt fails
182
+ state.set('setLoggedIn', {
183
+ authState: {type: AuthStateType.LOGGED_IN, token: 'token-1', currentUser: null},
184
+ })
185
+ expect(state.get()).toMatchObject({authState: {type: AuthStateType.ERROR, error: error1}})
186
+
187
+ // Second attempt also fails
188
+ state.set('setNewToken', {
189
+ authState: {type: AuthStateType.LOGGED_IN, token: 'token-2', currentUser: null},
190
+ })
191
+ expect(state.get()).toMatchObject({authState: {type: AuthStateType.ERROR, error: error2}})
192
+
193
+ // Third attempt succeeds
194
+ state.set('setNewToken', {
195
+ authState: {type: AuthStateType.LOGGED_IN, token: 'token-3', currentUser: null},
196
+ })
197
+ expect(state.get()).toMatchObject({
198
+ authState: {type: AuthStateType.LOGGED_IN, token: 'token-3', currentUser: mockUser},
199
+ })
200
+ expect(mockRequest).toHaveBeenCalledTimes(3)
201
+
202
+ subscription.unsubscribe()
203
+ })
204
+
205
+ it('does not re-fetch with the same token but recovers with a different token', () => {
206
+ const error = new Error('Unauthorized')
207
+ const mockUser = {id: 'recovered-user'} as CurrentUser
208
+ const mockRequest = vi
209
+ .fn()
210
+ .mockReturnValueOnce(throwError(() => error))
211
+ .mockReturnValueOnce(of(mockUser))
212
+ const mockClient = {observable: {request: mockRequest}}
213
+ const clientFactory = vi.fn().mockReturnValue(mockClient)
214
+ const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
215
+
216
+ const state = createStoreState(authStore.getInitialState(instance, null))
217
+ const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
218
+
219
+ // First attempt fails
220
+ state.set('setLoggedIn', {
221
+ authState: {type: AuthStateType.LOGGED_IN, token: 'same-token', currentUser: null},
222
+ })
223
+ expect(state.get()).toMatchObject({authState: {type: AuthStateType.ERROR, error}})
224
+
225
+ // Same token should be blocked by distinctUntilChanged — no re-fetch
226
+ state.set('setNewToken', {
227
+ authState: {type: AuthStateType.LOGGED_IN, token: 'same-token', currentUser: null},
228
+ })
229
+ expect(mockRequest).toHaveBeenCalledTimes(1)
230
+
231
+ // A different token should pass distinctUntilChanged and trigger recovery
232
+ state.set('setNewToken', {
233
+ authState: {type: AuthStateType.LOGGED_IN, token: 'different-token', currentUser: null},
234
+ })
235
+ expect(state.get()).toMatchObject({
236
+ authState: {
237
+ type: AuthStateType.LOGGED_IN,
238
+ token: 'different-token',
239
+ currentUser: mockUser,
240
+ },
241
+ })
242
+ expect(mockRequest).toHaveBeenCalledTimes(2)
243
+
244
+ subscription.unsubscribe()
245
+ })
113
246
  })
@@ -1,8 +1,17 @@
1
1
  import {type CurrentUser} from '@sanity/types'
2
- import {distinctUntilChanged, filter, map, type Subscription, switchMap} from 'rxjs'
2
+ import {
3
+ catchError,
4
+ distinctUntilChanged,
5
+ EMPTY,
6
+ filter,
7
+ map,
8
+ type Subscription,
9
+ switchMap,
10
+ } from 'rxjs'
3
11
 
4
12
  import {type StoreContext} from '../store/defineStore'
5
13
  import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
14
+ import {getAuthLogger} from './authLogger'
6
15
  import {isStudioConfig} from './authMode'
7
16
  import {AuthStateType} from './authStateType'
8
17
  import {type AuthMethodOptions, type AuthState, type AuthStoreState} from './authStore'
@@ -22,9 +31,11 @@ export const subscribeToStateAndFetchCurrentUser = (
22
31
  {state, instance}: StoreContext<AuthStoreState>,
23
32
  fetchOptions?: {useProjectHostname?: boolean},
24
33
  ): Subscription => {
34
+ const logger = getAuthLogger(instance)
35
+
25
36
  const {clientFactory, apiHost} = state.get().options
26
37
  const useProjectHostname = fetchOptions?.useProjectHostname ?? isStudioConfig(instance.config)
27
- const projectId = instance.config.studio?.projectId
38
+ const projectId = instance.config.projectId
28
39
 
29
40
  const currentUser$ = state.observable
30
41
  .pipe(
@@ -60,12 +71,35 @@ export const subscribeToStateAndFetchCurrentUser = (
60
71
  }),
61
72
  ),
62
73
  switchMap((client) =>
63
- client.observable.request<CurrentUser>({uri: '/users/me', method: 'GET'}),
74
+ client.observable
75
+ .request<CurrentUser>({
76
+ uri: '/users/me',
77
+ method: 'GET',
78
+ tag: 'users.get-current',
79
+ })
80
+ .pipe(
81
+ /**
82
+ * Catch inside switchMap so the outer subscription survives.
83
+ * Without this, a 401 terminates the subscription permanently
84
+ * and subsequent token refreshes via comlink never re-fetch /users/me.
85
+ * @see SDK-1409
86
+ */
87
+ catchError((error) => {
88
+ logger.error('Failed to fetch current user', {error})
89
+ state.set('setError', {authState: {type: AuthStateType.ERROR, error}})
90
+ return EMPTY
91
+ }),
92
+ ),
64
93
  ),
65
94
  )
66
95
 
67
96
  return currentUser$.subscribe({
68
97
  next: (currentUser) => {
98
+ logger.info('Current user fetched successfully', {
99
+ hasEmail: !!currentUser.email,
100
+ })
101
+ // userId is PII, so only surface it at debug level
102
+ logger.debug('Current user details', {userId: currentUser.id})
69
103
  state.set('setCurrentUser', (prev) => ({
70
104
  authState:
71
105
  prev.authState.type === AuthStateType.LOGGED_IN
@@ -73,8 +107,5 @@ export const subscribeToStateAndFetchCurrentUser = (
73
107
  : prev.authState,
74
108
  }))
75
109
  },
76
- error: (error) => {
77
- state.set('setError', {authState: {type: AuthStateType.ERROR, error}})
78
- },
79
110
  })
80
111
  }
@@ -23,6 +23,8 @@ describe('subscribeToStorageEventsAndSetToken', () => {
23
23
  vi.clearAllMocks()
24
24
 
25
25
  instance = createSanityInstance({
26
+ projectId: 'p',
27
+ dataset: 'd',
26
28
  auth: {storageArea: storageArea},
27
29
  })
28
30