@sanity/sdk 3.1.0 → 3.2.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.
Files changed (83) hide show
  1. package/dist/_exports/_internal.d.ts +52 -21
  2. package/dist/_exports/_internal.d.ts.map +1 -1
  3. package/dist/_exports/_internal.js +10 -9
  4. package/dist/_exports/_internal.js.map +1 -1
  5. package/dist/_exports/agent.d.ts +7 -7
  6. package/dist/_exports/agent.d.ts.map +1 -1
  7. package/dist/_exports/agent.js +1 -1
  8. package/dist/_exports/comlink.d.ts +9 -9
  9. package/dist/_exports/comlink.d.ts.map +1 -1
  10. package/dist/_exports/comlink.js +3 -2
  11. package/dist/_exports/comlink.js.map +1 -1
  12. package/dist/_exports/dashboard.d.ts +2 -0
  13. package/dist/_exports/dashboard.js +2 -0
  14. package/dist/{authStore-C-OEkGaM.d.ts → authStore-BO2vch3I.d.ts} +516 -63
  15. package/dist/authStore-BO2vch3I.d.ts.map +1 -0
  16. package/dist/{clientStore-gUALQ0qW.js → clientStore-BI4D8HhJ.js} +124 -7
  17. package/dist/clientStore-BI4D8HhJ.js.map +1 -0
  18. package/dist/{createSanityInstance-DCePpBYW.d.ts → createSanityInstance-BQIqyGLX.d.ts} +16 -1
  19. package/dist/createSanityInstance-BQIqyGLX.d.ts.map +1 -0
  20. package/dist/{object-BwynGPRZ.js → createStateSourceAction-DdAhEfnZ.js} +2 -82
  21. package/dist/createStateSourceAction-DdAhEfnZ.js.map +1 -0
  22. package/dist/defineProperty-Bho9Ngsi.js +31 -0
  23. package/dist/{getNodeState-E0hjqFOo.js → getNodeState-B4nG0sjm.js} +3 -2
  24. package/dist/{getNodeState-E0hjqFOo.js.map → getNodeState-B4nG0sjm.js.map} +1 -1
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.js +335 -51
  27. package/dist/index.js.map +1 -1
  28. package/dist/object-CxXlSVg7.js +83 -0
  29. package/dist/object-CxXlSVg7.js.map +1 -0
  30. package/dist/topicStore-Ct7IFNtY.js +565 -0
  31. package/dist/topicStore-Ct7IFNtY.js.map +1 -0
  32. package/dist/topicStore-D0WqhQGF.d.ts +435 -0
  33. package/dist/topicStore-D0WqhQGF.d.ts.map +1 -0
  34. package/dist/{version-C90rNWaX.js → version-BcRmCiYi.js} +6 -6
  35. package/dist/{version-C90rNWaX.js.map → version-BcRmCiYi.js.map} +1 -1
  36. package/package.json +19 -18
  37. package/src/_exports/_internal.ts +12 -0
  38. package/src/_exports/dashboard.ts +36 -0
  39. package/src/_exports/index.ts +17 -1
  40. package/src/applications/applications.test-d.ts +28 -4
  41. package/src/applications/applications.ts +39 -14
  42. package/src/auth/authMode.test.ts +24 -0
  43. package/src/auth/authMode.ts +8 -3
  44. package/src/auth/authStore.ts +12 -0
  45. package/src/auth/authStrategy.ts +2 -0
  46. package/src/auth/logout.test.ts +45 -0
  47. package/src/auth/logout.ts +1 -0
  48. package/src/auth/oauth/oauthActions.test.ts +457 -0
  49. package/src/auth/oauth/oauthActions.ts +389 -0
  50. package/src/auth/oauth/oauthAuth.test.ts +206 -0
  51. package/src/auth/oauth/oauthAuth.ts +168 -0
  52. package/src/auth/oauth/pkce.test.ts +44 -0
  53. package/src/auth/oauth/pkce.ts +60 -0
  54. package/src/auth/oauth/types.ts +12 -0
  55. package/src/config/authConfig.ts +18 -0
  56. package/src/dashboard/messageBus/__fixtures__/test-topics.ts +18 -0
  57. package/src/dashboard/messageBus/bus.test.ts +1033 -0
  58. package/src/dashboard/messageBus/bus.ts +1246 -0
  59. package/src/dashboard/messageBus/store.test.ts +142 -0
  60. package/src/dashboard/messageBus/store.ts +74 -0
  61. package/src/dashboard/messageBus/topicStore.test.ts +172 -0
  62. package/src/dashboard/messageBus/topicStore.ts +184 -0
  63. package/src/dashboard/messageBus/topics.test.ts +71 -0
  64. package/src/dashboard/messageBus/topics.ts +260 -0
  65. package/src/document/actions.ts +3 -3
  66. package/src/document/applyDocumentActions.ts +3 -3
  67. package/src/document/documentStore.ts +12 -12
  68. package/src/document/processMutations.ts +58 -22
  69. package/src/installations/installations.test-d.ts +3 -3
  70. package/src/installations/installations.ts +2 -19
  71. package/src/projection/getProjectionState.ts +2 -2
  72. package/src/projection/resolveProjection.ts +2 -2
  73. package/src/query/queryStore.ts +3 -3
  74. package/src/typegen/defineProjection.test.ts +17 -0
  75. package/src/typegen/defineProjection.ts +19 -0
  76. package/src/typegen/groqCompat.ts +122 -0
  77. package/src/typegen/resolve.ts +37 -0
  78. package/src/utils/createGroqSearchFilter.test.ts +8 -0
  79. package/src/utils/createGroqSearchFilter.ts +6 -3
  80. package/dist/authStore-C-OEkGaM.d.ts.map +0 -1
  81. package/dist/clientStore-gUALQ0qW.js.map +0 -1
  82. package/dist/createSanityInstance-DCePpBYW.d.ts.map +0 -1
  83. 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.1.0",
3
+ "version": "3.2.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.4.0",
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.0.0",
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
56
  "@sanity/mutate": "^0.18.1",
56
57
  "@sanity/telemetry": "^1.1.0",
57
- "@sanity/types": "^6.12.0",
58
- "groq": "3.88.1-typegen-experimental.0",
58
+ "@sanity/types": "^6.13.0",
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.13"
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'
@@ -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('Application — config.studio adds a required config.studio', () => {
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
- Application<'config.studio'>['config']['studio']
41
- >().toEqualTypeOf<ApplicationStudioConfig>()
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
- // `config` is present when either config child was requested; each child is
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
- {config: {studio: ApplicationStudioConfig}}
162
+ {studio?: ApplicationStudioConfig}
147
163
  > &
148
- Included<ApplicationInclude, 'config.mfManifest', Include, {config: {mfManifest: unknown}}>
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', () => {
@@ -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. Dashboard context detected (`_context` URL param with content) → `'dashboard'`
24
- * 3. Otherwise → `'standalone'`
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
  }
@@ -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,7 @@ export const authStore = defineStore<AuthStoreState>({
183
191
  storageKey: result.storageKey,
184
192
  storageArea: result.storageArea,
185
193
  authMethod: result.authMethod,
194
+ oauth: authConfig.oauth,
186
195
  },
187
196
  }
188
197
  },
@@ -201,6 +210,9 @@ export const authStore = defineStore<AuthStoreState>({
201
210
  case 'studio':
202
211
  initResult = initializeStudioAuth(context, tokenRefresherRunning)
203
212
  break
213
+ case 'oauth':
214
+ initResult = initializeOauthAuth(context)
215
+ break
204
216
  case 'dashboard':
205
217
  initResult = initializeDashboardAuth(context, tokenRefresherRunning)
206
218
  break
@@ -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,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,48 @@ describe('logout', () => {
166
169
  expect(removeItem).toHaveBeenCalledWith('__sanity_auth_token')
167
170
  })
168
171
 
172
+ it('clears the OAuth tokens from the store on logout', 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
+ await logout(instance)
208
+
209
+ expect(getOAuthTokensState(instance).getCurrent()).toBeNull()
210
+ expect(getAuthState(instance).getCurrent()).toMatchObject({type: AuthStateType.LOGGED_OUT})
211
+ expect(map.get(OAUTH_TOKENS_KEY)).toBeUndefined()
212
+ })
213
+
169
214
  it('cleans up storage even if logout request fails', async () => {
170
215
  vi.mocked(getTokenFromStorage).mockReturnValue('token')
171
216
  const error = new Error('Logout request failed')
@@ -57,6 +57,7 @@ export const logout = bindActionGlobally(authStore, async ({state, instance}) =>
57
57
  logger.info('User logged out, clearing stored tokens')
58
58
  state.set('logoutSuccess', {
59
59
  authState: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
60
+ oauthTokens: undefined,
60
61
  })
61
62
  storageArea?.removeItem(storageKey)
62
63
  storageArea?.removeItem(`${storageKey}_last_refresh`)