@sanity/sdk 2.8.0 → 2.9.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/utils.d.ts +2396 -0
- package/dist/_chunks-es/_internal.js +129 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1460 -0
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
- package/dist/_chunks-es/telemetryManager.js +87 -0
- package/dist/_chunks-es/telemetryManager.js.map +1 -0
- package/dist/_chunks-es/version.js +7 -0
- package/dist/_chunks-es/version.js.map +1 -0
- package/dist/_exports/_internal.d.ts +64 -0
- package/dist/_exports/_internal.js +20 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/index.d.ts +2 -2343
- package/dist/index.js +383 -1777
- package/dist/index.js.map +1 -1
- package/package.json +11 -4
- package/src/_exports/_internal.ts +14 -0
- package/src/_exports/index.ts +10 -1
- package/src/auth/authStore.test.ts +150 -1
- package/src/auth/authStore.ts +11 -11
- package/src/auth/dashboardAuth.ts +2 -2
- package/src/auth/handleAuthCallback.ts +9 -3
- package/src/auth/logout.test.ts +1 -1
- package/src/auth/logout.ts +1 -1
- package/src/auth/refreshStampedToken.test.ts +118 -1
- package/src/auth/refreshStampedToken.ts +3 -2
- package/src/auth/standaloneAuth.ts +9 -3
- package/src/auth/studioAuth.ts +34 -7
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +10 -2
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +5 -1
- package/src/auth/subscribeToStorageEventsAndSetToken.ts +2 -2
- package/src/auth/utils.ts +33 -0
- package/src/client/clientStore.test.ts +14 -0
- package/src/client/clientStore.ts +2 -1
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/sanityConfig.ts +6 -0
- package/src/document/actions.ts +18 -11
- package/src/document/applyDocumentActions.test.ts +7 -6
- package/src/document/applyDocumentActions.ts +10 -4
- package/src/document/documentStore.test.ts +536 -188
- package/src/document/documentStore.ts +142 -76
- package/src/document/events.ts +7 -2
- package/src/document/permissions.test.ts +18 -16
- package/src/document/permissions.ts +35 -11
- package/src/document/processActions.test.ts +359 -32
- package/src/document/processActions.ts +104 -76
- package/src/document/reducers.test.ts +117 -29
- package/src/document/reducers.ts +43 -36
- package/src/document/sharedListener.ts +16 -6
- package/src/document/util.ts +14 -0
- package/src/favorites/favorites.test.ts +9 -2
- package/src/presence/bifurTransport.ts +6 -1
- package/src/preview/getPreviewState.test.ts +115 -98
- package/src/preview/getPreviewState.ts +38 -60
- package/src/preview/previewProjectionUtils.test.ts +179 -0
- package/src/preview/previewProjectionUtils.ts +93 -0
- package/src/preview/resolvePreview.test.ts +42 -25
- package/src/preview/resolvePreview.ts +29 -10
- package/src/preview/{previewStore.ts → types.ts} +8 -17
- package/src/projection/getProjectionState.test.ts +16 -16
- package/src/projection/getProjectionState.ts +2 -1
- package/src/projection/projectionQuery.ts +2 -3
- package/src/projection/types.ts +1 -1
- package/src/query/queryStore.ts +2 -1
- package/src/releases/getPerspectiveState.ts +7 -6
- package/src/releases/releasesStore.test.ts +20 -5
- package/src/releases/releasesStore.ts +20 -8
- package/src/store/createStateSourceAction.test.ts +62 -0
- package/src/store/createStateSourceAction.ts +34 -39
- package/src/telemetry/__telemetry__/sdk.telemetry.ts +42 -0
- package/src/telemetry/devMode.test.ts +52 -0
- package/src/telemetry/devMode.ts +40 -0
- package/src/telemetry/initTelemetry.test.ts +225 -0
- package/src/telemetry/initTelemetry.ts +205 -0
- package/src/telemetry/telemetryManager.test.ts +263 -0
- package/src/telemetry/telemetryManager.ts +187 -0
- package/src/users/usersStore.test.ts +1 -0
- package/src/users/usersStore.ts +5 -1
- package/src/utils/createFetcherStore.test.ts +6 -4
- package/src/utils/createFetcherStore.ts +2 -1
- package/src/utils/getStagingApiHost.test.ts +21 -0
- package/src/utils/getStagingApiHost.ts +14 -0
- package/src/utils/ids.test.ts +1 -29
- package/src/utils/ids.ts +0 -10
- package/src/utils/setCleanupTimeout.ts +24 -0
- package/src/preview/previewQuery.test.ts +0 -236
- package/src/preview/previewQuery.ts +0 -153
- package/src/preview/previewStore.test.ts +0 -36
- package/src/preview/subscribeToStateAndFetchBatches.test.ts +0 -221
- package/src/preview/subscribeToStateAndFetchBatches.ts +0 -112
- package/src/preview/util.ts +0 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK",
|
|
6
6
|
"keywords": [
|
|
@@ -31,7 +31,12 @@
|
|
|
31
31
|
"import": "./dist/index.js",
|
|
32
32
|
"default": "./dist/index.js"
|
|
33
33
|
},
|
|
34
|
-
"./package.json": "./package.json"
|
|
34
|
+
"./package.json": "./package.json",
|
|
35
|
+
"./_internal": {
|
|
36
|
+
"source": "./src/_exports/_internal.ts",
|
|
37
|
+
"import": "./dist/_exports/_internal.js",
|
|
38
|
+
"default": "./dist/_exports/_internal.js"
|
|
39
|
+
}
|
|
35
40
|
},
|
|
36
41
|
"main": "./dist/index.js",
|
|
37
42
|
"module": "./dist/index.js",
|
|
@@ -49,9 +54,11 @@
|
|
|
49
54
|
"@sanity/diff-match-patch": "^3.2.0",
|
|
50
55
|
"@sanity/diff-patch": "^6.0.0",
|
|
51
56
|
"@sanity/id-utils": "^1.0.0",
|
|
57
|
+
"@sanity/image-url": "^2.0.3",
|
|
52
58
|
"@sanity/json-match": "^1.0.5",
|
|
53
59
|
"@sanity/message-protocol": "^0.18.0",
|
|
54
|
-
"@sanity/mutate": "^0.
|
|
60
|
+
"@sanity/mutate": "^0.16.1",
|
|
61
|
+
"@sanity/telemetry": "^1.0.0",
|
|
55
62
|
"@sanity/types": "^5.2.0",
|
|
56
63
|
"groq": "3.88.1-typegen-experimental.0",
|
|
57
64
|
"groq-js": "^1.19.0",
|
|
@@ -70,7 +77,7 @@
|
|
|
70
77
|
"prettier": "^3.7.3",
|
|
71
78
|
"rollup-plugin-visualizer": "^5.14.0",
|
|
72
79
|
"typescript": "^5.8.3",
|
|
73
|
-
"vite": "^
|
|
80
|
+
"vite": "^7.0.0",
|
|
74
81
|
"vitest": "^3.2.4",
|
|
75
82
|
"@repo/config-eslint": "0.0.0",
|
|
76
83
|
"@repo/config-test": "0.0.1",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export {isStudioConfig} from '../auth/authMode'
|
|
2
|
+
export {
|
|
3
|
+
type ApiErrorBody,
|
|
4
|
+
getClientErrorApiBody,
|
|
5
|
+
getClientErrorApiDescription,
|
|
6
|
+
getClientErrorApiType,
|
|
7
|
+
isProjectUserNotFoundClientError,
|
|
8
|
+
} from '../auth/utils'
|
|
9
|
+
export {PREVIEW_PROJECTION} from '../preview/previewConstants'
|
|
10
|
+
export {transformProjectionToPreview} from '../preview/previewProjectionUtils'
|
|
11
|
+
export {getQueryKey, parseQueryKey} from '../query/queryStore' // only used for memoizing in React, not needed for actual functionality
|
|
12
|
+
export {getTelemetryManager, initTelemetry, trackHookMounted} from '../telemetry/initTelemetry'
|
|
13
|
+
export {getUsersKey, parseUsersKey} from '../users/reducers' // only used for memoizing in React, not needed for actual functionality
|
|
14
|
+
export {createGroqSearchFilter} from '../utils/createGroqSearchFilter'
|
package/src/_exports/index.ts
CHANGED
|
@@ -141,6 +141,7 @@ export {
|
|
|
141
141
|
type DocumentEditedEvent,
|
|
142
142
|
type DocumentEvent,
|
|
143
143
|
type DocumentPublishedEvent,
|
|
144
|
+
type DocumentTransactionSubmissionResult,
|
|
144
145
|
type DocumentUnpublishedEvent,
|
|
145
146
|
type TransactionAcceptedEvent,
|
|
146
147
|
type TransactionRevertedEvent,
|
|
@@ -159,8 +160,16 @@ export type {
|
|
|
159
160
|
UserPresence,
|
|
160
161
|
} from '../presence/types'
|
|
161
162
|
export {getPreviewState, type GetPreviewStateOptions} from '../preview/getPreviewState'
|
|
162
|
-
export
|
|
163
|
+
export {PREVIEW_PROJECTION} from '../preview/previewConstants'
|
|
164
|
+
export {transformProjectionToPreview} from '../preview/previewProjectionUtils'
|
|
163
165
|
export {resolvePreview, type ResolvePreviewOptions} from '../preview/resolvePreview'
|
|
166
|
+
export type {
|
|
167
|
+
PreviewMedia,
|
|
168
|
+
PreviewQueryResult,
|
|
169
|
+
PreviewStoreState,
|
|
170
|
+
PreviewValue,
|
|
171
|
+
ValuePending,
|
|
172
|
+
} from '../preview/types'
|
|
164
173
|
export {type OrgVerificationResult} from '../project/organizationVerification'
|
|
165
174
|
export {getProjectState, resolveProject} from '../project/project'
|
|
166
175
|
export {getProjectionState} from '../projection/getProjectionState'
|
|
@@ -7,6 +7,7 @@ import {createSanityInstance} from '../store/createSanityInstance'
|
|
|
7
7
|
import {AuthStateType} from './authStateType'
|
|
8
8
|
import {
|
|
9
9
|
authStore,
|
|
10
|
+
getAuthMethodState,
|
|
10
11
|
getAuthState,
|
|
11
12
|
getCurrentUserState,
|
|
12
13
|
getDashboardOrganizationId,
|
|
@@ -17,7 +18,12 @@ import {handleAuthCallback} from './handleAuthCallback'
|
|
|
17
18
|
import {checkForCookieAuth, getStudioTokenFromLocalStorage} from './studioModeAuth'
|
|
18
19
|
import {subscribeToStateAndFetchCurrentUser} from './subscribeToStateAndFetchCurrentUser'
|
|
19
20
|
import {subscribeToStorageEventsAndSetToken} from './subscribeToStorageEventsAndSetToken'
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
createLoggedInAuthState,
|
|
23
|
+
getAuthCode,
|
|
24
|
+
getTokenFromLocation,
|
|
25
|
+
getTokenFromStorage,
|
|
26
|
+
} from './utils'
|
|
21
27
|
|
|
22
28
|
vi.mock('./utils', async (importOriginal) => {
|
|
23
29
|
const original = await importOriginal<typeof import('./utils')>()
|
|
@@ -67,6 +73,35 @@ describe('authStore', () => {
|
|
|
67
73
|
instance?.dispose()
|
|
68
74
|
})
|
|
69
75
|
|
|
76
|
+
it('uses staging apiHost when __SANITY_STAGING__ is true and no explicit apiHost', () => {
|
|
77
|
+
vi.stubGlobal('__SANITY_STAGING__', true)
|
|
78
|
+
|
|
79
|
+
instance = createSanityInstance({
|
|
80
|
+
projectId: 'p',
|
|
81
|
+
dataset: 'd',
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const {options} = authStore.getInitialState(instance, null)
|
|
85
|
+
expect(options.apiHost).toBe('https://api.sanity.work')
|
|
86
|
+
|
|
87
|
+
vi.unstubAllGlobals()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('prefers explicit apiHost over __SANITY_STAGING__', () => {
|
|
91
|
+
vi.stubGlobal('__SANITY_STAGING__', true)
|
|
92
|
+
|
|
93
|
+
instance = createSanityInstance({
|
|
94
|
+
projectId: 'p',
|
|
95
|
+
dataset: 'd',
|
|
96
|
+
auth: {apiHost: 'https://custom.host'},
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const {options} = authStore.getInitialState(instance, null)
|
|
100
|
+
expect(options.apiHost).toBe('https://custom.host')
|
|
101
|
+
|
|
102
|
+
vi.unstubAllGlobals()
|
|
103
|
+
})
|
|
104
|
+
|
|
70
105
|
it('sets initial options onto state', () => {
|
|
71
106
|
const apiHost = 'test-api-host'
|
|
72
107
|
const callbackUrl = '/login/callback'
|
|
@@ -368,6 +403,61 @@ describe('authStore', () => {
|
|
|
368
403
|
expect(checkForCookieAuth).not.toHaveBeenCalled()
|
|
369
404
|
})
|
|
370
405
|
|
|
406
|
+
it('treats null token as cookie auth when studio reports authenticated', () => {
|
|
407
|
+
let tokenObserver!: {next: (token: string | null) => void}
|
|
408
|
+
const mockSubscribe = vi.fn((observer: {next: (token: string | null) => void}) => {
|
|
409
|
+
tokenObserver = observer
|
|
410
|
+
return {unsubscribe: vi.fn()}
|
|
411
|
+
})
|
|
412
|
+
const mockTokenSource = {subscribe: mockSubscribe}
|
|
413
|
+
|
|
414
|
+
instance = createSanityInstance({
|
|
415
|
+
projectId: 'studio-project',
|
|
416
|
+
dataset: 'production',
|
|
417
|
+
studio: {
|
|
418
|
+
authenticated: true,
|
|
419
|
+
auth: {token: mockTokenSource},
|
|
420
|
+
},
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
getAuthState(instance)
|
|
424
|
+
tokenObserver.next(null)
|
|
425
|
+
|
|
426
|
+
expect(getAuthState(instance).getCurrent()).toMatchObject({
|
|
427
|
+
type: AuthStateType.LOGGED_IN,
|
|
428
|
+
token: '',
|
|
429
|
+
})
|
|
430
|
+
expect(getAuthMethodState(instance).getCurrent()).toBe('cookie')
|
|
431
|
+
expect(checkForCookieAuth).not.toHaveBeenCalled()
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it('sets logged out when token is null and studio is not authenticated', () => {
|
|
435
|
+
let tokenObserver!: {next: (token: string | null) => void}
|
|
436
|
+
const mockSubscribe = vi.fn((observer: {next: (token: string | null) => void}) => {
|
|
437
|
+
tokenObserver = observer
|
|
438
|
+
return {unsubscribe: vi.fn()}
|
|
439
|
+
})
|
|
440
|
+
const mockTokenSource = {subscribe: mockSubscribe}
|
|
441
|
+
|
|
442
|
+
instance = createSanityInstance({
|
|
443
|
+
projectId: 'studio-project',
|
|
444
|
+
dataset: 'production',
|
|
445
|
+
studio: {
|
|
446
|
+
authenticated: false,
|
|
447
|
+
auth: {token: mockTokenSource},
|
|
448
|
+
},
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
getAuthState(instance)
|
|
452
|
+
tokenObserver.next(null)
|
|
453
|
+
|
|
454
|
+
expect(getAuthState(instance).getCurrent()).toMatchObject({
|
|
455
|
+
type: AuthStateType.LOGGED_OUT,
|
|
456
|
+
})
|
|
457
|
+
expect(getAuthMethodState(instance).getCurrent()).toBeUndefined()
|
|
458
|
+
expect(checkForCookieAuth).not.toHaveBeenCalled()
|
|
459
|
+
})
|
|
460
|
+
|
|
371
461
|
it('falls back to default auth (storage token) when studio mode is disabled', () => {
|
|
372
462
|
const storageToken = 'regular-storage-token'
|
|
373
463
|
vi.mocked(getTokenFromStorage).mockReturnValue(storageToken)
|
|
@@ -673,4 +763,63 @@ describe('authStore', () => {
|
|
|
673
763
|
expect(organizationId.getCurrent()).toBeUndefined()
|
|
674
764
|
})
|
|
675
765
|
})
|
|
766
|
+
|
|
767
|
+
describe('createLoggedInAuthState', () => {
|
|
768
|
+
beforeEach(() => {
|
|
769
|
+
vi.useFakeTimers()
|
|
770
|
+
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'))
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
afterEach(() => {
|
|
774
|
+
vi.useRealTimers()
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
it('sets lastTokenRefresh for stamped tokens', () => {
|
|
778
|
+
const state = createLoggedInAuthState('sk-stamped-token-st123', null)
|
|
779
|
+
|
|
780
|
+
expect(state).toEqual({
|
|
781
|
+
type: AuthStateType.LOGGED_IN,
|
|
782
|
+
token: 'sk-stamped-token-st123',
|
|
783
|
+
currentUser: null,
|
|
784
|
+
lastTokenRefresh: Date.now(),
|
|
785
|
+
})
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
it('does not set lastTokenRefresh for non-stamped tokens', () => {
|
|
789
|
+
const state = createLoggedInAuthState('sk-regular-token', null)
|
|
790
|
+
|
|
791
|
+
expect(state).toEqual({
|
|
792
|
+
type: AuthStateType.LOGGED_IN,
|
|
793
|
+
token: 'sk-regular-token',
|
|
794
|
+
currentUser: null,
|
|
795
|
+
})
|
|
796
|
+
expect(state.lastTokenRefresh).toBeUndefined()
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
it('preserves an existing lastTokenRefresh when provided', () => {
|
|
800
|
+
const existingTimestamp = Date.now() - 5000
|
|
801
|
+
const state = createLoggedInAuthState('sk-stamped-token-st123', null, existingTimestamp)
|
|
802
|
+
|
|
803
|
+
expect(state.lastTokenRefresh).toBe(existingTimestamp)
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
it('passes through the currentUser', () => {
|
|
807
|
+
const user = {id: 'user-1', name: 'Test'} as CurrentUser
|
|
808
|
+
const state = createLoggedInAuthState('sk-stamped-token-st123', user)
|
|
809
|
+
|
|
810
|
+
expect(state.currentUser).toBe(user)
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
it('handles null currentUser', () => {
|
|
814
|
+
const state = createLoggedInAuthState('sk-stamped-token-st123', null)
|
|
815
|
+
|
|
816
|
+
expect(state.currentUser).toBeNull()
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
it('does not set lastTokenRefresh when explicitly undefined for non-stamped token', () => {
|
|
820
|
+
const state = createLoggedInAuthState('sk-regular-token', null, undefined)
|
|
821
|
+
|
|
822
|
+
expect(state.lastTokenRefresh).toBeUndefined()
|
|
823
|
+
})
|
|
824
|
+
})
|
|
676
825
|
})
|
package/src/auth/authStore.ts
CHANGED
|
@@ -5,13 +5,14 @@ import {type AuthConfig, type AuthProvider} from '../config/authConfig'
|
|
|
5
5
|
import {bindActionGlobally} from '../store/createActionBinder'
|
|
6
6
|
import {createStateSourceAction} from '../store/createStateSourceAction'
|
|
7
7
|
import {defineStore} from '../store/defineStore'
|
|
8
|
+
import {getStagingApiHost} from '../utils/getStagingApiHost'
|
|
8
9
|
import {resolveAuthMode} from './authMode'
|
|
9
10
|
import {AuthStateType} from './authStateType'
|
|
10
11
|
import {type AuthStrategyOptions} from './authStrategy'
|
|
11
12
|
import {getDashboardInitialState, initializeDashboardAuth} from './dashboardAuth'
|
|
12
13
|
import {getStandaloneInitialState, initializeStandaloneAuth} from './standaloneAuth'
|
|
13
14
|
import {getStudioInitialState, initializeStudioAuth} from './studioAuth'
|
|
14
|
-
import {getCleanedUrl, getDefaultLocation} from './utils'
|
|
15
|
+
import {createLoggedInAuthState, getCleanedUrl, getDefaultLocation} from './utils'
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Public types
|
|
@@ -102,7 +103,7 @@ export const authStore = defineStore<AuthStoreState>({
|
|
|
102
103
|
|
|
103
104
|
getInitialState(instance) {
|
|
104
105
|
const {
|
|
105
|
-
apiHost,
|
|
106
|
+
apiHost: configApiHost,
|
|
106
107
|
callbackUrl,
|
|
107
108
|
providers: customProviders,
|
|
108
109
|
token: providedToken,
|
|
@@ -110,6 +111,7 @@ export const authStore = defineStore<AuthStoreState>({
|
|
|
110
111
|
initialLocationHref = getDefaultLocation(),
|
|
111
112
|
} = instance.config.auth ?? {}
|
|
112
113
|
|
|
114
|
+
const apiHost = configApiHost ?? getStagingApiHost()
|
|
113
115
|
const authConfig = instance.config.auth ?? {}
|
|
114
116
|
|
|
115
117
|
// Build login URL (used by standalone mode, but always computed for the
|
|
@@ -274,16 +276,14 @@ export const setAuthToken = bindActionGlobally(authStore, ({state}, token: strin
|
|
|
274
276
|
if (token) {
|
|
275
277
|
// Update state only if the new token is different or currently logged out
|
|
276
278
|
if (currentAuthState.type !== AuthStateType.LOGGED_IN || currentAuthState.token !== token) {
|
|
277
|
-
|
|
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
|
|
278
285
|
state.set('setToken', {
|
|
279
|
-
authState:
|
|
280
|
-
type: AuthStateType.LOGGED_IN,
|
|
281
|
-
token: token,
|
|
282
|
-
// Keep existing user or set to null? Setting to null forces refetch.
|
|
283
|
-
// Keep existing user to avoid unnecessary refetches if user data is still valid.
|
|
284
|
-
currentUser:
|
|
285
|
-
currentAuthState.type === AuthStateType.LOGGED_IN ? currentAuthState.currentUser : null,
|
|
286
|
-
},
|
|
286
|
+
authState: createLoggedInAuthState(token, currentUser, preservedLastTokenRefresh),
|
|
287
287
|
})
|
|
288
288
|
}
|
|
289
289
|
} else {
|
|
@@ -7,7 +7,7 @@ import {type AuthStoreState, type DashboardContext} from './authStore'
|
|
|
7
7
|
import {type AuthStrategyOptions, type AuthStrategyResult} from './authStrategy'
|
|
8
8
|
import {refreshStampedToken} from './refreshStampedToken'
|
|
9
9
|
import {subscribeToStateAndFetchCurrentUser} from './subscribeToStateAndFetchCurrentUser'
|
|
10
|
-
import {getAuthCode, getTokenFromLocation} from './utils'
|
|
10
|
+
import {createLoggedInAuthState, getAuthCode, getTokenFromLocation} from './utils'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Parses the dashboard context from a location href's `_context` URL parameter.
|
|
@@ -66,7 +66,7 @@ export function getDashboardInitialState(options: AuthStrategyOptions): AuthStra
|
|
|
66
66
|
// Provided token always wins, even in dashboard
|
|
67
67
|
if (providedToken) {
|
|
68
68
|
return {
|
|
69
|
-
authState:
|
|
69
|
+
authState: createLoggedInAuthState(providedToken, null),
|
|
70
70
|
storageKey,
|
|
71
71
|
storageArea,
|
|
72
72
|
authMethod: undefined,
|
|
@@ -2,7 +2,13 @@ import {bindActionGlobally} from '../store/createActionBinder'
|
|
|
2
2
|
import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
|
|
3
3
|
import {AuthStateType} from './authStateType'
|
|
4
4
|
import {authStore, type AuthStoreState, type DashboardContext} from './authStore'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createLoggedInAuthState,
|
|
7
|
+
getAuthCode,
|
|
8
|
+
getCleanedUrl,
|
|
9
|
+
getDefaultLocation,
|
|
10
|
+
getTokenFromLocation,
|
|
11
|
+
} from './utils'
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
14
|
* @public
|
|
@@ -27,7 +33,7 @@ export const handleAuthCallback = bindActionGlobally(
|
|
|
27
33
|
const tokenFromUrl = getTokenFromLocation(locationHref)
|
|
28
34
|
if (tokenFromUrl) {
|
|
29
35
|
state.set('setTokenFromUrl', {
|
|
30
|
-
authState:
|
|
36
|
+
authState: createLoggedInAuthState(tokenFromUrl, null),
|
|
31
37
|
})
|
|
32
38
|
return cleanedUrl
|
|
33
39
|
}
|
|
@@ -77,7 +83,7 @@ export const handleAuthCallback = bindActionGlobally(
|
|
|
77
83
|
})
|
|
78
84
|
|
|
79
85
|
storageArea?.setItem(storageKey, JSON.stringify({token}))
|
|
80
|
-
state.set('setToken', {authState:
|
|
86
|
+
state.set('setToken', {authState: createLoggedInAuthState(token, null)})
|
|
81
87
|
|
|
82
88
|
return cleanedUrl
|
|
83
89
|
} catch (error) {
|
package/src/auth/logout.test.ts
CHANGED
|
@@ -59,7 +59,7 @@ describe('logout', () => {
|
|
|
59
59
|
useProjectHostname: false,
|
|
60
60
|
useCdn: false,
|
|
61
61
|
})
|
|
62
|
-
expect(mockRequest).toHaveBeenCalledWith({method: 'POST', uri: '/auth/logout'})
|
|
62
|
+
expect(mockRequest).toHaveBeenCalledWith({method: 'POST', uri: '/auth/logout', tag: 'logout'})
|
|
63
63
|
expect(removeItem).toHaveBeenCalledWith('__sanity_auth_token')
|
|
64
64
|
})
|
|
65
65
|
|
package/src/auth/logout.ts
CHANGED
|
@@ -33,7 +33,7 @@ export const logout = bindActionGlobally(authStore, async ({state}) => {
|
|
|
33
33
|
useCdn: false,
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
await client.request<void>({uri: '/auth/logout', method: 'POST'})
|
|
36
|
+
await client.request<void>({uri: '/auth/logout', method: 'POST', tag: 'logout'})
|
|
37
37
|
}
|
|
38
38
|
} finally {
|
|
39
39
|
state.set('logoutSuccess', {
|
|
@@ -7,13 +7,13 @@ import {createSanityInstance} from '../store/createSanityInstance'
|
|
|
7
7
|
import {createStoreState} from '../store/createStoreState'
|
|
8
8
|
import {AuthStateType} from './authStateType'
|
|
9
9
|
import {type AuthState, authStore} from './authStore'
|
|
10
|
-
// Import only the public function
|
|
11
10
|
import {
|
|
12
11
|
getLastRefreshTime,
|
|
13
12
|
getNextRefreshDelay,
|
|
14
13
|
refreshStampedToken,
|
|
15
14
|
setLastRefreshTime,
|
|
16
15
|
} from './refreshStampedToken'
|
|
16
|
+
import {createLoggedInAuthState} from './utils'
|
|
17
17
|
|
|
18
18
|
// Type definitions for Web Locks (can be kept if needed for context)
|
|
19
19
|
// ... (Lock, LockOptions, LockGrantedCallback types)
|
|
@@ -147,6 +147,123 @@ describe('refreshStampedToken', () => {
|
|
|
147
147
|
expect(locksRequest).not.toHaveBeenCalled()
|
|
148
148
|
})
|
|
149
149
|
|
|
150
|
+
it('does not refresh on visibility change when lastTokenRefresh is recent', async () => {
|
|
151
|
+
const mockClient = {
|
|
152
|
+
observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
|
|
153
|
+
}
|
|
154
|
+
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
155
|
+
const instance = createSanityInstance({
|
|
156
|
+
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
157
|
+
})
|
|
158
|
+
const initialState = authStore.getInitialState(instance, null)
|
|
159
|
+
initialState.authState = createLoggedInAuthState('sk-initial-token-st123', null)
|
|
160
|
+
initialState.dashboardContext = {mode: 'test'}
|
|
161
|
+
const state = createStoreState(initialState)
|
|
162
|
+
|
|
163
|
+
const subscription = refreshStampedToken({state, instance, key: null})
|
|
164
|
+
subscriptions.push(subscription)
|
|
165
|
+
|
|
166
|
+
const addEventListenerMock = global.document.addEventListener as ReturnType<typeof vi.fn>
|
|
167
|
+
expect(addEventListenerMock).toHaveBeenCalledWith('visibilitychange', expect.any(Function))
|
|
168
|
+
const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
|
|
169
|
+
|
|
170
|
+
Object.defineProperty(global.document, 'visibilityState', {
|
|
171
|
+
value: 'visible',
|
|
172
|
+
writable: true,
|
|
173
|
+
configurable: true,
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
visibilityHandler()
|
|
177
|
+
await vi.advanceTimersByTimeAsync(100)
|
|
178
|
+
|
|
179
|
+
expect(mockClient.observable.request).not.toHaveBeenCalled()
|
|
180
|
+
const finalAuthState = state.get().authState
|
|
181
|
+
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
182
|
+
expect(finalAuthState.token).toBe('sk-initial-token-st123')
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('refreshes on visibility change when lastTokenRefresh is stale', async () => {
|
|
187
|
+
const REFRESH_INTERVAL = 12 * 60 * 60 * 1000
|
|
188
|
+
const mockClient = {
|
|
189
|
+
observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
|
|
190
|
+
}
|
|
191
|
+
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
192
|
+
const instance = createSanityInstance({
|
|
193
|
+
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
194
|
+
})
|
|
195
|
+
const initialState = authStore.getInitialState(instance, null)
|
|
196
|
+
const staleTimestamp = Date.now() - REFRESH_INTERVAL - 1000
|
|
197
|
+
initialState.authState = {
|
|
198
|
+
type: AuthStateType.LOGGED_IN,
|
|
199
|
+
token: 'sk-initial-token-st123',
|
|
200
|
+
currentUser: null,
|
|
201
|
+
lastTokenRefresh: staleTimestamp,
|
|
202
|
+
}
|
|
203
|
+
initialState.dashboardContext = {mode: 'test'}
|
|
204
|
+
const state = createStoreState(initialState)
|
|
205
|
+
|
|
206
|
+
const subscription = refreshStampedToken({state, instance, key: null})
|
|
207
|
+
subscriptions.push(subscription)
|
|
208
|
+
|
|
209
|
+
const addEventListenerMock = global.document.addEventListener as ReturnType<typeof vi.fn>
|
|
210
|
+
expect(addEventListenerMock).toHaveBeenCalledWith('visibilitychange', expect.any(Function))
|
|
211
|
+
const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
|
|
212
|
+
|
|
213
|
+
Object.defineProperty(global.document, 'visibilityState', {
|
|
214
|
+
value: 'visible',
|
|
215
|
+
writable: true,
|
|
216
|
+
configurable: true,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
visibilityHandler()
|
|
220
|
+
await vi.advanceTimersToNextTimerAsync()
|
|
221
|
+
|
|
222
|
+
expect(mockClient.observable.request).toHaveBeenCalled()
|
|
223
|
+
const finalAuthState = state.get().authState
|
|
224
|
+
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
225
|
+
expect(finalAuthState.token).toBe('sk-refreshed-token-st123')
|
|
226
|
+
expect(finalAuthState.lastTokenRefresh).toBeGreaterThan(staleTimestamp)
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('refreshes on visibility change when lastTokenRefresh is undefined (pre-fix behavior)', async () => {
|
|
231
|
+
const mockClient = {
|
|
232
|
+
observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
|
|
233
|
+
}
|
|
234
|
+
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
235
|
+
const instance = createSanityInstance({
|
|
236
|
+
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
237
|
+
})
|
|
238
|
+
const initialState = authStore.getInitialState(instance, null)
|
|
239
|
+
initialState.authState = {
|
|
240
|
+
type: AuthStateType.LOGGED_IN,
|
|
241
|
+
token: 'sk-initial-token-st123',
|
|
242
|
+
currentUser: null,
|
|
243
|
+
// lastTokenRefresh intentionally omitted to demonstrate the old bug
|
|
244
|
+
}
|
|
245
|
+
initialState.dashboardContext = {mode: 'test'}
|
|
246
|
+
const state = createStoreState(initialState)
|
|
247
|
+
|
|
248
|
+
const subscription = refreshStampedToken({state, instance, key: null})
|
|
249
|
+
subscriptions.push(subscription)
|
|
250
|
+
|
|
251
|
+
const addEventListenerMock = global.document.addEventListener as ReturnType<typeof vi.fn>
|
|
252
|
+
const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
|
|
253
|
+
|
|
254
|
+
Object.defineProperty(global.document, 'visibilityState', {
|
|
255
|
+
value: 'visible',
|
|
256
|
+
writable: true,
|
|
257
|
+
configurable: true,
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
visibilityHandler()
|
|
261
|
+
await vi.advanceTimersToNextTimerAsync()
|
|
262
|
+
|
|
263
|
+
// Without lastTokenRefresh, shouldRefreshToken returns true — this is the bug
|
|
264
|
+
expect(mockClient.observable.request).toHaveBeenCalled()
|
|
265
|
+
})
|
|
266
|
+
|
|
150
267
|
it('does not refresh when tab is not visible', async () => {
|
|
151
268
|
// Set visibility to hidden
|
|
152
269
|
Object.defineProperty(global, 'document', {
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from 'rxjs'
|
|
14
14
|
|
|
15
15
|
import {type StoreContext} from '../store/defineStore'
|
|
16
|
-
import {DEFAULT_API_VERSION} from './authConstants'
|
|
16
|
+
import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
|
|
17
17
|
import {AuthStateType} from './authStateType'
|
|
18
18
|
import {type AuthState, type AuthStoreState} from './authStore'
|
|
19
19
|
|
|
@@ -58,7 +58,7 @@ function createTokenRefreshStream(
|
|
|
58
58
|
return new Observable((subscriber) => {
|
|
59
59
|
const client = clientFactory({
|
|
60
60
|
apiVersion: DEFAULT_API_VERSION,
|
|
61
|
-
requestTagPrefix:
|
|
61
|
+
requestTagPrefix: REQUEST_TAG_PREFIX,
|
|
62
62
|
useProjectHostname: false,
|
|
63
63
|
useCdn: false,
|
|
64
64
|
token,
|
|
@@ -70,6 +70,7 @@ function createTokenRefreshStream(
|
|
|
70
70
|
.request<{token: string}>({
|
|
71
71
|
uri: 'auth/refresh-token',
|
|
72
72
|
method: 'POST',
|
|
73
|
+
tag: 'refresh-token',
|
|
73
74
|
body: {
|
|
74
75
|
token,
|
|
75
76
|
},
|
|
@@ -7,7 +7,13 @@ import {type AuthStrategyOptions, type AuthStrategyResult} from './authStrategy'
|
|
|
7
7
|
import {refreshStampedToken} from './refreshStampedToken'
|
|
8
8
|
import {subscribeToStateAndFetchCurrentUser} from './subscribeToStateAndFetchCurrentUser'
|
|
9
9
|
import {subscribeToStorageEventsAndSetToken} from './subscribeToStorageEventsAndSetToken'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
createLoggedInAuthState,
|
|
12
|
+
getAuthCode,
|
|
13
|
+
getDefaultStorage,
|
|
14
|
+
getTokenFromLocation,
|
|
15
|
+
getTokenFromStorage,
|
|
16
|
+
} from './utils'
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Resolves the initial auth state for Standalone mode.
|
|
@@ -30,7 +36,7 @@ export function getStandaloneInitialState(options: AuthStrategyOptions): AuthStr
|
|
|
30
36
|
// Provided token always wins
|
|
31
37
|
if (providedToken) {
|
|
32
38
|
return {
|
|
33
|
-
authState:
|
|
39
|
+
authState: createLoggedInAuthState(providedToken, null),
|
|
34
40
|
storageKey,
|
|
35
41
|
storageArea,
|
|
36
42
|
authMethod: undefined,
|
|
@@ -53,7 +59,7 @@ export function getStandaloneInitialState(options: AuthStrategyOptions): AuthStr
|
|
|
53
59
|
const token = getTokenFromStorage(storageArea, storageKey)
|
|
54
60
|
if (token) {
|
|
55
61
|
return {
|
|
56
|
-
authState:
|
|
62
|
+
authState: createLoggedInAuthState(token, null),
|
|
57
63
|
storageKey,
|
|
58
64
|
storageArea,
|
|
59
65
|
authMethod: 'localstorage',
|