@sanity/sdk 3.1.0 → 3.3.0-rc.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/_exports/_internal.d.ts +52 -21
- package/dist/_exports/_internal.d.ts.map +1 -1
- package/dist/_exports/_internal.js +10 -9
- package/dist/_exports/_internal.js.map +1 -1
- package/dist/_exports/agent.d.ts +7 -7
- package/dist/_exports/agent.d.ts.map +1 -1
- package/dist/_exports/agent.js +1 -1
- package/dist/_exports/comlink.d.ts +9 -9
- package/dist/_exports/comlink.d.ts.map +1 -1
- package/dist/_exports/comlink.js +3 -2
- package/dist/_exports/comlink.js.map +1 -1
- package/dist/_exports/dashboard.d.ts +2 -0
- package/dist/_exports/dashboard.js +2 -0
- package/dist/{authStore-C-OEkGaM.d.ts → authStore-BGoEGxOq.d.ts} +531 -63
- package/dist/authStore-BGoEGxOq.d.ts.map +1 -0
- package/dist/{clientStore-gUALQ0qW.js → clientStore-QDrJNTM4.js} +128 -7
- package/dist/clientStore-QDrJNTM4.js.map +1 -0
- package/dist/{createSanityInstance-DCePpBYW.d.ts → createSanityInstance-BQIqyGLX.d.ts} +16 -1
- package/dist/createSanityInstance-BQIqyGLX.d.ts.map +1 -0
- package/dist/{object-BwynGPRZ.js → createStateSourceAction-DdAhEfnZ.js} +2 -82
- package/dist/createStateSourceAction-DdAhEfnZ.js.map +1 -0
- package/dist/defineProperty-Bho9Ngsi.js +31 -0
- package/dist/{getNodeState-E0hjqFOo.js → getNodeState-B4nG0sjm.js} +3 -2
- package/dist/{getNodeState-E0hjqFOo.js.map → getNodeState-B4nG0sjm.js.map} +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +371 -54
- package/dist/index.js.map +1 -1
- package/dist/object-CxXlSVg7.js +83 -0
- package/dist/object-CxXlSVg7.js.map +1 -0
- package/dist/topicStore-Ct7IFNtY.js +565 -0
- package/dist/topicStore-Ct7IFNtY.js.map +1 -0
- package/dist/topicStore-DXvuH9h4.d.ts +435 -0
- package/dist/topicStore-DXvuH9h4.d.ts.map +1 -0
- package/dist/{version-C90rNWaX.js → version-CIhX5ppJ.js} +6 -6
- package/dist/{version-C90rNWaX.js.map → version-CIhX5ppJ.js.map} +1 -1
- package/package.json +20 -19
- package/src/_exports/_internal.ts +12 -0
- package/src/_exports/dashboard.ts +36 -0
- package/src/_exports/index.ts +17 -1
- package/src/applications/applications.test-d.ts +28 -4
- package/src/applications/applications.ts +39 -14
- package/src/auth/authMode.test.ts +24 -0
- package/src/auth/authMode.ts +8 -3
- package/src/auth/authStore.test.ts +18 -0
- package/src/auth/authStore.ts +15 -0
- package/src/auth/authStrategy.ts +2 -0
- package/src/auth/handleAuthCallback.test.ts +33 -0
- package/src/auth/handleAuthCallback.ts +11 -1
- package/src/auth/logout.test.ts +53 -0
- package/src/auth/logout.ts +19 -1
- package/src/auth/oauth/oauthActions.test.ts +593 -0
- package/src/auth/oauth/oauthActions.ts +438 -0
- package/src/auth/oauth/oauthAuth.test.ts +229 -0
- package/src/auth/oauth/oauthAuth.ts +178 -0
- package/src/auth/oauth/pkce.test.ts +44 -0
- package/src/auth/oauth/pkce.ts +60 -0
- package/src/auth/oauth/types.ts +12 -0
- package/src/config/authConfig.ts +18 -0
- package/src/dashboard/messageBus/__fixtures__/test-topics.ts +18 -0
- package/src/dashboard/messageBus/bus.test.ts +1033 -0
- package/src/dashboard/messageBus/bus.ts +1246 -0
- package/src/dashboard/messageBus/store.test.ts +142 -0
- package/src/dashboard/messageBus/store.ts +74 -0
- package/src/dashboard/messageBus/topicStore.test.ts +172 -0
- package/src/dashboard/messageBus/topicStore.ts +184 -0
- package/src/dashboard/messageBus/topics.test.ts +71 -0
- package/src/dashboard/messageBus/topics.ts +260 -0
- package/src/document/actions.ts +3 -3
- package/src/document/applyDocumentActions.ts +3 -3
- package/src/document/documentStore.ts +12 -12
- package/src/document/processMutations.ts +58 -22
- package/src/installations/installations.test-d.ts +3 -3
- package/src/installations/installations.ts +2 -19
- package/src/projection/getProjectionState.ts +2 -2
- package/src/projection/resolveProjection.ts +2 -2
- package/src/query/queryStore.ts +3 -3
- package/src/typegen/defineProjection.test.ts +17 -0
- package/src/typegen/defineProjection.ts +19 -0
- package/src/typegen/groqCompat.ts +122 -0
- package/src/typegen/resolve.ts +37 -0
- package/src/utils/createGroqSearchFilter.test.ts +8 -0
- package/src/utils/createGroqSearchFilter.ts +6 -3
- package/dist/authStore-C-OEkGaM.d.ts.map +0 -1
- package/dist/clientStore-gUALQ0qW.js.map +0 -1
- package/dist/createSanityInstance-DCePpBYW.d.ts.map +0 -1
- package/dist/object-BwynGPRZ.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0-rc.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK",
|
|
6
6
|
"keywords": [
|
|
@@ -37,46 +37,47 @@
|
|
|
37
37
|
"./package.json": "./package.json",
|
|
38
38
|
"./_internal": "./dist/_exports/_internal.js",
|
|
39
39
|
"./agent": "./dist/_exports/agent.js",
|
|
40
|
-
"./comlink": "./dist/_exports/comlink.js"
|
|
40
|
+
"./comlink": "./dist/_exports/comlink.js",
|
|
41
|
+
"./dashboard": "./dist/_exports/dashboard.js"
|
|
41
42
|
},
|
|
42
43
|
"publishConfig": {
|
|
43
44
|
"access": "public"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
47
|
"@sanity/bifur-client": "^1.0.0",
|
|
47
|
-
"@sanity/client": "^8.
|
|
48
|
+
"@sanity/client": "^8.6.1",
|
|
48
49
|
"@sanity/comlink": "^4.0.3",
|
|
49
50
|
"@sanity/diff-match-patch": "^3.2.0",
|
|
50
51
|
"@sanity/diff-patch": "^6.0.0",
|
|
51
|
-
"@sanity/id-utils": "^1.
|
|
52
|
+
"@sanity/id-utils": "^1.1.0",
|
|
52
53
|
"@sanity/image-url": "^2.1.1",
|
|
53
54
|
"@sanity/json-match": "^1.0.5",
|
|
54
55
|
"@sanity/message-protocol": "^0.24.0",
|
|
55
|
-
"@sanity/mutate": "^0.18.
|
|
56
|
+
"@sanity/mutate": "^0.18.2",
|
|
56
57
|
"@sanity/telemetry": "^1.1.0",
|
|
57
|
-
"@sanity/types": "^6.
|
|
58
|
-
"groq": "
|
|
58
|
+
"@sanity/types": "^6.13.2",
|
|
59
|
+
"groq": "^6.13.2",
|
|
59
60
|
"groq-js": "^2.0.0",
|
|
60
61
|
"reselect": "^5.1.1",
|
|
61
62
|
"rxjs": "^7.8.2",
|
|
62
|
-
"zustand": "^5.0.
|
|
63
|
+
"zustand": "^5.0.15"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
|
-
"@sanity/browserslist-config": "^1.0.5",
|
|
66
|
-
"@sanity/pkg-utils": "^12.1.0",
|
|
67
|
-
"@types/node": "^24.13.3",
|
|
68
|
-
"@vitest/coverage-v8": "^4.1.10",
|
|
69
|
-
"eslint": "^10.6.0",
|
|
70
|
-
"oxfmt": "^0.58.0",
|
|
71
|
-
"rolldown": "^1.2.3",
|
|
72
|
-
"typescript": "^6.0.3",
|
|
73
|
-
"vite": "^8.1.5",
|
|
74
|
-
"vitest": "^4.1.10",
|
|
75
66
|
"@repo/config-eslint": "0.0.0",
|
|
76
67
|
"@repo/config-test": "0.0.1",
|
|
77
68
|
"@repo/package.bundle": "3.82.0",
|
|
78
69
|
"@repo/package.config": "0.0.1",
|
|
79
|
-
"@repo/tsconfig": "0.0.1"
|
|
70
|
+
"@repo/tsconfig": "0.0.1",
|
|
71
|
+
"@sanity/browserslist-config": "^1.0.5",
|
|
72
|
+
"@sanity/pkg-utils": "^13.0.0",
|
|
73
|
+
"@types/node": "^24.13.4",
|
|
74
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
75
|
+
"eslint": "^10.10.0",
|
|
76
|
+
"oxfmt": "^0.67.0",
|
|
77
|
+
"rolldown": "^1.2.8",
|
|
78
|
+
"typescript": "^6.0.3",
|
|
79
|
+
"vite": "^8.3.0",
|
|
80
|
+
"vitest": "^5.0.0"
|
|
80
81
|
},
|
|
81
82
|
"browserslist": "extends @sanity/browserslist-config",
|
|
82
83
|
"scripts": {
|
|
@@ -7,6 +7,17 @@ export {
|
|
|
7
7
|
isProjectUserNotFoundClientError,
|
|
8
8
|
} from '../auth/utils'
|
|
9
9
|
export {getCommentsOptionsKey, parseCommentsOptionsKey} from '../comments/commentsStore' // only used for memoizing in React, not needed for actual functionality
|
|
10
|
+
export {
|
|
11
|
+
installMessageBus,
|
|
12
|
+
type InstallMessageBusOptions,
|
|
13
|
+
resetMessageBus,
|
|
14
|
+
} from '../dashboard/messageBus/bus'
|
|
15
|
+
export {
|
|
16
|
+
getDashboardMessageBus,
|
|
17
|
+
isDashboardEnvironment,
|
|
18
|
+
requireDashboardMessageBus,
|
|
19
|
+
} from '../dashboard/messageBus/store'
|
|
20
|
+
export {getTopicState, resolveTopic} from '../dashboard/messageBus/topicStore'
|
|
10
21
|
export {PREVIEW_PROJECTION} from '../preview/previewConstants'
|
|
11
22
|
export {transformProjectionToPreview} from '../preview/previewProjectionUtils'
|
|
12
23
|
export {getQueryKey, parseQueryKey} from '../query/queryStore' // only used for memoizing in React, not needed for actual functionality
|
|
@@ -25,4 +36,5 @@ export {getTelemetryManager, initTelemetry, trackHookMounted} from '../telemetry
|
|
|
25
36
|
export {getUsersKey, parseUsersKey} from '../users/reducers' // only used for memoizing in React, not needed for actual functionality
|
|
26
37
|
export {createGroqSearchFilter} from '../utils/createGroqSearchFilter'
|
|
27
38
|
export {randomId, randomUuid} from '../utils/ids'
|
|
39
|
+
export {createLogger} from '../utils/logger'
|
|
28
40
|
export {isDeepEqual, pickProperties} from '../utils/object'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
ConnectMessageBusOptions,
|
|
3
|
+
MessageBus,
|
|
4
|
+
MessageBusAbortOptions,
|
|
5
|
+
MessageBusClient,
|
|
6
|
+
MessageBusConnection,
|
|
7
|
+
MessageBusEmitOptions,
|
|
8
|
+
MessageBusEmitResult,
|
|
9
|
+
MessageBusErrorCode,
|
|
10
|
+
MessageBusHost,
|
|
11
|
+
MessageBusMessage,
|
|
12
|
+
MessageBusMeta,
|
|
13
|
+
MessageBusQueryOptions,
|
|
14
|
+
MessageBusStateSource,
|
|
15
|
+
} from '../dashboard/messageBus/bus'
|
|
16
|
+
export {connectMessageBus} from '../dashboard/messageBus/bus'
|
|
17
|
+
export {MessageBusError} from '../dashboard/messageBus/bus'
|
|
18
|
+
export type {
|
|
19
|
+
ApplicationConfig,
|
|
20
|
+
ApplicationConfigAppType,
|
|
21
|
+
DashboardTopics,
|
|
22
|
+
EventTopic,
|
|
23
|
+
EventTopicDef,
|
|
24
|
+
NavigationLocation,
|
|
25
|
+
NavigationTarget,
|
|
26
|
+
PayloadOf,
|
|
27
|
+
RemoteModuleRef,
|
|
28
|
+
ReplyOf,
|
|
29
|
+
StateTopic,
|
|
30
|
+
StateTopicDef,
|
|
31
|
+
TopicName,
|
|
32
|
+
TopicResult,
|
|
33
|
+
Topics,
|
|
34
|
+
ValueOf,
|
|
35
|
+
} from '../dashboard/messageBus/topics'
|
|
36
|
+
export {type TopicData, TopicError} from '../dashboard/messageBus/topicStore'
|
package/src/_exports/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ export {
|
|
|
20
20
|
type ApplicationDeployment,
|
|
21
21
|
type ApplicationInclude,
|
|
22
22
|
type ApplicationInterface,
|
|
23
|
+
type ApplicationInterfacePlacementMetadata,
|
|
23
24
|
type ApplicationsOptions,
|
|
24
25
|
type ApplicationsResponse,
|
|
25
26
|
type ApplicationStudioConfig,
|
|
@@ -48,6 +49,14 @@ export {
|
|
|
48
49
|
export {observeOrganizationVerificationState} from '../auth/getOrganizationVerificationState'
|
|
49
50
|
export {handleAuthCallback} from '../auth/handleAuthCallback'
|
|
50
51
|
export {logout} from '../auth/logout'
|
|
52
|
+
export {
|
|
53
|
+
getOAuthTokensState,
|
|
54
|
+
handleOAuthCallback,
|
|
55
|
+
refreshOAuthTokens,
|
|
56
|
+
revokeOAuthTokens,
|
|
57
|
+
startOAuthAuthorization,
|
|
58
|
+
} from '../auth/oauth/oauthActions'
|
|
59
|
+
export {type OAuthTokens} from '../auth/oauth/types'
|
|
51
60
|
export type {ClientStoreState as ClientState} from '../client/clientStore'
|
|
52
61
|
export {type ClientOptions, getClient, getClientState} from '../client/clientStore'
|
|
53
62
|
export {
|
|
@@ -181,6 +190,12 @@ export {
|
|
|
181
190
|
export {type JsonMatch} from '../document/patchOperations'
|
|
182
191
|
export {type DocumentPermissionsResult, type PermissionDeniedReason} from '../document/permissions'
|
|
183
192
|
export {getReleaseDocumentId} from '../document/processActions/releaseUtil'
|
|
193
|
+
export {
|
|
194
|
+
type DocumentSet,
|
|
195
|
+
patchOperations,
|
|
196
|
+
processMutations,
|
|
197
|
+
type ProcessMutationsOptions,
|
|
198
|
+
} from '../document/processMutations'
|
|
184
199
|
export type {
|
|
185
200
|
FavoriteDocumentContext,
|
|
186
201
|
FavoriteStatusResponse,
|
|
@@ -194,7 +209,6 @@ export {
|
|
|
194
209
|
type InstallationActiveConfig,
|
|
195
210
|
type InstallationBase,
|
|
196
211
|
type InstallationInclude,
|
|
197
|
-
type InstallationInterface,
|
|
198
212
|
type InstallationsOptions,
|
|
199
213
|
type InstallationsResponse,
|
|
200
214
|
} from '../installations/installations'
|
|
@@ -244,6 +258,8 @@ export type {ReleaseState} from '../releases/releasesStore'
|
|
|
244
258
|
export {getActiveReleasesState, getAllReleasesState} from '../releases/releasesStore'
|
|
245
259
|
export {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
246
260
|
export {type Selector, type StateSource} from '../store/createStateSourceAction'
|
|
261
|
+
export {defineProjection} from '../typegen/defineProjection'
|
|
262
|
+
export type {ResolveDocument, ResolveProjectionResult, ResolveQueryResult} from '../typegen/resolve'
|
|
247
263
|
export {
|
|
248
264
|
type GetUserOptions,
|
|
249
265
|
type GetUsersOptions,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ApplicationDeployment,
|
|
11
11
|
type ApplicationInclude,
|
|
12
12
|
type ApplicationInterface,
|
|
13
|
+
type ApplicationInterfacePlacementMetadata,
|
|
13
14
|
applications,
|
|
14
15
|
type ApplicationsResponse,
|
|
15
16
|
type ApplicationStudioConfig,
|
|
@@ -35,10 +36,30 @@ test('ApplicationBase — carries name and reference as strings', () => {
|
|
|
35
36
|
expectTypeOf<ApplicationBase['reference']>().toEqualTypeOf<string>()
|
|
36
37
|
})
|
|
37
38
|
|
|
38
|
-
test('
|
|
39
|
+
test('ApplicationInterface — matches the interface response', () => {
|
|
40
|
+
expectTypeOf<ApplicationInterface['type']>().toEqualTypeOf<
|
|
41
|
+
'app' | 'worker' | 'asset_source' | 'panel' | 'tile'
|
|
42
|
+
>()
|
|
43
|
+
expectTypeOf<ApplicationInterfacePlacementMetadata>().toEqualTypeOf<{
|
|
44
|
+
dock?: {group?: string; order?: number}
|
|
45
|
+
}>()
|
|
46
|
+
expectTypeOf<
|
|
47
|
+
Extract<ApplicationInterface, {type: 'app'}>['metadata']
|
|
48
|
+
>().toEqualTypeOf<ApplicationInterfacePlacementMetadata | null>()
|
|
39
49
|
expectTypeOf<
|
|
40
|
-
|
|
41
|
-
>().toEqualTypeOf<
|
|
50
|
+
Extract<ApplicationInterface, {type: 'panel'}>['metadata']
|
|
51
|
+
>().toEqualTypeOf<ApplicationInterfacePlacementMetadata | null>()
|
|
52
|
+
expectTypeOf<Extract<ApplicationInterface, {type: 'worker'}>['metadata']>().toEqualTypeOf<null>()
|
|
53
|
+
expectTypeOf<Extract<ApplicationInterface, {type: 'tile'}>['metadata']>().toEqualTypeOf<{
|
|
54
|
+
order?: number
|
|
55
|
+
size: 'small' | 'large' | 'banner'
|
|
56
|
+
}>()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('Application — config.studio adds config with an optional stored value', () => {
|
|
60
|
+
expectTypeOf<Application<'config.studio'>['config']['studio']>().toEqualTypeOf<
|
|
61
|
+
ApplicationStudioConfig | undefined
|
|
62
|
+
>()
|
|
42
63
|
// mfManifest not requested → not present under config
|
|
43
64
|
expectTypeOf<
|
|
44
65
|
Extract<keyof Application<'config.studio'>['config'], 'mfManifest'>
|
|
@@ -51,7 +72,7 @@ test('Application — config.studio adds a required config.studio', () => {
|
|
|
51
72
|
|
|
52
73
|
test('Application — both config tokens merge under a single config', () => {
|
|
53
74
|
type Config = Application<'config.studio' | 'config.mfManifest'>['config']
|
|
54
|
-
expectTypeOf<Config['studio']>().toEqualTypeOf<ApplicationStudioConfig>()
|
|
75
|
+
expectTypeOf<Config['studio']>().toEqualTypeOf<ApplicationStudioConfig | undefined>()
|
|
55
76
|
expectTypeOf<Config['mfManifest']>().toEqualTypeOf<unknown>()
|
|
56
77
|
})
|
|
57
78
|
|
|
@@ -73,6 +94,9 @@ test('Application — a wide include array makes config and activeDeployment opt
|
|
|
73
94
|
type Wide = Application<ApplicationInclude>
|
|
74
95
|
expectTypeOf<KeyModifier<Wide, 'activeDeployment'>>().toEqualTypeOf<'optional'>()
|
|
75
96
|
expectTypeOf<KeyModifier<Wide, 'config'>>().toEqualTypeOf<'optional'>()
|
|
97
|
+
type Config = NonNullable<Wide['config']>
|
|
98
|
+
expectTypeOf<Config['studio']>().toEqualTypeOf<ApplicationStudioConfig | undefined>()
|
|
99
|
+
expectTypeOf<Config['mfManifest']>().toEqualTypeOf<unknown>()
|
|
76
100
|
})
|
|
77
101
|
|
|
78
102
|
test('applications.resolveState — resolves the wide list envelope', () => {
|
|
@@ -25,23 +25,40 @@ export type ApplicationInclude =
|
|
|
25
25
|
| 'config.mfManifest'
|
|
26
26
|
| 'activeDeployment'
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
* An interface exposed by a deployment, embedded when `interfaces` is included.
|
|
30
|
-
*
|
|
31
|
-
* @see https://www.sanity.io/docs/http-reference/applications-api
|
|
32
|
-
* @public
|
|
33
|
-
*/
|
|
34
|
-
export interface ApplicationInterface {
|
|
28
|
+
type ApplicationInterfaceBase = {
|
|
35
29
|
id: string
|
|
36
|
-
type: 'app' | 'worker' | 'asset_source' | 'panel'
|
|
37
30
|
name: string
|
|
38
31
|
title: string
|
|
39
32
|
version: string
|
|
40
33
|
/** Module federation module ID; resolved from the host mf-manifest at runtime */
|
|
41
34
|
moduleId: string
|
|
42
|
-
metadata: {group?: string; priority?: number} | null
|
|
43
35
|
}
|
|
44
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Placement metadata for application and panel interfaces.
|
|
39
|
+
*
|
|
40
|
+
* @see https://www.sanity.io/docs/http-reference/applications-api
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
export type ApplicationInterfacePlacementMetadata = {
|
|
44
|
+
dock?: {group?: string; order?: number}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* An interface exposed by a deployment, embedded when `interfaces` is included.
|
|
49
|
+
*
|
|
50
|
+
* @see https://www.sanity.io/docs/http-reference/applications-api
|
|
51
|
+
* @public
|
|
52
|
+
*/
|
|
53
|
+
export type ApplicationInterface = ApplicationInterfaceBase &
|
|
54
|
+
(
|
|
55
|
+
| {type: 'app'; metadata: ApplicationInterfacePlacementMetadata | null}
|
|
56
|
+
| {type: 'panel'; metadata: ApplicationInterfacePlacementMetadata | null}
|
|
57
|
+
| {type: 'asset_source'; metadata: null}
|
|
58
|
+
| {type: 'worker'; metadata: null}
|
|
59
|
+
| {type: 'tile'; metadata: {order?: number; size: 'small' | 'large' | 'banner'}}
|
|
60
|
+
)
|
|
61
|
+
|
|
45
62
|
/**
|
|
46
63
|
* A studio workspace of a deployment, embedded when `workspaces` is included.
|
|
47
64
|
* Studio applications only.
|
|
@@ -136,16 +153,24 @@ export interface ApplicationBase {
|
|
|
136
153
|
|
|
137
154
|
// Requesting any of these tokens forces the deployment summary into the response.
|
|
138
155
|
type DeploymentToken = 'activeDeployment' | 'interfaces' | 'workspaces' | 'access'
|
|
156
|
+
type ConfigToken = 'config.studio' | 'config.mfManifest'
|
|
139
157
|
|
|
140
|
-
|
|
141
|
-
// gated on its own token.
|
|
142
|
-
type ApplicationConfigShape<Include extends ApplicationInclude> = Included<
|
|
158
|
+
type ApplicationConfig<Include extends ApplicationInclude> = Included<
|
|
143
159
|
ApplicationInclude,
|
|
144
160
|
'config.studio',
|
|
145
161
|
Include,
|
|
146
|
-
{
|
|
162
|
+
{studio?: ApplicationStudioConfig}
|
|
147
163
|
> &
|
|
148
|
-
Included<ApplicationInclude, 'config.mfManifest', Include, {
|
|
164
|
+
Included<ApplicationInclude, 'config.mfManifest', Include, {mfManifest?: unknown}>
|
|
165
|
+
|
|
166
|
+
// Brett returns `config: {}` when a requested config value is not stored.
|
|
167
|
+
type ApplicationConfigShape<Include extends ApplicationInclude> = [ApplicationInclude] extends [
|
|
168
|
+
Include,
|
|
169
|
+
]
|
|
170
|
+
? {config?: ApplicationConfig<Include>}
|
|
171
|
+
: IncludesAny<Include, ConfigToken> extends true
|
|
172
|
+
? {config: ApplicationConfig<Include>}
|
|
173
|
+
: unknown
|
|
149
174
|
|
|
150
175
|
// `activeDeployment` is present when any deployment-related token was requested;
|
|
151
176
|
// a wide include array makes it optional.
|
|
@@ -18,6 +18,30 @@ describe('resolveAuthMode', () => {
|
|
|
18
18
|
it('returns "standalone" by default', () => {
|
|
19
19
|
expect(resolveAuthMode({}, 'https://example.com')).toBe('standalone')
|
|
20
20
|
})
|
|
21
|
+
|
|
22
|
+
it('returns "oauth" when auth.oauth config is provided', () => {
|
|
23
|
+
const config: SanityConfig = {
|
|
24
|
+
auth: {oauth: {clientId: 'c', redirectUri: 'https://app/cb', organizationId: 'o'}},
|
|
25
|
+
}
|
|
26
|
+
expect(resolveAuthMode(config, 'https://example.com')).toBe('oauth')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('prefers "oauth" over dashboard context detection', () => {
|
|
30
|
+
const context = encodeURIComponent(JSON.stringify({orgId: '123'}))
|
|
31
|
+
const href = `https://example.com?_context=${context}`
|
|
32
|
+
const config: SanityConfig = {
|
|
33
|
+
auth: {oauth: {clientId: 'c', redirectUri: 'https://app/cb', organizationId: 'o'}},
|
|
34
|
+
}
|
|
35
|
+
expect(resolveAuthMode(config, href)).toBe('oauth')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('prefers "studio" over oauth config', () => {
|
|
39
|
+
const config: SanityConfig = {
|
|
40
|
+
studio: {},
|
|
41
|
+
auth: {oauth: {clientId: 'c', redirectUri: 'https://app/cb', organizationId: 'o'}},
|
|
42
|
+
}
|
|
43
|
+
expect(resolveAuthMode(config, 'https://example.com')).toBe('studio')
|
|
44
|
+
})
|
|
21
45
|
})
|
|
22
46
|
|
|
23
47
|
describe('isStudioConfig', () => {
|
package/src/auth/authMode.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {DEFAULT_BASE} from './authConstants'
|
|
|
6
6
|
*
|
|
7
7
|
* - `studio` — Running inside Sanity Studio. Token is discovered from
|
|
8
8
|
* Studio's localStorage entry or via cookie auth.
|
|
9
|
+
* - `oauth` — Configured with `auth.oauth`. Authenticates via Sanity's
|
|
10
|
+
* OAuth 2.0 authorization-code + PKCE flow.
|
|
9
11
|
* - `dashboard` — Running inside the Sanity Dashboard iframe. Token is
|
|
10
12
|
* provided by the parent frame via Comlink.
|
|
11
13
|
* - `standalone` — Running as an independent app. Token comes from
|
|
@@ -13,20 +15,23 @@ import {DEFAULT_BASE} from './authConstants'
|
|
|
13
15
|
*
|
|
14
16
|
* @internal
|
|
15
17
|
*/
|
|
16
|
-
type AuthMode = 'studio' | 'dashboard' | 'standalone'
|
|
18
|
+
type AuthMode = 'studio' | 'oauth' | 'dashboard' | 'standalone'
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Determines the auth mode from instance config and environment.
|
|
20
22
|
*
|
|
21
23
|
* Priority:
|
|
22
24
|
* 1. `studio` config provided → `'studio'`
|
|
23
|
-
* 2.
|
|
24
|
-
*
|
|
25
|
+
* 2. `auth.oauth` config provided → `'oauth'` (explicit config wins over
|
|
26
|
+
* environment detection)
|
|
27
|
+
* 3. Dashboard context detected (`_context` URL param with content) → `'dashboard'`
|
|
28
|
+
* 4. Otherwise → `'standalone'`
|
|
25
29
|
*
|
|
26
30
|
* @internal
|
|
27
31
|
*/
|
|
28
32
|
export function resolveAuthMode(config: SanityConfig, locationHref: string): AuthMode {
|
|
29
33
|
if (isStudioConfig(config)) return 'studio'
|
|
34
|
+
if (config.auth?.oauth) return 'oauth'
|
|
30
35
|
if (detectDashboardContext(locationHref)) return 'dashboard'
|
|
31
36
|
return 'standalone'
|
|
32
37
|
}
|
|
@@ -339,6 +339,24 @@ describe('authStore', () => {
|
|
|
339
339
|
expect(options.authMethod).toBe('localstorage')
|
|
340
340
|
})
|
|
341
341
|
|
|
342
|
+
it('only exposes oauth options when the resolved mode is oauth', () => {
|
|
343
|
+
const oauth = {
|
|
344
|
+
clientId: 'client-abc',
|
|
345
|
+
redirectUri: 'https://app.example.com/callback',
|
|
346
|
+
organizationId: 'org123',
|
|
347
|
+
}
|
|
348
|
+
vi.mocked(getStudioTokenFromLocalStorage).mockReturnValue(null)
|
|
349
|
+
vi.mocked(getTokenFromStorage).mockReturnValue(null)
|
|
350
|
+
|
|
351
|
+
// studio takes precedence over oauth, so oauth options must be dropped
|
|
352
|
+
instance = createSanityInstance({projectId: 'p', dataset: 'd', studio: {}, auth: {oauth}})
|
|
353
|
+
expect(authStore.getInitialState(instance, null).options.oauth).toBeUndefined()
|
|
354
|
+
instance.dispose()
|
|
355
|
+
|
|
356
|
+
instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {oauth}})
|
|
357
|
+
expect(authStore.getInitialState(instance, null).options.oauth).toEqual(oauth)
|
|
358
|
+
})
|
|
359
|
+
|
|
342
360
|
it('logs auth initialized in logged out state when no token available', () => {
|
|
343
361
|
instance = createSanityInstance({
|
|
344
362
|
projectId: 'p',
|
package/src/auth/authStore.ts
CHANGED
|
@@ -11,6 +11,8 @@ import {resolveAuthMode} from './authMode'
|
|
|
11
11
|
import {AuthStateType} from './authStateType'
|
|
12
12
|
import {type AuthStrategyOptions} from './authStrategy'
|
|
13
13
|
import {getDashboardInitialState, initializeDashboardAuth} from './dashboardAuth'
|
|
14
|
+
import {getOauthInitialState, initializeOauthAuth} from './oauth/oauthAuth'
|
|
15
|
+
import {type OAuthTokens} from './oauth/types'
|
|
14
16
|
import {getStandaloneInitialState, initializeStandaloneAuth} from './standaloneAuth'
|
|
15
17
|
import {getStudioInitialState, initializeStudioAuth} from './studioAuth'
|
|
16
18
|
import {createLoggedInAuthState, getCleanedUrl, getDefaultLocation} from './utils'
|
|
@@ -91,8 +93,10 @@ export interface AuthStoreState {
|
|
|
91
93
|
callbackUrl: string | undefined
|
|
92
94
|
providedToken: string | undefined
|
|
93
95
|
authMethod: AuthMethodOptions
|
|
96
|
+
oauth: AuthConfig['oauth']
|
|
94
97
|
}
|
|
95
98
|
dashboardContext?: DashboardContext
|
|
99
|
+
oauthTokens?: OAuthTokens
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
// ---------------------------------------------------------------------------
|
|
@@ -155,6 +159,9 @@ export const authStore = defineStore<AuthStoreState>({
|
|
|
155
159
|
case 'studio':
|
|
156
160
|
result = getStudioInitialState(strategyOptions)
|
|
157
161
|
break
|
|
162
|
+
case 'oauth':
|
|
163
|
+
result = getOauthInitialState(strategyOptions)
|
|
164
|
+
break
|
|
158
165
|
case 'dashboard':
|
|
159
166
|
result = getDashboardInitialState(strategyOptions)
|
|
160
167
|
break
|
|
@@ -172,6 +179,7 @@ export const authStore = defineStore<AuthStoreState>({
|
|
|
172
179
|
return {
|
|
173
180
|
authState: result.authState,
|
|
174
181
|
dashboardContext: result.dashboardContext,
|
|
182
|
+
oauthTokens: result.oauthTokens,
|
|
175
183
|
options: {
|
|
176
184
|
apiHost,
|
|
177
185
|
loginUrl: loginUrl.toString(),
|
|
@@ -183,6 +191,10 @@ export const authStore = defineStore<AuthStoreState>({
|
|
|
183
191
|
storageKey: result.storageKey,
|
|
184
192
|
storageArea: result.storageArea,
|
|
185
193
|
authMethod: result.authMethod,
|
|
194
|
+
// Only expose oauth options when the oauth strategy is actually in use,
|
|
195
|
+
// so mode-gated actions (logout, handleAuthCallback) stay consistent
|
|
196
|
+
// with the resolved mode (e.g. `studio` takes precedence over `oauth`).
|
|
197
|
+
oauth: mode === 'oauth' ? authConfig.oauth : undefined,
|
|
186
198
|
},
|
|
187
199
|
}
|
|
188
200
|
},
|
|
@@ -201,6 +213,9 @@ export const authStore = defineStore<AuthStoreState>({
|
|
|
201
213
|
case 'studio':
|
|
202
214
|
initResult = initializeStudioAuth(context, tokenRefresherRunning)
|
|
203
215
|
break
|
|
216
|
+
case 'oauth':
|
|
217
|
+
initResult = initializeOauthAuth(context)
|
|
218
|
+
break
|
|
204
219
|
case 'dashboard':
|
|
205
220
|
initResult = initializeDashboardAuth(context, tokenRefresherRunning)
|
|
206
221
|
break
|
package/src/auth/authStrategy.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {type ClientConfig, type SanityClient} from '@sanity/client'
|
|
|
3
3
|
import {type AuthConfig} from '../config/authConfig'
|
|
4
4
|
import {type TokenSource} from '../config/sanityConfig'
|
|
5
5
|
import {type AuthMethodOptions, type AuthState, type DashboardContext} from './authStore'
|
|
6
|
+
import {type OAuthTokens} from './oauth/types'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* The result returned by each auth strategy's `getInitialState` function.
|
|
@@ -17,6 +18,7 @@ export interface AuthStrategyResult {
|
|
|
17
18
|
storageArea: Storage | undefined
|
|
18
19
|
authMethod: AuthMethodOptions
|
|
19
20
|
dashboardContext: DashboardContext
|
|
21
|
+
oauthTokens?: OAuthTokens
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/**
|
|
@@ -5,6 +5,7 @@ import {createSanityInstance, type SanityInstance} from '../store/createSanityIn
|
|
|
5
5
|
import {AuthStateType} from './authStateType'
|
|
6
6
|
import {getAuthState} from './authStore'
|
|
7
7
|
import {handleAuthCallback} from './handleAuthCallback'
|
|
8
|
+
import {handleOAuthCallback} from './oauth/oauthActions'
|
|
8
9
|
import {subscribeToStateAndFetchCurrentUser} from './subscribeToStateAndFetchCurrentUser'
|
|
9
10
|
import {subscribeToStorageEventsAndSetToken} from './subscribeToStorageEventsAndSetToken'
|
|
10
11
|
import {getAuthCode, getTokenFromLocation, getTokenFromStorage} from './utils'
|
|
@@ -22,6 +23,11 @@ vi.mock('./utils', async (importOriginal) => {
|
|
|
22
23
|
vi.mock('./subscribeToStateAndFetchCurrentUser')
|
|
23
24
|
vi.mock('./subscribeToStorageEventsAndSetToken')
|
|
24
25
|
|
|
26
|
+
vi.mock('./oauth/oauthActions', async (importOriginal) => {
|
|
27
|
+
const original = await importOriginal<typeof import('./oauth/oauthActions')>()
|
|
28
|
+
return {...original, handleOAuthCallback: vi.fn()}
|
|
29
|
+
})
|
|
30
|
+
|
|
25
31
|
// Mock logger to prevent actual logging during tests
|
|
26
32
|
vi.mock('../utils/logger', async (importOriginal) => {
|
|
27
33
|
const original = await importOriginal<typeof import('../utils/logger')>()
|
|
@@ -277,4 +283,31 @@ describe('handleCallback', () => {
|
|
|
277
283
|
// Verify logger was called
|
|
278
284
|
expect(createLogger).toHaveBeenCalled()
|
|
279
285
|
})
|
|
286
|
+
|
|
287
|
+
it('delegates to handleOAuthCallback in OAuth mode', async () => {
|
|
288
|
+
const clientFactory = vi.fn()
|
|
289
|
+
vi.mocked(getTokenFromStorage).mockReturnValue(null)
|
|
290
|
+
vi.mocked(handleOAuthCallback).mockResolvedValue('https://app.example.com/callback')
|
|
291
|
+
|
|
292
|
+
instance = createSanityInstance({
|
|
293
|
+
projectId: 'p',
|
|
294
|
+
dataset: 'd',
|
|
295
|
+
auth: {
|
|
296
|
+
clientFactory,
|
|
297
|
+
oauth: {
|
|
298
|
+
clientId: 'client-abc',
|
|
299
|
+
redirectUri: 'https://app.example.com/callback',
|
|
300
|
+
organizationId: 'org123',
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
const href = 'https://app.example.com/callback?code=abc&state=xyz'
|
|
306
|
+
const result = await handleAuthCallback(instance, href)
|
|
307
|
+
|
|
308
|
+
expect(result).toBe('https://app.example.com/callback')
|
|
309
|
+
expect(handleOAuthCallback).toHaveBeenCalledWith(instance, href)
|
|
310
|
+
expect(getAuthCode).not.toHaveBeenCalled()
|
|
311
|
+
expect(clientFactory).not.toHaveBeenCalled()
|
|
312
|
+
})
|
|
280
313
|
})
|
|
@@ -3,6 +3,7 @@ import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
|
|
|
3
3
|
import {getAuthLogger} from './authLogger'
|
|
4
4
|
import {AuthStateType} from './authStateType'
|
|
5
5
|
import {authStore, type AuthStoreState, type DashboardContext} from './authStore'
|
|
6
|
+
import {handleOAuthCallback} from './oauth/oauthActions'
|
|
6
7
|
import {
|
|
7
8
|
createLoggedInAuthState,
|
|
8
9
|
getAuthCode,
|
|
@@ -12,6 +13,10 @@ import {
|
|
|
12
13
|
} from './utils'
|
|
13
14
|
|
|
14
15
|
/**
|
|
16
|
+
* Handles the auth redirect callback. In OAuth mode this delegates to
|
|
17
|
+
* {@link handleOAuthCallback}; otherwise it exchanges the legacy `sid` for a
|
|
18
|
+
* long-lived token.
|
|
19
|
+
*
|
|
15
20
|
* @public
|
|
16
21
|
*/
|
|
17
22
|
export const handleAuthCallback = bindActionGlobally(
|
|
@@ -19,7 +24,7 @@ export const handleAuthCallback = bindActionGlobally(
|
|
|
19
24
|
async ({state, instance}, locationHref: string = getDefaultLocation()) => {
|
|
20
25
|
const logger = getAuthLogger(instance)
|
|
21
26
|
|
|
22
|
-
const {providedToken, callbackUrl, clientFactory, apiHost, storageArea, storageKey} =
|
|
27
|
+
const {providedToken, callbackUrl, clientFactory, apiHost, storageArea, storageKey, oauth} =
|
|
23
28
|
state.get().options
|
|
24
29
|
|
|
25
30
|
// If a token is provided, no need to handle callback
|
|
@@ -28,6 +33,11 @@ export const handleAuthCallback = bindActionGlobally(
|
|
|
28
33
|
return false
|
|
29
34
|
}
|
|
30
35
|
|
|
36
|
+
if (oauth) {
|
|
37
|
+
logger.debug('OAuth mode - delegating to handleOAuthCallback')
|
|
38
|
+
return handleOAuthCallback(instance, locationHref)
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
// Don't handle the callback if already in flight.
|
|
32
42
|
const {authState} = state.get()
|
|
33
43
|
if (authState.type === AuthStateType.LOGGING_IN && authState.isExchangingToken) {
|
package/src/auth/logout.test.ts
CHANGED
|
@@ -5,6 +5,9 @@ import {createSanityInstance, type SanityInstance} from '../store/createSanityIn
|
|
|
5
5
|
import {AuthStateType} from './authStateType'
|
|
6
6
|
import {getAuthState} from './authStore'
|
|
7
7
|
import {logout} from './logout'
|
|
8
|
+
import {getOAuthTokensState, serializeTokens} from './oauth/oauthActions'
|
|
9
|
+
import {OAUTH_TOKENS_KEY} from './oauth/oauthAuth'
|
|
10
|
+
import {type OAuthTokens} from './oauth/types'
|
|
8
11
|
import {subscribeToStateAndFetchCurrentUser} from './subscribeToStateAndFetchCurrentUser'
|
|
9
12
|
import {subscribeToStorageEventsAndSetToken} from './subscribeToStorageEventsAndSetToken'
|
|
10
13
|
import {getTokenFromStorage} from './utils'
|
|
@@ -166,6 +169,56 @@ describe('logout', () => {
|
|
|
166
169
|
expect(removeItem).toHaveBeenCalledWith('__sanity_auth_token')
|
|
167
170
|
})
|
|
168
171
|
|
|
172
|
+
it('revokes OAuth tokens instead of calling /auth/logout in OAuth mode', async () => {
|
|
173
|
+
const oauthTokens: OAuthTokens = {
|
|
174
|
+
accessToken: 'oauth-access',
|
|
175
|
+
tokenType: 'bearer',
|
|
176
|
+
expiresIn: 3600,
|
|
177
|
+
expiresAt: new Date('2030-01-01T00:00:00.000Z'),
|
|
178
|
+
refreshToken: 'oauth-refresh',
|
|
179
|
+
}
|
|
180
|
+
const map = new Map<string, string>([[OAUTH_TOKENS_KEY, serializeTokens(oauthTokens)]])
|
|
181
|
+
const storageArea = {
|
|
182
|
+
getItem: (k: string) => map.get(k) ?? null,
|
|
183
|
+
setItem: (k: string, v: string) => void map.set(k, v),
|
|
184
|
+
removeItem: (k: string) => void map.delete(k),
|
|
185
|
+
} as Storage
|
|
186
|
+
|
|
187
|
+
const mockRequest = vi.fn().mockResolvedValue(undefined)
|
|
188
|
+
const clientFactory = vi.fn().mockReturnValue({request: mockRequest})
|
|
189
|
+
|
|
190
|
+
instance = createSanityInstance({
|
|
191
|
+
projectId: 'p',
|
|
192
|
+
dataset: 'd',
|
|
193
|
+
auth: {
|
|
194
|
+
clientFactory,
|
|
195
|
+
storageArea,
|
|
196
|
+
oauth: {
|
|
197
|
+
clientId: 'client-abc',
|
|
198
|
+
redirectUri: 'https://app.example.com/callback',
|
|
199
|
+
organizationId: 'org123',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Boots logged in with the persisted OAuth tokens exposed on the store.
|
|
205
|
+
expect(getOAuthTokensState(instance).getCurrent()).toMatchObject({accessToken: 'oauth-access'})
|
|
206
|
+
|
|
207
|
+
const first = logout(instance)
|
|
208
|
+
// Session is marked as destroying so a concurrent call is a no-op
|
|
209
|
+
expect(getAuthState(instance).getCurrent()).toMatchObject({isDestroyingSession: true})
|
|
210
|
+
await logout(instance)
|
|
211
|
+
await first
|
|
212
|
+
|
|
213
|
+
expect(mockRequest).toHaveBeenCalledTimes(1)
|
|
214
|
+
expect(mockRequest).toHaveBeenCalledWith(
|
|
215
|
+
expect.objectContaining({url: '/auth/oauth/revoke', tag: 'oauth.revoke'}),
|
|
216
|
+
)
|
|
217
|
+
expect(getOAuthTokensState(instance).getCurrent()).toBeNull()
|
|
218
|
+
expect(getAuthState(instance).getCurrent()).toMatchObject({type: AuthStateType.LOGGED_OUT})
|
|
219
|
+
expect(map.get(OAUTH_TOKENS_KEY)).toBeUndefined()
|
|
220
|
+
})
|
|
221
|
+
|
|
169
222
|
it('cleans up storage even if logout request fails', async () => {
|
|
170
223
|
vi.mocked(getTokenFromStorage).mockReturnValue('token')
|
|
171
224
|
const error = new Error('Logout request failed')
|