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

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 (197) hide show
  1. package/dist/_chunks-dts/createGroqSearchFilter.d.ts +1147 -0
  2. package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -0
  3. package/dist/_chunks-es/_internal.js +134 -0
  4. package/dist/_chunks-es/_internal.js.map +1 -0
  5. package/dist/_chunks-es/createGroqSearchFilter.js +1864 -0
  6. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
  7. package/dist/_chunks-es/telemetryManager.js +93 -0
  8. package/dist/_chunks-es/telemetryManager.js.map +1 -0
  9. package/dist/_exports/_internal.d.ts +95 -0
  10. package/dist/_exports/_internal.d.ts.map +1 -0
  11. package/dist/_exports/_internal.js +24 -0
  12. package/dist/_exports/_internal.js.map +1 -0
  13. package/dist/index.d.ts +1079 -1076
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +1967 -2302
  16. package/dist/index.js.map +1 -1
  17. package/package.json +48 -47
  18. package/src/_exports/_internal.ts +26 -0
  19. package/src/_exports/index.ts +120 -11
  20. package/src/access/checkPermissions.test.ts +55 -0
  21. package/src/access/checkPermissions.ts +54 -0
  22. package/src/agent/agentActions.test.ts +16 -60
  23. package/src/agent/agentActions.ts +25 -38
  24. package/src/applications/applications.test-d.ts +83 -0
  25. package/src/applications/applications.test.ts +130 -0
  26. package/src/applications/applications.ts +364 -0
  27. package/src/applications/userApplications.test.ts +126 -0
  28. package/src/applications/userApplications.ts +261 -0
  29. package/src/auth/authLogger.ts +30 -0
  30. package/src/auth/authMode.test.ts +25 -0
  31. package/src/auth/authMode.ts +6 -3
  32. package/src/auth/authStore.test.ts +194 -13
  33. package/src/auth/authStore.ts +59 -26
  34. package/src/auth/getOrganizationVerificationState.test.ts +18 -17
  35. package/src/auth/getOrganizationVerificationState.ts +4 -4
  36. package/src/auth/handleAuthCallback.test.ts +35 -1
  37. package/src/auth/handleAuthCallback.ts +25 -6
  38. package/src/auth/logout.test.ts +75 -2
  39. package/src/auth/logout.ts +23 -4
  40. package/src/auth/refreshStampedToken.test.ts +219 -469
  41. package/src/auth/refreshStampedToken.ts +79 -195
  42. package/src/auth/studioAuth.ts +1 -1
  43. package/src/auth/studioModeAuth.ts +2 -1
  44. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
  45. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
  46. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
  47. package/src/client/clientStore.test.ts +62 -37
  48. package/src/client/clientStore.ts +45 -75
  49. package/src/client/liveEvents.test.ts +151 -0
  50. package/src/client/liveEvents.ts +107 -0
  51. package/src/comlink/controller/actions/destroyController.test.ts +4 -1
  52. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
  53. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  54. package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
  55. package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
  56. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
  57. package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
  58. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  59. package/src/comlink/node/actions/releaseNode.test.ts +7 -4
  60. package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
  61. package/src/comlink/node/getNodeState.test.ts +1 -1
  62. package/src/comlink/node/getNodeState.ts +2 -1
  63. package/src/config/__tests__/handles.test.ts +18 -12
  64. package/src/config/handles.ts +25 -7
  65. package/src/config/sanityConfig.ts +109 -79
  66. package/src/datasets/datasets.test.ts +2 -2
  67. package/src/datasets/datasets.ts +10 -4
  68. package/src/document/actions.test.ts +115 -33
  69. package/src/document/actions.ts +212 -15
  70. package/src/document/applyDocumentActions.test.ts +38 -12
  71. package/src/document/applyDocumentActions.ts +30 -9
  72. package/src/document/documentConstants.ts +7 -0
  73. package/src/document/documentStore.concurrency.test.ts +1306 -0
  74. package/src/document/documentStore.test.ts +673 -278
  75. package/src/document/documentStore.ts +162 -71
  76. package/src/document/events.test.ts +57 -2
  77. package/src/document/events.ts +82 -26
  78. package/src/document/listen.test.ts +56 -0
  79. package/src/document/listen.ts +74 -21
  80. package/src/document/listenerEventOperators.test.ts +311 -0
  81. package/src/document/listenerEventOperators.ts +217 -0
  82. package/src/document/patchOperations.test.ts +49 -2
  83. package/src/document/patchOperations.ts +50 -0
  84. package/src/document/permissions.test.ts +105 -60
  85. package/src/document/permissions.ts +44 -19
  86. package/src/document/processActions/create.ts +138 -0
  87. package/src/document/processActions/delete.ts +100 -0
  88. package/src/document/processActions/discard.ts +63 -0
  89. package/src/document/processActions/edit.ts +154 -0
  90. package/src/document/processActions/processActions.ts +218 -0
  91. package/src/document/processActions/publish.ts +120 -0
  92. package/src/document/processActions/releaseArchive.ts +77 -0
  93. package/src/document/processActions/releaseCreate.ts +59 -0
  94. package/src/document/processActions/releaseDelete.ts +65 -0
  95. package/src/document/processActions/releaseEdit.ts +37 -0
  96. package/src/document/processActions/releasePublish.ts +45 -0
  97. package/src/document/processActions/releaseSchedule.ts +87 -0
  98. package/src/document/processActions/releaseUtil.ts +31 -0
  99. package/src/document/processActions/shared.ts +308 -0
  100. package/src/document/processActions/unpublish.ts +85 -0
  101. package/src/document/processActions.test.ts +1129 -121
  102. package/src/document/reducers.test.ts +639 -64
  103. package/src/document/reducers.ts +187 -48
  104. package/src/document/resourceRules.test.ts +178 -0
  105. package/src/document/resourceRules.ts +117 -0
  106. package/src/document/sharedListener.test.ts +16 -14
  107. package/src/document/sharedListener.ts +24 -8
  108. package/src/document/util.ts +14 -0
  109. package/src/favorites/favorites.test.ts +5 -6
  110. package/src/installations/installations.test-d.ts +68 -0
  111. package/src/installations/installations.test.ts +62 -0
  112. package/src/installations/installations.ts +202 -0
  113. package/src/organization/organization.test-d.ts +36 -0
  114. package/src/organization/organization.test.ts +138 -0
  115. package/src/organization/organization.ts +138 -0
  116. package/src/organizations/organizations.test-d.ts +56 -0
  117. package/src/organizations/organizations.test.ts +150 -0
  118. package/src/organizations/organizations.ts +107 -0
  119. package/src/presence/bifurTransport.test.ts +46 -6
  120. package/src/presence/bifurTransport.ts +19 -2
  121. package/src/presence/presenceStore.test.ts +94 -23
  122. package/src/presence/presenceStore.ts +85 -29
  123. package/src/preview/getPreviewState.test.ts +137 -0
  124. package/src/preview/getPreviewState.ts +69 -0
  125. package/src/preview/previewProjectionUtils.test.ts +9 -22
  126. package/src/preview/previewProjectionUtils.ts +7 -3
  127. package/src/preview/resolvePreview.test.ts +64 -0
  128. package/src/preview/resolvePreview.ts +43 -0
  129. package/src/preview/types.ts +10 -0
  130. package/src/project/project.test-d.ts +37 -0
  131. package/src/project/project.test.ts +157 -12
  132. package/src/project/project.ts +138 -25
  133. package/src/projection/getProjectionState.test.ts +18 -20
  134. package/src/projection/getProjectionState.ts +23 -3
  135. package/src/projection/projectionQuery.ts +5 -5
  136. package/src/projection/projectionStore.test.ts +1 -1
  137. package/src/projection/resolveProjection.test.ts +1 -2
  138. package/src/projection/resolveProjection.ts +16 -0
  139. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  140. package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
  141. package/src/projection/types.ts +8 -0
  142. package/src/projects/projects.test-d.ts +32 -0
  143. package/src/projects/projects.test.ts +108 -42
  144. package/src/projects/projects.ts +63 -27
  145. package/src/query/queryStore.test.ts +259 -123
  146. package/src/query/queryStore.ts +78 -63
  147. package/src/query/reducers.test.ts +0 -42
  148. package/src/query/reducers.ts +2 -18
  149. package/src/releases/getPerspectiveState.test.ts +17 -23
  150. package/src/releases/getPerspectiveState.ts +8 -20
  151. package/src/releases/observeReleases.test.ts +127 -0
  152. package/src/releases/observeReleases.ts +102 -0
  153. package/src/releases/releasesStore.test.ts +100 -53
  154. package/src/releases/releasesStore.ts +69 -44
  155. package/src/releases/utils/sortReleases.test.ts +38 -2
  156. package/src/releases/utils/sortReleases.ts +15 -10
  157. package/src/store/createActionBinder.test.ts +87 -90
  158. package/src/store/createActionBinder.ts +79 -25
  159. package/src/store/createSanityInstance.test.ts +83 -13
  160. package/src/store/createSanityInstance.ts +77 -2
  161. package/src/store/createStateSourceAction.test.ts +64 -2
  162. package/src/store/createStateSourceAction.ts +39 -44
  163. package/src/store/createStoreInstance.test.ts +4 -2
  164. package/src/store/fetcherStore.test-d.ts +121 -0
  165. package/src/store/fetcherStore.test.ts +299 -0
  166. package/src/store/fetcherStore.ts +801 -0
  167. package/src/telemetry/environment.test.ts +119 -0
  168. package/src/telemetry/environment.ts +92 -0
  169. package/src/telemetry/events.ts +42 -0
  170. package/src/telemetry/initTelemetry.test.ts +432 -0
  171. package/src/telemetry/initTelemetry.ts +218 -0
  172. package/src/telemetry/telemetryManager.test.ts +327 -0
  173. package/src/telemetry/telemetryManager.ts +199 -0
  174. package/src/users/reducers.test.ts +6 -1
  175. package/src/users/reducers.ts +5 -6
  176. package/src/users/types.ts +4 -4
  177. package/src/users/usersStore.test.ts +16 -12
  178. package/src/users/usersStore.ts +5 -1
  179. package/src/utils/buildQuery.test.ts +14 -0
  180. package/src/utils/buildQuery.ts +17 -0
  181. package/src/utils/createFetcherStore.test.ts +7 -5
  182. package/src/utils/createFetcherStore.ts +8 -5
  183. package/src/utils/getStagingApiHost.test.ts +21 -0
  184. package/src/utils/getStagingApiHost.ts +14 -0
  185. package/src/utils/ids.test.ts +1 -29
  186. package/src/utils/ids.ts +0 -10
  187. package/src/utils/includeShape.ts +36 -0
  188. package/src/utils/isImportError.test.ts +72 -0
  189. package/src/utils/isImportError.ts +34 -0
  190. package/src/utils/logger.test.ts +12 -0
  191. package/src/utils/logger.ts +8 -3
  192. package/src/utils/object.test.ts +95 -0
  193. package/src/utils/object.ts +142 -0
  194. package/src/utils/setCleanupTimeout.ts +37 -0
  195. package/src/document/processActions.ts +0 -707
  196. package/src/utils/defineIntent.test.ts +0 -477
  197. package/src/utils/defineIntent.ts +0 -244
@@ -1,4 +1,4 @@
1
- import {type Observable} from 'rxjs'
1
+ import {map, type Observable} from 'rxjs'
2
2
  import {TestScheduler} from 'rxjs/testing'
3
3
  import {beforeEach, describe, expect, it, vi} from 'vitest'
4
4
 
@@ -6,7 +6,7 @@ import {
6
6
  compareProjectOrganization,
7
7
  type OrgVerificationResult,
8
8
  } from '../project/organizationVerification'
9
- import {getProjectState} from '../project/project'
9
+ import {project} from '../project/project'
10
10
  import {type SanityInstance} from '../store/createSanityInstance'
11
11
  import {type StateSource} from '../store/createStateSourceAction'
12
12
  import {getDashboardOrganizationId} from './dashboardUtils'
@@ -17,7 +17,7 @@ vi.mock('./dashboardUtils', () => ({
17
17
  getDashboardOrganizationId: vi.fn(),
18
18
  }))
19
19
  vi.mock('../project/project', () => ({
20
- getProjectState: vi.fn(),
20
+ project: {getState: vi.fn()},
21
21
  }))
22
22
  // Mock the comparison function to check its inputs
23
23
  vi.mock('../project/organizationVerification', async (importOriginal) => {
@@ -32,10 +32,10 @@ vi.mock('../project/organizationVerification', async (importOriginal) => {
32
32
  describe('observeOrganizationVerificationState', () => {
33
33
  let testScheduler: TestScheduler
34
34
 
35
- // Mock instance with studio.projectId
35
+ // Mock instance (only config.projectId is used)
36
36
  const mockInstance = {
37
- config: {studio: {projectId: 'proj-1'}},
38
- } as unknown as SanityInstance
37
+ config: {projectId: 'proj-1', dataset: 'd'},
38
+ } as SanityInstance
39
39
 
40
40
  beforeEach(() => {
41
41
  testScheduler = new TestScheduler((actual, expected) => {
@@ -54,11 +54,11 @@ describe('observeOrganizationVerificationState', () => {
54
54
  } as any) // Cast to any to bypass strict type checking in mock
55
55
  }
56
56
 
57
- // Helper to mock getProjectState
57
+ // Helper to mock the project fetcher's state; wraps plain values in the snapshot envelope
58
58
  const mockProjectOrgId = (observable: Observable<{organizationId: string | null} | null>) => {
59
- vi.mocked(getProjectState).mockReturnValue({
59
+ vi.mocked(project.getState).mockReturnValue({
60
60
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
- observable: observable as any, // Cast needed due to complex type
61
+ observable: observable.pipe(map((value) => ({data: value ?? undefined}))) as any,
62
62
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
63
  } as StateSource<any>)
64
64
  }
@@ -80,17 +80,18 @@ describe('observeOrganizationVerificationState', () => {
80
80
  const expectedValues = {a: {error: null}}
81
81
 
82
82
  const result$ = observeOrganizationVerificationState(mockInstance, [
83
- mockInstance.config.studio!.projectId!,
83
+ mockInstance.config.projectId!,
84
84
  ])
85
85
  expectObservable(result$).toBe(expectedMarble, expectedValues)
86
86
  })
87
87
  expect(compareProjectOrganization).not.toHaveBeenCalled()
88
88
  })
89
89
 
90
- it('should emit {error: null} if instance has no default source projectId', () => {
90
+ it('should emit {error: null} if instance.config.projectId is missing', () => {
91
91
  const instanceWithoutProjectId = {
92
- config: {auth: {}},
93
- } as unknown as SanityInstance
92
+ config: {projectId: undefined, dataset: 'd'},
93
+ // Add other required properties if SanityInstance type needs them
94
+ } as SanityInstance
94
95
 
95
96
  testScheduler.run(({hot, expectObservable}) => {
96
97
  // Dashboard org ID doesn't matter much here, but provide one
@@ -109,7 +110,7 @@ describe('observeOrganizationVerificationState', () => {
109
110
  expectObservable(result$).toBe(expectedMarble, expectedValues)
110
111
  })
111
112
  // No project fetch or comparison should occur
112
- expect(getProjectState).not.toHaveBeenCalled()
113
+ expect(project.getState).not.toHaveBeenCalled()
113
114
  expect(compareProjectOrganization).not.toHaveBeenCalled()
114
115
  })
115
116
 
@@ -138,7 +139,7 @@ describe('observeOrganizationVerificationState', () => {
138
139
  const expectedValues = {r: {error: null}} // Expect null error
139
140
 
140
141
  const result$ = observeOrganizationVerificationState(mockInstance, [
141
- mockInstance.config.studio!.projectId!,
142
+ mockInstance.config.projectId!,
142
143
  ])
143
144
  expectObservable(result$).toBe(expectedMarble, expectedValues)
144
145
  })
@@ -162,7 +163,7 @@ describe('observeOrganizationVerificationState', () => {
162
163
  const expectedValues = {r: comparisonResult}
163
164
 
164
165
  const result$ = observeOrganizationVerificationState(mockInstance, [
165
- mockInstance.config.studio!.projectId!,
166
+ mockInstance.config.projectId!,
166
167
  ])
167
168
  expectObservable(result$).toBe(expectedMarble, expectedValues)
168
169
  })
@@ -186,7 +187,7 @@ describe('observeOrganizationVerificationState', () => {
186
187
  const expectedValues = {r: comparisonResult}
187
188
 
188
189
  const result$ = observeOrganizationVerificationState(mockInstance, [
189
- mockInstance.config.studio!.projectId!,
190
+ mockInstance.config.projectId!,
190
191
  ])
191
192
  expectObservable(result$).toBe(expectedMarble, expectedValues)
192
193
  })
@@ -4,14 +4,14 @@ import {
4
4
  compareProjectOrganization,
5
5
  type OrgVerificationResult,
6
6
  } from '../project/organizationVerification'
7
- import {getProjectState} from '../project/project'
7
+ import {project} from '../project/project'
8
8
  import {type SanityInstance} from '../store/createSanityInstance'
9
9
  import {getDashboardOrganizationId} from './dashboardUtils'
10
10
 
11
11
  /**
12
12
  * Creates an observable that emits the organization verification state for a given instance.
13
13
  * It combines the dashboard organization ID (from auth context) with the
14
- * project's actual organization ID (fetched via getProjectState) and compares them.
14
+ * project's actual organization ID (fetched via the project fetcher) and compares them.
15
15
  * @public
16
16
  */
17
17
  export function observeOrganizationVerificationState(
@@ -24,8 +24,8 @@ export function observeOrganizationVerificationState(
24
24
 
25
25
  // Create observables for each project's org ID
26
26
  const projectOrgIdObservables = projectIds.map((id) =>
27
- getProjectState(instance, {projectId: id}).observable.pipe(
28
- map((project) => ({projectId: id, orgId: project?.organizationId ?? null})),
27
+ project.getState(instance, {projectId: id}).observable.pipe(
28
+ map((snapshot) => ({projectId: id, orgId: snapshot.data?.organizationId ?? null})),
29
29
  // Ensure we only proceed if the orgId is loaded, distinct prevents unnecessary checks
30
30
  distinctUntilChanged((prev, curr) => prev.orgId === curr.orgId),
31
31
  ),
@@ -1,5 +1,5 @@
1
1
  import {NEVER} from 'rxjs'
2
- import {beforeEach, describe, it} from 'vitest'
2
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
3
3
 
4
4
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
5
5
  import {AuthStateType} from './authStateType'
@@ -22,6 +22,25 @@ vi.mock('./utils', async (importOriginal) => {
22
22
  vi.mock('./subscribeToStateAndFetchCurrentUser')
23
23
  vi.mock('./subscribeToStorageEventsAndSetToken')
24
24
 
25
+ // Mock logger to prevent actual logging during tests
26
+ vi.mock('../utils/logger', async (importOriginal) => {
27
+ const original = await importOriginal<typeof import('../utils/logger')>()
28
+ return {
29
+ ...original,
30
+ createLogger: vi.fn(() => ({
31
+ info: vi.fn(),
32
+ debug: vi.fn(),
33
+ warn: vi.fn(),
34
+ error: vi.fn(),
35
+ trace: vi.fn(),
36
+ })),
37
+ }
38
+ })
39
+
40
+ // Import createLogger after mocking
41
+ // eslint-disable-next-line import-x/first
42
+ import {createLogger} from '../utils/logger'
43
+
25
44
  let instance: SanityInstance | undefined
26
45
 
27
46
  describe('handleCallback', () => {
@@ -44,6 +63,8 @@ describe('handleCallback', () => {
44
63
  vi.mocked(getAuthCode).mockReturnValue(authCode)
45
64
 
46
65
  instance = createSanityInstance({
66
+ projectId: 'p',
67
+ dataset: 'd',
47
68
  auth: {
48
69
  clientFactory,
49
70
  storageArea: {setItem} as unknown as Storage,
@@ -87,6 +108,8 @@ describe('handleCallback', () => {
87
108
  vi.mocked(getAuthCode).mockReturnValue(authCode)
88
109
 
89
110
  instance = createSanityInstance({
111
+ projectId: 'p',
112
+ dataset: 'd',
90
113
  auth: {
91
114
  clientFactory,
92
115
  storageArea: {setItem} as unknown as Storage,
@@ -115,6 +138,8 @@ describe('handleCallback', () => {
115
138
  const setItem = vi.fn()
116
139
 
117
140
  instance = createSanityInstance({
141
+ projectId: 'p',
142
+ dataset: 'd',
118
143
  auth: {
119
144
  clientFactory,
120
145
  storageArea: {setItem: setItem as Storage['setItem']} as Storage,
@@ -155,6 +180,8 @@ describe('handleCallback', () => {
155
180
  const setItem = vi.fn()
156
181
 
157
182
  instance = createSanityInstance({
183
+ projectId: 'p',
184
+ dataset: 'd',
158
185
  auth: {
159
186
  clientFactory,
160
187
  storageArea: {setItem} as unknown as Storage,
@@ -182,6 +209,8 @@ describe('handleCallback', () => {
182
209
  vi.mocked(getAuthCode).mockReturnValue(authCode)
183
210
 
184
211
  instance = createSanityInstance({
212
+ projectId: 'p',
213
+ dataset: 'd',
185
214
  auth: {
186
215
  clientFactory,
187
216
  storageArea: {setItem} as unknown as Storage,
@@ -219,6 +248,8 @@ describe('handleCallback', () => {
219
248
  vi.mocked(getTokenFromLocation).mockReturnValue('hash-token')
220
249
 
221
250
  instance = createSanityInstance({
251
+ projectId: 'p',
252
+ dataset: 'd',
222
253
  auth: {
223
254
  clientFactory,
224
255
  storageArea: {setItem} as unknown as Storage,
@@ -242,5 +273,8 @@ describe('handleCallback', () => {
242
273
  })
243
274
  expect(clientFactory).not.toHaveBeenCalled()
244
275
  expect(setItem).not.toHaveBeenCalled()
276
+
277
+ // Verify logger was called
278
+ expect(createLogger).toHaveBeenCalled()
245
279
  })
246
280
  })
@@ -1,5 +1,6 @@
1
1
  import {bindActionGlobally} from '../store/createActionBinder'
2
2
  import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
3
+ import {getAuthLogger} from './authLogger'
3
4
  import {AuthStateType} from './authStateType'
4
5
  import {authStore, type AuthStoreState, type DashboardContext} from './authStore'
5
6
  import {
@@ -15,16 +16,24 @@ import {
15
16
  */
16
17
  export const handleAuthCallback = bindActionGlobally(
17
18
  authStore,
18
- async ({state}, locationHref: string = getDefaultLocation()) => {
19
+ async ({state, instance}, locationHref: string = getDefaultLocation()) => {
20
+ const logger = getAuthLogger(instance)
21
+
19
22
  const {providedToken, callbackUrl, clientFactory, apiHost, storageArea, storageKey} =
20
23
  state.get().options
21
24
 
22
25
  // If a token is provided, no need to handle callback
23
- if (providedToken) return false
26
+ if (providedToken) {
27
+ logger.debug('Skipping auth callback - token already provided')
28
+ return false
29
+ }
24
30
 
25
31
  // Don't handle the callback if already in flight.
26
32
  const {authState} = state.get()
27
- if (authState.type === AuthStateType.LOGGING_IN && authState.isExchangingToken) return false
33
+ if (authState.type === AuthStateType.LOGGING_IN && authState.isExchangingToken) {
34
+ logger.debug('Skipping auth callback - token exchange already in progress')
35
+ return false
36
+ }
28
37
 
29
38
  // Prepare the cleaned-up URL early. It will be returned on both success and error if an authCode/token was processed.
30
39
  const cleanedUrl = getCleanedUrl(locationHref)
@@ -32,6 +41,7 @@ export const handleAuthCallback = bindActionGlobally(
32
41
  // Check if there is a token in the is in the Dashboard iframe url hash
33
42
  const tokenFromUrl = getTokenFromLocation(locationHref)
34
43
  if (tokenFromUrl) {
44
+ logger.info('Auth token found in URL, logging in')
35
45
  state.set('setTokenFromUrl', {
36
46
  authState: createLoggedInAuthState(tokenFromUrl, null),
37
47
  })
@@ -40,7 +50,10 @@ export const handleAuthCallback = bindActionGlobally(
40
50
 
41
51
  // If there is no matching `authCode` then we can't handle the callback
42
52
  const authCode = getAuthCode(callbackUrl, locationHref)
43
- if (!authCode) return false
53
+ if (!authCode) {
54
+ logger.debug('No auth code found in callback URL')
55
+ return false
56
+ }
44
57
 
45
58
  // Get the SanityOS dashboard context from the url
46
59
  const parsedUrl = new URL(locationHref)
@@ -52,15 +65,18 @@ export const handleAuthCallback = bindActionGlobally(
52
65
  if (parsedContext && typeof parsedContext === 'object') {
53
66
  delete parsedContext.sid
54
67
  dashboardContext = parsedContext
68
+ logger.debug('Dashboard context parsed from callback URL', {
69
+ hasDashboardContext: true,
70
+ })
55
71
  }
56
72
  }
57
73
  } catch (err) {
58
74
  // If JSON parsing fails, use empty context
59
- // eslint-disable-next-line no-console
60
- console.error('Failed to parse dashboard context:', err)
75
+ logger.warn('Failed to parse dashboard context from callback URL', {error: err})
61
76
  }
62
77
 
63
78
  // Otherwise, start the exchange
79
+ logger.info('Exchanging auth code for token')
64
80
  state.set('exchangeSessionForToken', {
65
81
  authState: {type: AuthStateType.LOGGING_IN, isExchangingToken: true},
66
82
  dashboardContext,
@@ -75,6 +91,7 @@ export const handleAuthCallback = bindActionGlobally(
75
91
  ...(apiHost && {apiHost}),
76
92
  })
77
93
 
94
+ logger.debug('Fetching token from auth endpoint')
78
95
  const {token} = await client.request<{token: string; label: string}>({
79
96
  method: 'GET',
80
97
  uri: '/auth/fetch',
@@ -82,11 +99,13 @@ export const handleAuthCallback = bindActionGlobally(
82
99
  tag: 'fetch-token',
83
100
  })
84
101
 
102
+ logger.info('Auth token obtained successfully, user logged in')
85
103
  storageArea?.setItem(storageKey, JSON.stringify({token}))
86
104
  state.set('setToken', {authState: createLoggedInAuthState(token, null)})
87
105
 
88
106
  return cleanedUrl
89
107
  } catch (error) {
108
+ logger.error('Failed to exchange auth code for token', {error})
90
109
  state.set('exchangeSessionForTokenError', {authState: {type: AuthStateType.ERROR, error}})
91
110
  return cleanedUrl
92
111
  }
@@ -1,5 +1,5 @@
1
1
  import {NEVER} from 'rxjs'
2
- import {describe, it} from 'vitest'
2
+ import {describe, expect, it, vi} from 'vitest'
3
3
 
4
4
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
5
5
  import {AuthStateType} from './authStateType'
@@ -17,9 +17,25 @@ vi.mock('./utils', async (importOriginal) => {
17
17
  vi.mock('./subscribeToStateAndFetchCurrentUser')
18
18
  vi.mock('./subscribeToStorageEventsAndSetToken')
19
19
 
20
+ // Mock logger to prevent actual logging during tests
21
+ vi.mock('../utils/logger', async (importOriginal) => {
22
+ const original = await importOriginal<typeof import('../utils/logger')>()
23
+ return {
24
+ ...original,
25
+ createLogger: vi.fn(() => ({
26
+ info: vi.fn(),
27
+ debug: vi.fn(),
28
+ warn: vi.fn(),
29
+ error: vi.fn(),
30
+ trace: vi.fn(),
31
+ })),
32
+ }
33
+ })
34
+
20
35
  let instance: SanityInstance | undefined
21
36
 
22
37
  beforeEach(() => {
38
+ vi.clearAllMocks()
23
39
  vi.mocked(subscribeToStateAndFetchCurrentUser).mockImplementation(() => NEVER.subscribe())
24
40
  vi.mocked(subscribeToStorageEventsAndSetToken).mockImplementation(() => NEVER.subscribe())
25
41
  })
@@ -36,6 +52,8 @@ describe('logout', () => {
36
52
  const removeItem = vi.fn() as Storage['removeItem']
37
53
 
38
54
  instance = createSanityInstance({
55
+ projectId: 'p',
56
+ dataset: 'd',
39
57
  auth: {
40
58
  clientFactory,
41
59
  storageArea: {removeItem} as Storage,
@@ -57,7 +75,7 @@ describe('logout', () => {
57
75
  useProjectHostname: false,
58
76
  useCdn: false,
59
77
  })
60
- expect(mockRequest).toHaveBeenCalledWith({method: 'POST', uri: '/auth/logout'})
78
+ expect(mockRequest).toHaveBeenCalledWith({method: 'POST', uri: '/auth/logout', tag: 'logout'})
61
79
  expect(removeItem).toHaveBeenCalledWith('__sanity_auth_token')
62
80
  })
63
81
 
@@ -65,6 +83,8 @@ describe('logout', () => {
65
83
  const clientFactory = vi.fn()
66
84
  const removeItem = vi.fn() as Storage['removeItem']
67
85
  instance = createSanityInstance({
86
+ projectId: 'p',
87
+ dataset: 'd',
68
88
  auth: {
69
89
  token: 'provided-token',
70
90
  clientFactory,
@@ -89,6 +109,8 @@ describe('logout', () => {
89
109
  vi.mocked(getTokenFromStorage).mockReturnValue('token')
90
110
 
91
111
  instance = createSanityInstance({
112
+ projectId: 'p',
113
+ dataset: 'd',
92
114
  auth: {
93
115
  clientFactory,
94
116
  storageArea: {removeItem} as Storage,
@@ -115,4 +137,55 @@ describe('logout', () => {
115
137
  await originalLogout
116
138
  expect(removeItem).toHaveBeenCalledTimes(2)
117
139
  })
140
+
141
+ it('handles logout when already logged out', async () => {
142
+ vi.mocked(getTokenFromStorage).mockReturnValue(null)
143
+ const mockRequest = vi.fn()
144
+ const clientFactory = vi.fn().mockReturnValue({request: mockRequest})
145
+ const removeItem = vi.fn() as Storage['removeItem']
146
+
147
+ instance = createSanityInstance({
148
+ projectId: 'p',
149
+ dataset: 'd',
150
+ auth: {
151
+ clientFactory,
152
+ storageArea: {removeItem} as Storage,
153
+ },
154
+ })
155
+
156
+ const authState = getAuthState(instance)
157
+ expect(authState.getCurrent()).toMatchObject({type: AuthStateType.LOGGED_OUT})
158
+
159
+ await logout(instance)
160
+
161
+ // Should not make API call when already logged out
162
+ expect(clientFactory).not.toHaveBeenCalled()
163
+ expect(mockRequest).not.toHaveBeenCalled()
164
+
165
+ // Should still clean up storage
166
+ expect(removeItem).toHaveBeenCalledWith('__sanity_auth_token')
167
+ })
168
+
169
+ it('cleans up storage even if logout request fails', async () => {
170
+ vi.mocked(getTokenFromStorage).mockReturnValue('token')
171
+ const error = new Error('Logout request failed')
172
+ const mockRequest = vi.fn().mockRejectedValue(error)
173
+ const clientFactory = vi.fn().mockReturnValue({request: mockRequest})
174
+ const removeItem = vi.fn() as Storage['removeItem']
175
+
176
+ instance = createSanityInstance({
177
+ projectId: 'p',
178
+ dataset: 'd',
179
+ auth: {
180
+ clientFactory,
181
+ storageArea: {removeItem} as Storage,
182
+ },
183
+ })
184
+
185
+ // logout rejects when the request fails, matching the pre-logging behavior
186
+ await expect(logout(instance)).rejects.toThrow('Logout request failed')
187
+
188
+ // Should still clean up storage even on error
189
+ expect(removeItem).toHaveBeenCalledWith('__sanity_auth_token')
190
+ })
118
191
  })
@@ -1,25 +1,35 @@
1
1
  import {bindActionGlobally} from '../store/createActionBinder'
2
2
  import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
3
+ import {getAuthLogger} from './authLogger'
3
4
  import {AuthStateType} from './authStateType'
4
5
  import {authStore} from './authStore'
5
6
 
6
7
  /**
7
8
  * @public
8
9
  */
9
- export const logout = bindActionGlobally(authStore, async ({state}) => {
10
+ export const logout = bindActionGlobally(authStore, async ({state, instance}) => {
11
+ const logger = getAuthLogger(instance)
12
+
10
13
  const {clientFactory, apiHost, providedToken, storageArea, storageKey} = state.get().options
11
14
 
12
15
  // If a token is statically provided, logout does nothing
13
- if (providedToken) return
16
+ if (providedToken) {
17
+ logger.debug('Skipping logout - token is statically provided')
18
+ return
19
+ }
14
20
 
15
21
  const {authState} = state.get()
16
22
 
17
23
  // If we already have an inflight request, no-op
18
- if (authState.type === AuthStateType.LOGGED_OUT && authState.isDestroyingSession) return
24
+ if (authState.type === AuthStateType.LOGGED_OUT && authState.isDestroyingSession) {
25
+ logger.debug('Skipping logout - already in progress')
26
+ return
27
+ }
19
28
  const token = authState.type === AuthStateType.LOGGED_IN && authState.token
20
29
 
21
30
  try {
22
31
  if (token) {
32
+ logger.info('Logging out user')
23
33
  state.set('loggingOut', {
24
34
  authState: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: true},
25
35
  })
@@ -33,9 +43,18 @@ export const logout = bindActionGlobally(authStore, async ({state}) => {
33
43
  useCdn: false,
34
44
  })
35
45
 
36
- await client.request<void>({uri: '/auth/logout', method: 'POST'})
46
+ logger.debug('Calling logout endpoint')
47
+ await client.request<void>({uri: '/auth/logout', method: 'POST', tag: 'logout'})
48
+ } else {
49
+ logger.debug('No token to logout - already logged out')
37
50
  }
51
+ } catch (error) {
52
+ // Re-throw to preserve the existing contract: logout rejects when the
53
+ // request fails. Local state is still cleared in the finally block.
54
+ logger.error('Logout request failed', {error})
55
+ throw error
38
56
  } finally {
57
+ logger.info('User logged out, clearing stored tokens')
39
58
  state.set('logoutSuccess', {
40
59
  authState: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
41
60
  })