@sanity/sdk 2.12.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts +925 -0
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +261 -225
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
- package/dist/_chunks-es/version.js +1 -1
- package/dist/_exports/_internal.d.ts +3 -2
- package/dist/_exports/_internal.d.ts.map +1 -0
- package/dist/index.d.ts +1856 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +207 -133
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
- package/src/auth/authLogger.ts +30 -0
- package/src/auth/authStore.test.ts +96 -1
- package/src/auth/authStore.ts +55 -24
- package/src/auth/handleAuthCallback.test.ts +23 -1
- package/src/auth/handleAuthCallback.ts +25 -6
- package/src/auth/logout.test.ts +68 -1
- package/src/auth/logout.ts +22 -3
- package/src/auth/refreshStampedToken.test.ts +15 -0
- package/src/auth/refreshStampedToken.ts +12 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +17 -2
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +9 -0
- package/src/document/applyDocumentActions.test.ts +24 -0
- package/src/document/applyDocumentActions.ts +13 -2
- package/src/document/documentConstants.ts +7 -0
- package/src/document/documentStore.test.ts +69 -0
- package/src/document/documentStore.ts +36 -5
- package/src/document/listen.ts +1 -1
- package/src/document/permissions.test.ts +79 -0
- package/src/document/permissions.ts +8 -7
- package/src/document/processActions/create.ts +7 -4
- package/src/document/processActions/delete.ts +4 -4
- package/src/document/processActions/discard.ts +2 -2
- package/src/document/processActions/edit.ts +4 -3
- package/src/document/processActions/processActions.ts +9 -0
- package/src/document/processActions/publish.ts +4 -4
- package/src/document/processActions/releaseArchive.ts +4 -4
- package/src/document/processActions/releaseCreate.ts +2 -2
- package/src/document/processActions/releaseDelete.ts +2 -2
- package/src/document/processActions/releaseEdit.ts +2 -1
- package/src/document/processActions/releasePublish.ts +2 -2
- package/src/document/processActions/releaseSchedule.ts +4 -4
- package/src/document/processActions/shared.ts +15 -3
- package/src/document/processActions/unpublish.ts +3 -3
- package/src/document/reducers.ts +4 -3
- package/src/document/resourceRules.test.ts +178 -0
- package/src/document/resourceRules.ts +117 -0
- package/dist/_chunks-dts/utils.d.ts +0 -2774
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK",
|
|
6
6
|
"keywords": [
|
|
@@ -57,32 +57,32 @@
|
|
|
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.
|
|
60
|
+
"@sanity/mutate": "^0.18.0",
|
|
61
61
|
"@sanity/telemetry": "^1.1.0",
|
|
62
|
-
"@sanity/types": "^
|
|
62
|
+
"@sanity/types": "^6.0.0",
|
|
63
63
|
"groq": "3.88.1-typegen-experimental.0",
|
|
64
|
-
"groq-js": "^1.30.
|
|
64
|
+
"groq-js": "^1.30.2",
|
|
65
65
|
"reselect": "^5.1.1",
|
|
66
66
|
"rxjs": "^7.8.2",
|
|
67
67
|
"zustand": "^5.0.13"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@sanity/browserslist-config": "^1.0.5",
|
|
71
|
-
"@sanity/pkg-utils": "^
|
|
71
|
+
"@sanity/pkg-utils": "^10.5.3",
|
|
72
72
|
"@sanity/prettier-config": "^1.0.6",
|
|
73
73
|
"@types/node": "^24.12.4",
|
|
74
|
-
"@vitest/coverage-v8": "4.1.
|
|
74
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
75
75
|
"eslint": "^9.39.4",
|
|
76
76
|
"prettier": "^3.8.3",
|
|
77
77
|
"rollup-plugin-visualizer": "^5.14.0",
|
|
78
78
|
"typescript": "^5.9.3",
|
|
79
|
-
"vite": "^7.3.
|
|
80
|
-
"vitest": "^4.1.
|
|
79
|
+
"vite": "^7.3.5",
|
|
80
|
+
"vitest": "^4.1.8",
|
|
81
|
+
"@repo/config-eslint": "0.0.0",
|
|
82
|
+
"@repo/config-test": "0.0.1",
|
|
81
83
|
"@repo/package.bundle": "3.82.0",
|
|
82
84
|
"@repo/package.config": "0.0.1",
|
|
83
|
-
"@repo/tsconfig": "0.0.1"
|
|
84
|
-
"@repo/config-test": "0.0.1",
|
|
85
|
-
"@repo/config-eslint": "0.0.0"
|
|
85
|
+
"@repo/tsconfig": "0.0.1"
|
|
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', () => {
|
package/src/auth/authStore.ts
CHANGED
|
@@ -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
|
|
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(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
}
|
package/src/auth/logout.test.ts
CHANGED
|
@@ -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
|
})
|
package/src/auth/logout.ts
CHANGED
|
@@ -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)
|
|
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)
|
|
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
|
|