@sanity/sdk 2.13.0 → 2.14.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk",
3
- "version": "2.13.0",
3
+ "version": "2.14.1",
4
4
  "private": false,
5
5
  "description": "Sanity SDK",
6
6
  "keywords": [
@@ -57,9 +57,9 @@
57
57
  "@sanity/image-url": "^2.1.1",
58
58
  "@sanity/json-match": "^1.0.5",
59
59
  "@sanity/message-protocol": "^0.23.0",
60
- "@sanity/mutate": "^0.18.0",
60
+ "@sanity/mutate": "^0.18.1",
61
61
  "@sanity/telemetry": "^1.1.0",
62
- "@sanity/types": "^5.26.0",
62
+ "@sanity/types": "^6.0.0",
63
63
  "groq": "3.88.1-typegen-experimental.0",
64
64
  "groq-js": "^1.30.2",
65
65
  "reselect": "^5.1.1",
@@ -68,21 +68,21 @@
68
68
  },
69
69
  "devDependencies": {
70
70
  "@sanity/browserslist-config": "^1.0.5",
71
- "@sanity/pkg-utils": "^9.2.3",
72
- "@sanity/prettier-config": "^1.0.6",
71
+ "@sanity/pkg-utils": "^10.5.5",
72
+ "@sanity/prettier-config": "^3.0.0",
73
73
  "@types/node": "^24.12.4",
74
74
  "@vitest/coverage-v8": "^4.1.8",
75
75
  "eslint": "^9.39.4",
76
- "prettier": "^3.8.3",
77
- "rollup-plugin-visualizer": "^5.14.0",
76
+ "prettier": "^3.8.4",
77
+ "rollup-plugin-visualizer": "^6.0.11",
78
78
  "typescript": "^5.9.3",
79
79
  "vite": "^7.3.5",
80
80
  "vitest": "^4.1.8",
81
+ "@repo/config-eslint": "0.0.0",
81
82
  "@repo/config-test": "0.0.1",
82
83
  "@repo/package.config": "0.0.1",
83
- "@repo/config-eslint": "0.0.0",
84
- "@repo/package.bundle": "3.82.0",
85
- "@repo/tsconfig": "0.0.1"
84
+ "@repo/tsconfig": "0.0.1",
85
+ "@repo/package.bundle": "3.82.0"
86
86
  },
87
87
  "publishConfig": {
88
88
  "access": "public"
@@ -0,0 +1,30 @@
1
+ import {type SanityInstance} from '../store/createSanityInstance'
2
+ import {createLogger, type Logger} from '../utils/logger'
3
+
4
+ const loggers = new WeakMap<SanityInstance, Logger>()
5
+
6
+ /**
7
+ * Returns the auth logger for a Sanity instance, creating it on first use.
8
+ *
9
+ * The logger is cached per instance so repeated action calls (logout,
10
+ * setAuthToken, handleAuthCallback) reuse the same logger instead of
11
+ * rebuilding it. The instance details are nested under `instanceContext`,
12
+ * which is what the logger's formatter reads to render the
13
+ * `[project:x] [dataset:y] [instance:z]` prefix.
14
+ *
15
+ * @internal
16
+ */
17
+ export function getAuthLogger(instance: SanityInstance): Logger {
18
+ let logger = loggers.get(instance)
19
+ if (!logger) {
20
+ logger = createLogger('auth', {
21
+ instanceContext: {
22
+ instanceId: instance.instanceId,
23
+ projectId: instance.config.projectId,
24
+ dataset: instance.config.dataset,
25
+ },
26
+ })
27
+ loggers.set(instance, logger)
28
+ }
29
+ return logger
30
+ }
@@ -1,7 +1,7 @@
1
1
  import {type ClientConfig, createClient, type SanityClient} from '@sanity/client'
2
2
  import {type CurrentUser} from '@sanity/types'
3
3
  import {NEVER, type Subscription} from 'rxjs'
4
- import {afterEach, beforeEach, describe, it, vi} from 'vitest'
4
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
5
5
 
6
6
  import {createSanityInstance} from '../store/createSanityInstance'
7
7
  import {AuthStateType} from './authStateType'
@@ -46,6 +46,24 @@ vi.mock('./studioModeAuth', async (importOriginal) => {
46
46
 
47
47
  vi.mock('./subscribeToStateAndFetchCurrentUser')
48
48
  vi.mock('./subscribeToStorageEventsAndSetToken')
49
+ // Mock logger to prevent actual logging during tests
50
+ vi.mock('../utils/logger', async (importOriginal) => {
51
+ const original = await importOriginal<typeof import('../utils/logger')>()
52
+ return {
53
+ ...original,
54
+ createLogger: vi.fn(() => ({
55
+ info: vi.fn(),
56
+ debug: vi.fn(),
57
+ warn: vi.fn(),
58
+ error: vi.fn(),
59
+ trace: vi.fn(),
60
+ })),
61
+ }
62
+ })
63
+
64
+ // Import createLogger after mocking
65
+ // eslint-disable-next-line import/first
66
+ import {createLogger} from '../utils/logger'
49
67
 
50
68
  describe('authStore', () => {
51
69
  // Global beforeEach and afterEach for all tests
@@ -321,6 +339,54 @@ describe('authStore', () => {
321
339
  expect(options.authMethod).toBe('localstorage')
322
340
  })
323
341
 
342
+ it('logs auth initialized in logged out state when no token available', () => {
343
+ instance = createSanityInstance({
344
+ projectId: 'p',
345
+ dataset: 'd',
346
+ })
347
+
348
+ vi.mocked(getAuthCode).mockReturnValue(null)
349
+ vi.mocked(getTokenFromStorage).mockReturnValue(null)
350
+ vi.mocked(getTokenFromLocation).mockReturnValue(null)
351
+
352
+ const {authState} = authStore.getInitialState(instance, null)
353
+ expect(authState.type).toBe(AuthStateType.LOGGED_OUT)
354
+ // Logger should have been called
355
+ expect(createLogger).toHaveBeenCalled()
356
+ })
357
+
358
+ it('logs when auth initialized with storage token', () => {
359
+ const storageToken = 'storage-token'
360
+ instance = createSanityInstance({
361
+ projectId: 'p',
362
+ dataset: 'd',
363
+ })
364
+
365
+ vi.mocked(getAuthCode).mockReturnValue(null)
366
+ vi.mocked(getTokenFromStorage).mockReturnValue(storageToken)
367
+ vi.mocked(getTokenFromLocation).mockReturnValue(null)
368
+
369
+ const {authState} = authStore.getInitialState(instance, null)
370
+ expect(authState).toMatchObject({type: AuthStateType.LOGGED_IN, token: storageToken})
371
+ // Logger should have been called
372
+ expect(createLogger).toHaveBeenCalled()
373
+ })
374
+
375
+ it('logs when auth initialized with logging in state', () => {
376
+ instance = createSanityInstance({
377
+ projectId: 'p',
378
+ dataset: 'd',
379
+ })
380
+
381
+ vi.mocked(getAuthCode).mockReturnValue('test-code')
382
+ vi.mocked(getTokenFromLocation).mockReturnValue(null)
383
+
384
+ const {authState} = authStore.getInitialState(instance, null)
385
+ expect(authState.type).toBe(AuthStateType.LOGGING_IN)
386
+ // Logger should have been called
387
+ expect(createLogger).toHaveBeenCalled()
388
+ })
389
+
324
390
  it('checks for cookie auth during initialize when studio config is provided and no studio token exists', () => {
325
391
  const projectId = 'studio-project'
326
392
  const studioStorageKey = `__studio_auth_token_${projectId}`
@@ -574,6 +640,35 @@ describe('authStore', () => {
574
640
  expect(stateUnsubscribe).toHaveBeenCalled()
575
641
  expect(storageEventsUnsubscribe).not.toHaveBeenCalled()
576
642
  })
643
+
644
+ it('logs when checking for cookie auth in studio mode', async () => {
645
+ const projectId = 'studio-project'
646
+ const mockStorage = {
647
+ getItem: vi.fn(),
648
+ setItem: vi.fn(),
649
+ removeItem: vi.fn(),
650
+ } as unknown as Storage
651
+ vi.mocked(getStudioTokenFromLocalStorage).mockReturnValue(null)
652
+ vi.mocked(checkForCookieAuth).mockResolvedValue(true)
653
+
654
+ instance = createSanityInstance({
655
+ projectId,
656
+ dataset: 'd',
657
+ studioMode: {enabled: true},
658
+ auth: {storageArea: mockStorage},
659
+ })
660
+
661
+ // Trigger store initialization
662
+ getAuthState(instance)
663
+
664
+ // Wait for async cookie check
665
+ await vi.waitFor(() => {
666
+ expect(checkForCookieAuth).toHaveBeenCalledWith(projectId, expect.any(Function))
667
+ })
668
+
669
+ // Logger should have been called during initialization
670
+ expect(createLogger).toHaveBeenCalled()
671
+ })
577
672
  })
578
673
 
579
674
  describe('getCurrentUserState', () => {
@@ -6,6 +6,7 @@ import {bindActionGlobally} from '../store/createActionBinder'
6
6
  import {createStateSourceAction} from '../store/createStateSourceAction'
7
7
  import {defineStore} from '../store/defineStore'
8
8
  import {getStagingApiHost} from '../utils/getStagingApiHost'
9
+ import {getAuthLogger} from './authLogger'
9
10
  import {resolveAuthMode} from './authMode'
10
11
  import {AuthStateType} from './authStateType'
11
12
  import {type AuthStrategyOptions} from './authStrategy'
@@ -102,6 +103,16 @@ export const authStore = defineStore<AuthStoreState>({
102
103
  name: 'Auth',
103
104
 
104
105
  getInitialState(instance) {
106
+ const logger = getAuthLogger(instance)
107
+
108
+ logger.debug('Initializing auth store', {
109
+ hasProvidedToken: !!instance.config.auth?.token,
110
+ hasCustomProviders: !!(
111
+ instance.config.auth?.providers && instance.config.auth.providers.length > 0
112
+ ),
113
+ studioMode: instance.config.studioMode?.enabled ?? false,
114
+ })
115
+
105
116
  const {
106
117
  apiHost: configApiHost,
107
118
  callbackUrl,
@@ -153,6 +164,12 @@ export const authStore = defineStore<AuthStoreState>({
153
164
  break
154
165
  }
155
166
 
167
+ logger.debug('Auth state initialized', {
168
+ authStateType: result.authState.type,
169
+ mode,
170
+ authMethod: result.authMethod,
171
+ })
172
+
156
173
  return {
157
174
  authState: result.authState,
158
175
  dashboardContext: result.dashboardContext,
@@ -172,10 +189,14 @@ export const authStore = defineStore<AuthStoreState>({
172
189
  },
173
190
 
174
191
  initialize(context) {
192
+ const logger = getAuthLogger(context.instance)
193
+
175
194
  const initialLocationHref =
176
195
  context.state.get().options?.initialLocationHref ?? getDefaultLocation()
177
196
  const mode = resolveAuthMode(context.instance.config, initialLocationHref)
178
197
 
198
+ logger.debug('Setting up auth subscriptions', {mode})
199
+
179
200
  let initResult
180
201
  switch (mode) {
181
202
  case 'studio':
@@ -193,7 +214,10 @@ export const authStore = defineStore<AuthStoreState>({
193
214
  tokenRefresherRunning = true
194
215
  }
195
216
 
196
- return initResult.dispose
217
+ return () => {
218
+ logger.debug('Cleaning up auth subscriptions')
219
+ initResult.dispose()
220
+ }
197
221
  },
198
222
  })
199
223
 
@@ -271,27 +295,34 @@ export const getIsInDashboardState = bindActionGlobally(
271
295
  * Used internally by the Comlink token refresh.
272
296
  * @internal
273
297
  */
274
- export const setAuthToken = bindActionGlobally(authStore, ({state}, token: string | null) => {
275
- const currentAuthState = state.get().authState
276
- if (token) {
277
- // Update state only if the new token is different or currently logged out
278
- if (currentAuthState.type !== AuthStateType.LOGGED_IN || currentAuthState.token !== token) {
279
- const currentUser =
280
- currentAuthState.type === AuthStateType.LOGGED_IN ? currentAuthState.currentUser : null
281
- const preservedLastTokenRefresh =
282
- currentAuthState.type === AuthStateType.LOGGED_IN
283
- ? currentAuthState.lastTokenRefresh
284
- : undefined
285
- state.set('setToken', {
286
- authState: createLoggedInAuthState(token, currentUser, preservedLastTokenRefresh),
287
- })
288
- }
289
- } else {
290
- // Handle setting token to null (logging out)
291
- if (currentAuthState.type !== AuthStateType.LOGGED_OUT) {
292
- state.set('setToken', {
293
- authState: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
294
- })
298
+ export const setAuthToken = bindActionGlobally(
299
+ authStore,
300
+ ({state, instance}, token: string | null) => {
301
+ const logger = getAuthLogger(instance)
302
+
303
+ const currentAuthState = state.get().authState
304
+ if (token) {
305
+ // Update state only if the new token is different or currently logged out
306
+ if (currentAuthState.type !== AuthStateType.LOGGED_IN || currentAuthState.token !== token) {
307
+ logger.info('Setting auth token')
308
+ const currentUser =
309
+ currentAuthState.type === AuthStateType.LOGGED_IN ? currentAuthState.currentUser : null
310
+ const preservedLastTokenRefresh =
311
+ currentAuthState.type === AuthStateType.LOGGED_IN
312
+ ? currentAuthState.lastTokenRefresh
313
+ : undefined
314
+ state.set('setToken', {
315
+ authState: createLoggedInAuthState(token, currentUser, preservedLastTokenRefresh),
316
+ })
317
+ }
318
+ } else {
319
+ // Handle setting token to null (logging out)
320
+ if (currentAuthState.type !== AuthStateType.LOGGED_OUT) {
321
+ logger.info('Clearing auth token')
322
+ state.set('setToken', {
323
+ authState: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
324
+ })
325
+ }
295
326
  }
296
- }
297
- })
327
+ },
328
+ )
@@ -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/first
42
+ import {createLogger} from '../utils/logger'
43
+
25
44
  let instance: SanityInstance | undefined
26
45
 
27
46
  describe('handleCallback', () => {
@@ -254,5 +273,8 @@ describe('handleCallback', () => {
254
273
  })
255
274
  expect(clientFactory).not.toHaveBeenCalled()
256
275
  expect(setItem).not.toHaveBeenCalled()
276
+
277
+ // Verify logger was called
278
+ expect(createLogger).toHaveBeenCalled()
257
279
  })
258
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
  })
@@ -121,4 +137,55 @@ describe('logout', () => {
121
137
  await originalLogout
122
138
  expect(removeItem).toHaveBeenCalledTimes(2)
123
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
+ })
124
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
 
46
+ logger.debug('Calling logout endpoint')
36
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
  })
@@ -15,6 +15,21 @@ import {
15
15
  } from './refreshStampedToken'
16
16
  import {createLoggedInAuthState} from './utils'
17
17
 
18
+ // Mock logger to prevent actual logging during tests
19
+ vi.mock('../utils/logger', async (importOriginal) => {
20
+ const original = await importOriginal<typeof import('../utils/logger')>()
21
+ return {
22
+ ...original,
23
+ createLogger: vi.fn(() => ({
24
+ info: vi.fn(),
25
+ debug: vi.fn(),
26
+ warn: vi.fn(),
27
+ error: vi.fn(),
28
+ trace: vi.fn(),
29
+ })),
30
+ }
31
+ })
32
+
18
33
  // Type definitions for Web Locks (can be kept if needed for context)
19
34
  // ... (Lock, LockOptions, LockGrantedCallback types)
20
35