@sanity/sdk 2.2.0 → 2.3.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/dist/index.d.ts +23 -339
- package/dist/index.js +182 -1800
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/_exports/index.ts +1 -0
- package/src/auth/authStore.test.ts +12 -20
- package/src/auth/authStore.ts +46 -15
- package/src/auth/studioModeAuth.test.ts +4 -4
- package/src/auth/studioModeAuth.ts +4 -5
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +20 -9
- package/src/auth/utils.test.ts +34 -0
- package/src/auth/utils.ts +10 -2
- package/src/document/permissions.test.ts +2 -3
- package/src/document/permissions.ts +3 -3
- package/src/document/processActions.test.ts +1 -1
- package/src/document/processActions.ts +3 -3
- package/src/projection/getProjectionState.ts +5 -5
- package/src/projection/projectionQuery.test.ts +5 -6
- package/src/projection/projectionQuery.ts +4 -7
- package/src/projection/resolveProjection.test.ts +2 -2
- package/src/projection/resolveProjection.ts +2 -2
- package/src/projection/types.ts +9 -6
- package/src/projection/util.ts +2 -4
- package/src/query/queryStore.ts +14 -2
- package/src/utils/getCorsErrorProjectId.test.ts +46 -0
- package/src/utils/getCorsErrorProjectId.ts +15 -0
- package/src/document/_synchronous-groq-js.mjs +0 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK",
|
|
6
6
|
"keywords": [
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"prettier": "@sanity/prettier-config",
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@sanity/bifur-client": "^0.4.1",
|
|
46
|
-
"@sanity/client": "^7.
|
|
46
|
+
"@sanity/client": "^7.12.0",
|
|
47
47
|
"@sanity/comlink": "^3.0.4",
|
|
48
48
|
"@sanity/diff-match-patch": "^3.2.0",
|
|
49
49
|
"@sanity/diff-patch": "^6.0.0",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"@sanity/mutate": "^0.12.4",
|
|
53
53
|
"@sanity/types": "^3.83.0",
|
|
54
54
|
"groq": "3.88.1-typegen-experimental.0",
|
|
55
|
+
"groq-js": "^1.19.0",
|
|
55
56
|
"lodash-es": "^4.17.21",
|
|
56
57
|
"reselect": "^5.1.1",
|
|
57
58
|
"rxjs": "^7.8.2",
|
|
@@ -64,17 +65,16 @@
|
|
|
64
65
|
"@types/lodash-es": "^4.17.12",
|
|
65
66
|
"@vitest/coverage-v8": "3.1.2",
|
|
66
67
|
"eslint": "^9.22.0",
|
|
67
|
-
"groq-js": "^1.16.1",
|
|
68
68
|
"prettier": "^3.5.3",
|
|
69
69
|
"rollup-plugin-visualizer": "^5.14.0",
|
|
70
70
|
"typescript": "^5.8.3",
|
|
71
71
|
"vite": "^6.3.4",
|
|
72
72
|
"vitest": "^3.1.2",
|
|
73
73
|
"@repo/config-eslint": "0.0.0",
|
|
74
|
-
"@repo/config-test": "0.0.1",
|
|
75
|
-
"@repo/package.config": "0.0.1",
|
|
76
74
|
"@repo/package.bundle": "3.82.0",
|
|
77
|
-
"@repo/
|
|
75
|
+
"@repo/package.config": "0.0.1",
|
|
76
|
+
"@repo/tsconfig": "0.0.1",
|
|
77
|
+
"@repo/config-test": "0.0.1"
|
|
78
78
|
},
|
|
79
79
|
"engines": {
|
|
80
80
|
"node": ">=20.0.0"
|
package/src/_exports/index.ts
CHANGED
|
@@ -157,6 +157,7 @@ export {
|
|
|
157
157
|
export {type FetcherStore, type FetcherStoreState} from '../utils/createFetcherStore'
|
|
158
158
|
export {createGroqSearchFilter} from '../utils/createGroqSearchFilter'
|
|
159
159
|
export {defineIntent, type Intent, type IntentFilter} from '../utils/defineIntent'
|
|
160
|
+
export {getCorsErrorProjectId} from '../utils/getCorsErrorProjectId'
|
|
160
161
|
export {CORE_SDK_VERSION} from '../version'
|
|
161
162
|
export {
|
|
162
163
|
getIndexForKey,
|
|
@@ -237,6 +237,7 @@ describe('authStore', () => {
|
|
|
237
237
|
it('sets to logged in using studio token when studio mode is enabled and token exists', () => {
|
|
238
238
|
const studioToken = 'studio-token'
|
|
239
239
|
const projectId = 'studio-project'
|
|
240
|
+
const studioStorageKey = `__studio_auth_token_${projectId}`
|
|
240
241
|
const mockStorage = {
|
|
241
242
|
getItem: vi.fn(),
|
|
242
243
|
setItem: vi.fn(),
|
|
@@ -252,40 +253,38 @@ describe('authStore', () => {
|
|
|
252
253
|
})
|
|
253
254
|
|
|
254
255
|
const {authState, options} = authStore.getInitialState(instance)
|
|
255
|
-
expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage,
|
|
256
|
+
expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage, studioStorageKey)
|
|
256
257
|
expect(authState).toMatchObject({type: AuthStateType.LOGGED_IN, token: studioToken})
|
|
257
258
|
expect(options.authMethod).toBe('localstorage')
|
|
258
259
|
})
|
|
259
260
|
|
|
260
|
-
it('checks for cookie auth when studio mode is enabled and no studio token exists',
|
|
261
|
-
vi.useFakeTimers()
|
|
261
|
+
it('checks for cookie auth during initialize when studio mode is enabled and no studio token exists', () => {
|
|
262
262
|
const projectId = 'studio-project'
|
|
263
|
+
const studioStorageKey = `__studio_auth_token_${projectId}`
|
|
263
264
|
const mockStorage = {
|
|
264
265
|
getItem: vi.fn(),
|
|
265
266
|
setItem: vi.fn(),
|
|
266
267
|
removeItem: vi.fn(),
|
|
267
268
|
} as unknown as Storage // Mock storage
|
|
268
269
|
vi.mocked(getStudioTokenFromLocalStorage).mockReturnValue(null)
|
|
269
|
-
// Mock cookie check to return true asynchronously
|
|
270
270
|
vi.mocked(checkForCookieAuth).mockResolvedValue(true)
|
|
271
271
|
|
|
272
272
|
instance = createSanityInstance({
|
|
273
273
|
projectId,
|
|
274
274
|
dataset: 'd',
|
|
275
275
|
studioMode: {enabled: true},
|
|
276
|
-
auth: {storageArea: mockStorage},
|
|
276
|
+
auth: {storageArea: mockStorage},
|
|
277
277
|
})
|
|
278
278
|
|
|
279
|
-
//
|
|
279
|
+
// Verify initial state without async cookie probe
|
|
280
280
|
const {authState: initialAuthState} = authStore.getInitialState(instance)
|
|
281
|
-
expect(initialAuthState.type).toBe(AuthStateType.LOGGED_OUT)
|
|
282
|
-
expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage,
|
|
283
|
-
expect(checkForCookieAuth).toHaveBeenCalledWith(projectId, expect.any(Function))
|
|
281
|
+
expect(initialAuthState.type).toBe(AuthStateType.LOGGED_OUT)
|
|
282
|
+
expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage, studioStorageKey)
|
|
284
283
|
|
|
285
|
-
//
|
|
286
|
-
|
|
284
|
+
// Trigger store creation + initialize
|
|
285
|
+
getAuthState(instance)
|
|
287
286
|
|
|
288
|
-
|
|
287
|
+
expect(checkForCookieAuth).toHaveBeenCalledWith(projectId, expect.any(Function))
|
|
289
288
|
})
|
|
290
289
|
|
|
291
290
|
it('falls back to default auth (storage token) when studio mode is disabled', () => {
|
|
@@ -573,14 +572,7 @@ describe('authStore', () => {
|
|
|
573
572
|
expect(initialOrgId.getCurrent()).toBe('initial-org-id')
|
|
574
573
|
|
|
575
574
|
// Call handleCallback with the callback URL
|
|
576
|
-
await handleAuthCallback(instance, callbackUrl)
|
|
577
|
-
|
|
578
|
-
// Wait for the state update to be reflected in the selector
|
|
579
|
-
await vi.waitUntil(
|
|
580
|
-
() => getDashboardOrganizationId(instance).getCurrent() === 'callback-org-id',
|
|
581
|
-
)
|
|
582
|
-
// Add a microtask yield just in case
|
|
583
|
-
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
575
|
+
await handleAuthCallback(instance, callbackUrl)
|
|
584
576
|
|
|
585
577
|
// Check that the orgId from the callback context is now set
|
|
586
578
|
const finalOrgId = getDashboardOrganizationId(instance)
|
package/src/auth/authStore.ts
CHANGED
|
@@ -67,7 +67,11 @@ export interface DashboardContext {
|
|
|
67
67
|
orgId?: string
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The method of authentication used.
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
export type AuthMethodOptions = 'localstorage' | 'cookie' | undefined
|
|
71
75
|
|
|
72
76
|
let tokenRefresherRunning = false
|
|
73
77
|
|
|
@@ -105,7 +109,8 @@ export const authStore = defineStore<AuthStoreState>({
|
|
|
105
109
|
} = instance.config.auth ?? {}
|
|
106
110
|
let storageArea = instance.config.auth?.storageArea
|
|
107
111
|
|
|
108
|
-
|
|
112
|
+
let storageKey = `__sanity_auth_token`
|
|
113
|
+
const studioModeEnabled = instance.config.studioMode?.enabled
|
|
109
114
|
|
|
110
115
|
// This login URL will only be used for local development
|
|
111
116
|
let loginDomain = 'https://www.sanity.io'
|
|
@@ -149,23 +154,21 @@ export const authStore = defineStore<AuthStoreState>({
|
|
|
149
154
|
console.error('Failed to parse dashboard context from initial location:', err)
|
|
150
155
|
}
|
|
151
156
|
|
|
152
|
-
if (!isInDashboard) {
|
|
157
|
+
if (!isInDashboard || studioModeEnabled) {
|
|
153
158
|
// If not in dashboard, use the storage area from the config
|
|
159
|
+
// If studio mode is enabled, use the local storage area (default)
|
|
154
160
|
storageArea = storageArea ?? getDefaultStorage()
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
let token: string | null
|
|
158
164
|
let authMethod: AuthMethodOptions
|
|
159
|
-
if (
|
|
160
|
-
|
|
165
|
+
if (studioModeEnabled) {
|
|
166
|
+
// In studio mode, always use the studio-specific storage key and subscribe to it
|
|
167
|
+
const studioStorageKey = `__studio_auth_token_${instance.config.projectId ?? ''}`
|
|
168
|
+
storageKey = studioStorageKey
|
|
169
|
+
token = getStudioTokenFromLocalStorage(storageArea, studioStorageKey)
|
|
161
170
|
if (token) {
|
|
162
171
|
authMethod = 'localstorage'
|
|
163
|
-
} else {
|
|
164
|
-
checkForCookieAuth(instance.config.projectId, clientFactory).then((isCookieAuthEnabled) => {
|
|
165
|
-
if (isCookieAuthEnabled) {
|
|
166
|
-
authMethod = 'cookie'
|
|
167
|
-
}
|
|
168
|
-
})
|
|
169
172
|
}
|
|
170
173
|
} else {
|
|
171
174
|
token = getTokenFromStorage(storageArea, storageKey)
|
|
@@ -177,14 +180,16 @@ export const authStore = defineStore<AuthStoreState>({
|
|
|
177
180
|
let authState: AuthState
|
|
178
181
|
if (providedToken) {
|
|
179
182
|
authState = {type: AuthStateType.LOGGED_IN, token: providedToken, currentUser: null}
|
|
183
|
+
} else if (token && studioModeEnabled) {
|
|
184
|
+
authState = {type: AuthStateType.LOGGED_IN, token: token ?? '', currentUser: null}
|
|
180
185
|
} else if (
|
|
181
186
|
getAuthCode(callbackUrl, initialLocationHref) ||
|
|
182
187
|
getTokenFromLocation(initialLocationHref)
|
|
183
188
|
) {
|
|
184
189
|
authState = {type: AuthStateType.LOGGING_IN, isExchangingToken: false}
|
|
185
190
|
// Note: dashboardContext from the callback URL can be set later in handleAuthCallback too
|
|
186
|
-
} else if (token && !isInDashboard) {
|
|
187
|
-
// Only use token from storage if NOT running in dashboard
|
|
191
|
+
} else if (token && !isInDashboard && !studioModeEnabled) {
|
|
192
|
+
// Only use token from storage if NOT running in dashboard and studio mode is not enabled
|
|
188
193
|
authState = {type: AuthStateType.LOGGED_IN, token, currentUser: null}
|
|
189
194
|
} else {
|
|
190
195
|
// Default to logged out if no provided token, not handling callback,
|
|
@@ -212,11 +217,37 @@ export const authStore = defineStore<AuthStoreState>({
|
|
|
212
217
|
initialize(context) {
|
|
213
218
|
const subscriptions: Subscription[] = []
|
|
214
219
|
subscriptions.push(subscribeToStateAndFetchCurrentUser(context))
|
|
215
|
-
|
|
216
|
-
if (
|
|
220
|
+
const storageArea = context.state.get().options?.storageArea
|
|
221
|
+
if (storageArea) {
|
|
217
222
|
subscriptions.push(subscribeToStorageEventsAndSetToken(context))
|
|
218
223
|
}
|
|
219
224
|
|
|
225
|
+
// If in Studio mode with no local token, resolve cookie auth asynchronously
|
|
226
|
+
try {
|
|
227
|
+
const {instance, state} = context
|
|
228
|
+
const studioModeEnabled = !!instance.config.studioMode?.enabled
|
|
229
|
+
const token: string | null =
|
|
230
|
+
state.get().authState?.type === AuthStateType.LOGGED_IN
|
|
231
|
+
? (state.get().authState as LoggedInAuthState).token
|
|
232
|
+
: null
|
|
233
|
+
if (studioModeEnabled && !token) {
|
|
234
|
+
const projectId = instance.config.projectId
|
|
235
|
+
const clientFactory = state.get().options.clientFactory
|
|
236
|
+
checkForCookieAuth(projectId, clientFactory).then((isCookieAuthEnabled) => {
|
|
237
|
+
if (!isCookieAuthEnabled) return
|
|
238
|
+
state.set('enableCookieAuth', (prev) => ({
|
|
239
|
+
options: {...prev.options, authMethod: 'cookie'},
|
|
240
|
+
authState:
|
|
241
|
+
prev.authState.type === AuthStateType.LOGGED_IN
|
|
242
|
+
? prev.authState
|
|
243
|
+
: {type: AuthStateType.LOGGED_IN, token: '', currentUser: null},
|
|
244
|
+
}))
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
// best-effort cookie detection
|
|
249
|
+
}
|
|
250
|
+
|
|
220
251
|
if (!tokenRefresherRunning) {
|
|
221
252
|
tokenRefresherRunning = true
|
|
222
253
|
subscriptions.push(refreshStampedToken(context))
|
|
@@ -96,7 +96,7 @@ describe('getStudioTokenFromLocalStorage', () => {
|
|
|
96
96
|
expect(getTokenFromStorageSpy).not.toHaveBeenCalled()
|
|
97
97
|
})
|
|
98
98
|
|
|
99
|
-
it('should return null if
|
|
99
|
+
it('should return null if storageKey is undefined', () => {
|
|
100
100
|
const result = getStudioTokenFromLocalStorage(storageArea, undefined)
|
|
101
101
|
expect(result).toBeNull()
|
|
102
102
|
expect(getTokenFromStorageSpy).not.toHaveBeenCalled()
|
|
@@ -104,19 +104,19 @@ describe('getStudioTokenFromLocalStorage', () => {
|
|
|
104
104
|
|
|
105
105
|
it('should call getTokenFromStorage with correct key', () => {
|
|
106
106
|
getTokenFromStorageSpy.mockReturnValue(null) // Assume token not found for this test
|
|
107
|
-
getStudioTokenFromLocalStorage(storageArea,
|
|
107
|
+
getStudioTokenFromLocalStorage(storageArea, studioStorageKey)
|
|
108
108
|
expect(getTokenFromStorageSpy).toHaveBeenCalledWith(storageArea, studioStorageKey)
|
|
109
109
|
})
|
|
110
110
|
|
|
111
111
|
it('should return the token if found in storage', () => {
|
|
112
112
|
getTokenFromStorageSpy.mockReturnValue(mockToken)
|
|
113
|
-
const result = getStudioTokenFromLocalStorage(storageArea,
|
|
113
|
+
const result = getStudioTokenFromLocalStorage(storageArea, studioStorageKey)
|
|
114
114
|
expect(result).toBe(mockToken)
|
|
115
115
|
})
|
|
116
116
|
|
|
117
117
|
it('should return null if token is not found in storage', () => {
|
|
118
118
|
getTokenFromStorageSpy.mockReturnValue(null)
|
|
119
|
-
const result = getStudioTokenFromLocalStorage(storageArea,
|
|
119
|
+
const result = getStudioTokenFromLocalStorage(storageArea, studioStorageKey)
|
|
120
120
|
expect(result).toBeNull()
|
|
121
121
|
})
|
|
122
122
|
})
|
|
@@ -33,17 +33,16 @@ export async function checkForCookieAuth(
|
|
|
33
33
|
/**
|
|
34
34
|
* Attempts to retrieve a studio token from local storage.
|
|
35
35
|
* @param storageArea - The storage area to retrieve the token from.
|
|
36
|
-
* @param
|
|
36
|
+
* @param storageKey - The storage key to retrieve the token from.
|
|
37
37
|
* @returns The studio token or null if it does not exist.
|
|
38
38
|
* @internal
|
|
39
39
|
*/
|
|
40
40
|
export function getStudioTokenFromLocalStorage(
|
|
41
41
|
storageArea: Storage | undefined,
|
|
42
|
-
|
|
42
|
+
storageKey: string | undefined,
|
|
43
43
|
): string | null {
|
|
44
|
-
if (!storageArea || !
|
|
45
|
-
const
|
|
46
|
-
const token = getTokenFromStorage(storageArea, studioStorageKey)
|
|
44
|
+
if (!storageArea || !storageKey) return null
|
|
45
|
+
const token = getTokenFromStorage(storageArea, storageKey)
|
|
47
46
|
if (token) {
|
|
48
47
|
return token
|
|
49
48
|
}
|
|
@@ -4,32 +4,43 @@ import {distinctUntilChanged, filter, map, type Subscription, switchMap} from 'r
|
|
|
4
4
|
import {type StoreContext} from '../store/defineStore'
|
|
5
5
|
import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
|
|
6
6
|
import {AuthStateType} from './authStateType'
|
|
7
|
-
import {type AuthState, type AuthStoreState} from './authStore'
|
|
7
|
+
import {type AuthMethodOptions, type AuthState, type AuthStoreState} from './authStore'
|
|
8
8
|
|
|
9
9
|
export const subscribeToStateAndFetchCurrentUser = ({
|
|
10
10
|
state,
|
|
11
|
+
instance,
|
|
11
12
|
}: StoreContext<AuthStoreState>): Subscription => {
|
|
12
13
|
const {clientFactory, apiHost} = state.get().options
|
|
14
|
+
const useProjectHostname = !!instance.config.studioMode?.enabled
|
|
15
|
+
const projectId = instance.config.projectId
|
|
13
16
|
|
|
14
17
|
const currentUser$ = state.observable
|
|
15
18
|
.pipe(
|
|
16
|
-
map(({authState}) => authState),
|
|
19
|
+
map(({authState, options}) => ({authState, authMethod: options.authMethod})),
|
|
17
20
|
filter(
|
|
18
|
-
(
|
|
19
|
-
|
|
21
|
+
(
|
|
22
|
+
value,
|
|
23
|
+
): value is {
|
|
24
|
+
authState: Extract<AuthState, {type: AuthStateType.LOGGED_IN}>
|
|
25
|
+
authMethod: AuthMethodOptions
|
|
26
|
+
} => value.authState.type === AuthStateType.LOGGED_IN && !value.authState.currentUser,
|
|
27
|
+
),
|
|
28
|
+
map((value) => ({token: value.authState.token, authMethod: value.authMethod})),
|
|
29
|
+
distinctUntilChanged(
|
|
30
|
+
(prev, curr) => prev.token === curr.token && prev.authMethod === curr.authMethod,
|
|
20
31
|
),
|
|
21
|
-
map((authState) => authState.token),
|
|
22
|
-
distinctUntilChanged(),
|
|
23
32
|
)
|
|
24
33
|
.pipe(
|
|
25
|
-
map((token) =>
|
|
34
|
+
map(({token, authMethod}) =>
|
|
26
35
|
clientFactory({
|
|
27
36
|
apiVersion: DEFAULT_API_VERSION,
|
|
28
37
|
requestTagPrefix: REQUEST_TAG_PREFIX,
|
|
29
|
-
token,
|
|
38
|
+
token: authMethod === 'cookie' ? undefined : token,
|
|
30
39
|
ignoreBrowserTokenWarning: true,
|
|
31
|
-
useProjectHostname
|
|
40
|
+
useProjectHostname,
|
|
32
41
|
useCdn: false,
|
|
42
|
+
...(authMethod === 'cookie' ? {withCredentials: true} : {}),
|
|
43
|
+
...(useProjectHostname && projectId ? {projectId} : {}),
|
|
33
44
|
...(apiHost && {apiHost}),
|
|
34
45
|
}),
|
|
35
46
|
),
|
package/src/auth/utils.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
|
4
4
|
import {AUTH_CODE_PARAM, DEFAULT_BASE} from './authConstants'
|
|
5
5
|
import {
|
|
6
6
|
getAuthCode,
|
|
7
|
+
getCleanedUrl,
|
|
7
8
|
getDefaultLocation,
|
|
8
9
|
getDefaultStorage,
|
|
9
10
|
getStorageEvents,
|
|
@@ -230,3 +231,36 @@ describe('getTokenFromLocation', () => {
|
|
|
230
231
|
expect(result).toBe(testToken)
|
|
231
232
|
})
|
|
232
233
|
})
|
|
234
|
+
|
|
235
|
+
describe('getCleanedUrl', () => {
|
|
236
|
+
it('removes only token from hash when it is the only param', () => {
|
|
237
|
+
const url = 'http://example.com/page#token=abc'
|
|
238
|
+
const cleaned = getCleanedUrl(url)
|
|
239
|
+
expect(cleaned).toBe('http://example.com/page')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('removes only token from hash and preserves other hash params', () => {
|
|
243
|
+
const url = 'http://example.com/page#token=abc&foo=bar'
|
|
244
|
+
const cleaned = getCleanedUrl(url)
|
|
245
|
+
expect(cleaned).toBe('http://example.com/page#foo=bar')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('removes token when it appears among multiple hash params', () => {
|
|
249
|
+
const url = 'http://example.com/page#foo=bar&token=abc&baz=qux'
|
|
250
|
+
const cleaned = getCleanedUrl(url)
|
|
251
|
+
expect(cleaned).toBe('http://example.com/page#foo=bar&baz=qux')
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('removes sid and url from query string while preserving others', () => {
|
|
255
|
+
const url =
|
|
256
|
+
'http://example.com/callback?sid=s1&url=https%3A%2F%2Freturn.example%2Fdone&x=1#token=abc'
|
|
257
|
+
const cleaned = getCleanedUrl(url)
|
|
258
|
+
expect(cleaned).toBe('http://example.com/callback?x=1')
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('preserves non key-value hash fragments', () => {
|
|
262
|
+
const url = 'http://example.com/page#section'
|
|
263
|
+
const cleaned = getCleanedUrl(url)
|
|
264
|
+
expect(cleaned).toBe('http://example.com/page#section')
|
|
265
|
+
})
|
|
266
|
+
})
|
package/src/auth/utils.ts
CHANGED
|
@@ -116,12 +116,20 @@ export function getDefaultLocation(): string {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
* Cleans up the URL by removing the hash and the sid and url
|
|
119
|
+
* Cleans up the URL by removing the `token` from the hash and the `sid` and `url` search params.
|
|
120
120
|
* @internal
|
|
121
121
|
*/
|
|
122
122
|
export function getCleanedUrl(locationUrl: string): string {
|
|
123
123
|
const loc = new URL(locationUrl)
|
|
124
|
-
|
|
124
|
+
// Remove only the `token` param from the hash while preserving other fragments
|
|
125
|
+
const rawHash = loc.hash.startsWith('#') ? loc.hash.slice(1) : loc.hash
|
|
126
|
+
if (rawHash && rawHash.includes('=')) {
|
|
127
|
+
const hashParams = new URLSearchParams(rawHash)
|
|
128
|
+
hashParams.delete('token')
|
|
129
|
+
hashParams.delete('withSid')
|
|
130
|
+
const nextHash = hashParams.toString()
|
|
131
|
+
loc.hash = nextHash ? `#${nextHash}` : ''
|
|
132
|
+
}
|
|
125
133
|
loc.searchParams.delete('sid')
|
|
126
134
|
loc.searchParams.delete('url')
|
|
127
135
|
return loc.toString()
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import {type SanityDocument} from '@sanity/types'
|
|
2
|
-
import {type ExprNode} from 'groq-js'
|
|
2
|
+
import {evaluateSync, type ExprNode, parse} from 'groq-js'
|
|
3
3
|
import {describe, expect, it} from 'vitest'
|
|
4
4
|
|
|
5
5
|
import {createSanityInstance} from '../store/createSanityInstance'
|
|
6
6
|
import {getDraftId, getPublishedId} from '../utils/ids'
|
|
7
|
-
import {evaluateSync, parse} from './_synchronous-groq-js.mjs'
|
|
8
7
|
import {type DocumentAction} from './actions'
|
|
9
8
|
import {calculatePermissions, createGrantsLookup, type DatasetAcl, type Grant} from './permissions'
|
|
10
9
|
import {type SyncTransactionState} from './reducers'
|
|
@@ -50,7 +49,7 @@ describe('createGrantsLookup', () => {
|
|
|
50
49
|
;(['read', 'update', 'create', 'history'] as Grant[]).forEach((key) => {
|
|
51
50
|
expect(grants[key]).toBeDefined()
|
|
52
51
|
// Evaluate the expression for the dummy document.
|
|
53
|
-
expect(evaluateSync(grants[key], {params: {document: dummyDoc}}).
|
|
52
|
+
expect(evaluateSync(grants[key], {params: {document: dummyDoc}}).data).toBe(true)
|
|
54
53
|
})
|
|
55
54
|
})
|
|
56
55
|
})
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import {type SanityDocument} from '@sanity/types'
|
|
2
|
-
import {type ExprNode} from 'groq-js'
|
|
2
|
+
import {evaluateSync, type ExprNode, parse} from 'groq-js'
|
|
3
3
|
import {createSelector} from 'reselect'
|
|
4
4
|
|
|
5
5
|
import {type SelectorContext} from '../store/createStateSourceAction'
|
|
6
6
|
import {getDraftId, getPublishedId} from '../utils/ids'
|
|
7
7
|
import {MultiKeyWeakMap} from '../utils/MultiKeyWeakMap'
|
|
8
|
-
import {evaluateSync, parse} from './_synchronous-groq-js.mjs'
|
|
9
8
|
import {type DocumentAction} from './actions'
|
|
10
9
|
import {ActionError, PermissionActionError, processActions} from './processActions'
|
|
11
10
|
import {type DocumentSet} from './processMutations'
|
|
@@ -127,7 +126,8 @@ const memoizedActionsSelector = createSelector(
|
|
|
127
126
|
)
|
|
128
127
|
|
|
129
128
|
function checkGrant(grantExpr: ExprNode, document: SanityDocument): boolean {
|
|
130
|
-
|
|
129
|
+
const value = evaluateSync(grantExpr, {params: {document}})
|
|
130
|
+
return value.type === 'boolean' && value.data
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
/** @beta */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {type Reference, type SanityDocument} from '@sanity/types'
|
|
2
|
+
import {parse} from 'groq-js'
|
|
2
3
|
import {describe, expect, it} from 'vitest'
|
|
3
4
|
|
|
4
|
-
import {parse} from './_synchronous-groq-js.mjs'
|
|
5
5
|
import {type DocumentAction} from './actions'
|
|
6
6
|
import {ActionError, processActions} from './processActions'
|
|
7
7
|
import {type DocumentSet} from './processMutations'
|
|
@@ -5,18 +5,18 @@ import {
|
|
|
5
5
|
type Reference,
|
|
6
6
|
type SanityDocument,
|
|
7
7
|
} from '@sanity/types'
|
|
8
|
-
import {type ExprNode} from 'groq-js'
|
|
8
|
+
import {evaluateSync, type ExprNode} from 'groq-js'
|
|
9
9
|
import {isEqual} from 'lodash-es'
|
|
10
10
|
|
|
11
11
|
import {getDraftId, getPublishedId} from '../utils/ids'
|
|
12
|
-
import {evaluateSync} from './_synchronous-groq-js.mjs'
|
|
13
12
|
import {type DocumentAction} from './actions'
|
|
14
13
|
import {type Grant} from './permissions'
|
|
15
14
|
import {type DocumentSet, getId, processMutations} from './processMutations'
|
|
16
15
|
import {type HttpAction} from './reducers'
|
|
17
16
|
|
|
18
17
|
function checkGrant(grantExpr: ExprNode, document: SanityDocument): boolean {
|
|
19
|
-
|
|
18
|
+
const value = evaluateSync(grantExpr, {params: {document}})
|
|
19
|
+
return value.type === 'boolean' && value.data
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
interface ProcessActionsOptions {
|
|
@@ -12,11 +12,11 @@ import {
|
|
|
12
12
|
import {hashString} from '../utils/hashString'
|
|
13
13
|
import {getPublishedId, insecureRandomId} from '../utils/ids'
|
|
14
14
|
import {projectionStore} from './projectionStore'
|
|
15
|
-
import {type ProjectionStoreState, type ProjectionValuePending
|
|
15
|
+
import {type ProjectionStoreState, type ProjectionValuePending} from './types'
|
|
16
16
|
import {PROJECTION_STATE_CLEAR_DELAY, STABLE_EMPTY_PROJECTION, validateProjection} from './util'
|
|
17
17
|
|
|
18
18
|
export interface ProjectionOptions<
|
|
19
|
-
TProjection extends
|
|
19
|
+
TProjection extends string = string,
|
|
20
20
|
TDocumentType extends string = string,
|
|
21
21
|
TDataset extends string = string,
|
|
22
22
|
TProjectId extends string = string,
|
|
@@ -28,7 +28,7 @@ export interface ProjectionOptions<
|
|
|
28
28
|
* @beta
|
|
29
29
|
*/
|
|
30
30
|
export function getProjectionState<
|
|
31
|
-
TProjection extends
|
|
31
|
+
TProjection extends string = string,
|
|
32
32
|
TDocumentType extends string = string,
|
|
33
33
|
TDataset extends string = string,
|
|
34
34
|
TProjectId extends string = string,
|
|
@@ -75,13 +75,13 @@ export const _getProjectionState = bindActionByDataset(
|
|
|
75
75
|
createStateSourceAction({
|
|
76
76
|
selector: (
|
|
77
77
|
{state}: SelectorContext<ProjectionStoreState>,
|
|
78
|
-
options: ProjectionOptions<
|
|
78
|
+
options: ProjectionOptions<string, string, string, string>,
|
|
79
79
|
): ProjectionValuePending<object> | undefined => {
|
|
80
80
|
const documentId = getPublishedId(options.documentId)
|
|
81
81
|
const projectionHash = hashString(options.projection)
|
|
82
82
|
return state.values[documentId]?.[projectionHash] ?? STABLE_EMPTY_PROJECTION
|
|
83
83
|
},
|
|
84
|
-
onSubscribe: ({state}, options: ProjectionOptions<
|
|
84
|
+
onSubscribe: ({state}, options: ProjectionOptions<string, string, string, string>) => {
|
|
85
85
|
const {projection, ...docHandle} = options
|
|
86
86
|
const subscriptionId = insecureRandomId()
|
|
87
87
|
const documentId = getPublishedId(docHandle.documentId)
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import {describe, expect, it} from 'vitest'
|
|
2
2
|
|
|
3
3
|
import {createProjectionQuery, processProjectionQuery} from './projectionQuery'
|
|
4
|
-
import {type ValidProjection} from './types'
|
|
5
4
|
|
|
6
5
|
describe('createProjectionQuery', () => {
|
|
7
6
|
it('creates a query and params for given ids and projections', () => {
|
|
8
7
|
const ids = new Set(['doc1', 'doc2'])
|
|
9
|
-
const projectionHash
|
|
8
|
+
const projectionHash = '{title, description}'
|
|
10
9
|
const documentProjections = {
|
|
11
10
|
doc1: {[projectionHash]: projectionHash},
|
|
12
11
|
doc2: {[projectionHash]: projectionHash},
|
|
@@ -21,8 +20,8 @@ describe('createProjectionQuery', () => {
|
|
|
21
20
|
|
|
22
21
|
it('handles multiple different projections', () => {
|
|
23
22
|
const ids = new Set(['doc1', 'doc2'])
|
|
24
|
-
const projectionHash1
|
|
25
|
-
const projectionHash2
|
|
23
|
+
const projectionHash1 = '{title, description}'
|
|
24
|
+
const projectionHash2 = '{name, age}'
|
|
26
25
|
const documentProjections = {
|
|
27
26
|
doc1: {[projectionHash1]: projectionHash1},
|
|
28
27
|
doc2: {[projectionHash2]: projectionHash2},
|
|
@@ -39,9 +38,9 @@ describe('createProjectionQuery', () => {
|
|
|
39
38
|
|
|
40
39
|
it('filters out ids without projections', () => {
|
|
41
40
|
const ids = new Set(['doc1', 'doc2', 'doc3'])
|
|
42
|
-
const projectionHash1
|
|
41
|
+
const projectionHash1 = '{title}'
|
|
43
42
|
// projectionHash2 missing intentionally
|
|
44
|
-
const projectionHash3
|
|
43
|
+
const projectionHash3 = '{name}'
|
|
45
44
|
|
|
46
45
|
const documentProjections = {
|
|
47
46
|
doc1: {[projectionHash1]: projectionHash1},
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import {getDraftId, getPublishedId} from '../utils/ids'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
type DocumentProjectionValues,
|
|
5
|
-
type ValidProjection,
|
|
6
|
-
} from './types'
|
|
2
|
+
import {type DocumentProjections, type DocumentProjectionValues} from './types'
|
|
3
|
+
import {validateProjection} from './util'
|
|
7
4
|
|
|
8
5
|
export type ProjectionQueryResult = {
|
|
9
6
|
_id: string
|
|
@@ -18,7 +15,7 @@ interface CreateProjectionQueryResult {
|
|
|
18
15
|
params: Record<string, unknown>
|
|
19
16
|
}
|
|
20
17
|
|
|
21
|
-
type ProjectionMap = Record<string, {projection:
|
|
18
|
+
type ProjectionMap = Record<string, {projection: string; documentIds: Set<string>}>
|
|
22
19
|
|
|
23
20
|
export function createProjectionQuery(
|
|
24
21
|
documentIds: Set<string>,
|
|
@@ -31,7 +28,7 @@ export function createProjectionQuery(
|
|
|
31
28
|
|
|
32
29
|
return Object.entries(projectionsForDoc).map(([projectionHash, projection]) => ({
|
|
33
30
|
documentId: id,
|
|
34
|
-
projection,
|
|
31
|
+
projection: validateProjection(projection),
|
|
35
32
|
projectionHash,
|
|
36
33
|
}))
|
|
37
34
|
})
|
|
@@ -6,7 +6,7 @@ import {createSanityInstance, type SanityInstance} from '../store/createSanityIn
|
|
|
6
6
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
7
7
|
import {getProjectionState} from './getProjectionState'
|
|
8
8
|
import {resolveProjection} from './resolveProjection'
|
|
9
|
-
import {type ProjectionValuePending
|
|
9
|
+
import {type ProjectionValuePending} from './types'
|
|
10
10
|
|
|
11
11
|
vi.mock('./getProjectionState')
|
|
12
12
|
|
|
@@ -35,7 +35,7 @@ describe('resolveProjection', () => {
|
|
|
35
35
|
documentId: 'doc123',
|
|
36
36
|
documentType: 'movie',
|
|
37
37
|
})
|
|
38
|
-
const projection = '{title}'
|
|
38
|
+
const projection = '{title}'
|
|
39
39
|
|
|
40
40
|
const result = await resolveProjection(instance, {...docHandle, projection})
|
|
41
41
|
|
|
@@ -5,11 +5,11 @@ import {bindActionByDataset} from '../store/createActionBinder'
|
|
|
5
5
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
6
6
|
import {getProjectionState, type ProjectionOptions} from './getProjectionState'
|
|
7
7
|
import {projectionStore} from './projectionStore'
|
|
8
|
-
import {type ProjectionValuePending
|
|
8
|
+
import {type ProjectionValuePending} from './types'
|
|
9
9
|
|
|
10
10
|
/** @beta */
|
|
11
11
|
export function resolveProjection<
|
|
12
|
-
TProjection extends
|
|
12
|
+
TProjection extends string = string,
|
|
13
13
|
TDocumentType extends string = string,
|
|
14
14
|
TDataset extends string = string,
|
|
15
15
|
TProjectId extends string = string,
|