@sanity/sdk 3.4.0-rc.0 → 3.4.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 (48) hide show
  1. package/dist/_exports/_internal.d.ts +3 -3
  2. package/dist/_exports/_internal.js +5 -5
  3. package/dist/_exports/agent.d.ts +1 -1
  4. package/dist/_exports/agent.js +1 -1
  5. package/dist/_exports/comlink.d.ts +1 -1
  6. package/dist/_exports/dashboard.d.ts +2 -2
  7. package/dist/_exports/dashboard.js +2 -2
  8. package/dist/{applications-903KQTrD.d.ts → applications-Bb-0Kgkc.d.ts} +6 -20
  9. package/dist/applications-Bb-0Kgkc.d.ts.map +1 -0
  10. package/dist/{clientStore-QDrJNTM4.js → clientStore-CD-USpIU.js} +11 -15
  11. package/dist/clientStore-CD-USpIU.js.map +1 -0
  12. package/dist/{createSanityInstance--Lrldto8.d.ts → createSanityInstance-BQIqyGLX.d.ts} +3 -6
  13. package/dist/createSanityInstance-BQIqyGLX.d.ts.map +1 -0
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.js +204 -211
  16. package/dist/index.js.map +1 -1
  17. package/dist/{topicStore-BJ54_X6H.js → topicStore-CPhGph6z.js} +53 -13
  18. package/dist/topicStore-CPhGph6z.js.map +1 -0
  19. package/dist/{topicStore-VQILEI1R.d.ts → topicStore-fPK2CfqH.d.ts} +91 -4
  20. package/dist/topicStore-fPK2CfqH.d.ts.map +1 -0
  21. package/dist/{version-CYqZgJX1.js → version-C7m7OAy9.js} +3 -3
  22. package/dist/{version-CYqZgJX1.js.map → version-C7m7OAy9.js.map} +1 -1
  23. package/package.json +6 -6
  24. package/src/_exports/dashboard.ts +6 -0
  25. package/src/auth/authStore.test.ts +0 -18
  26. package/src/auth/authStore.ts +1 -4
  27. package/src/auth/handleAuthCallback.test.ts +0 -33
  28. package/src/auth/handleAuthCallback.ts +1 -11
  29. package/src/auth/logout.test.ts +1 -9
  30. package/src/auth/logout.ts +1 -18
  31. package/src/auth/oauth/oauthActions.test.ts +1 -178
  32. package/src/auth/oauth/oauthActions.ts +46 -111
  33. package/src/auth/oauth/oauthAuth.test.ts +0 -23
  34. package/src/auth/oauth/oauthAuth.ts +8 -18
  35. package/src/config/authConfig.ts +2 -5
  36. package/src/dashboard/messageBus/bus.test.ts +118 -0
  37. package/src/dashboard/messageBus/bus.ts +81 -7
  38. package/src/dashboard/messageBus/topics.test.ts +20 -0
  39. package/src/dashboard/messageBus/topics.ts +74 -0
  40. package/src/document/documentConstants.ts +5 -2
  41. package/src/document/documentStore.test.ts +194 -0
  42. package/src/document/documentStore.ts +79 -39
  43. package/src/document/reducers.ts +2 -2
  44. package/dist/applications-903KQTrD.d.ts.map +0 -1
  45. package/dist/clientStore-QDrJNTM4.js.map +0 -1
  46. package/dist/createSanityInstance--Lrldto8.d.ts.map +0 -1
  47. package/dist/topicStore-BJ54_X6H.js.map +0 -1
  48. package/dist/topicStore-VQILEI1R.d.ts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk",
3
- "version": "3.4.0-rc.0",
3
+ "version": "3.4.0",
4
4
  "private": false,
5
5
  "description": "Sanity SDK",
6
6
  "keywords": [
@@ -55,8 +55,8 @@
55
55
  "@sanity/message-protocol": "^0.24.0",
56
56
  "@sanity/mutate": "^0.18.2",
57
57
  "@sanity/telemetry": "^1.1.0",
58
- "@sanity/types": "^6.14.1",
59
- "groq": "^6.13.2",
58
+ "@sanity/types": "^6.15.0",
59
+ "groq": "^6.15.0",
60
60
  "groq-js": "^2.0.0",
61
61
  "reselect": "^5.1.1",
62
62
  "rxjs": "^7.8.2",
@@ -69,12 +69,12 @@
69
69
  "@repo/package.config": "0.0.1",
70
70
  "@repo/tsconfig": "0.0.1",
71
71
  "@sanity/browserslist-config": "^1.0.5",
72
- "@sanity/pkg-utils": "^13.0.0",
73
- "@types/node": "^24.13.5",
72
+ "@sanity/pkg-utils": "^13.0.1",
73
+ "@types/node": "^24.13.6",
74
74
  "@vitest/coverage-v8": "^5.0.1",
75
75
  "eslint": "^10.10.0",
76
76
  "oxfmt": "^0.68.0",
77
- "rolldown": "^1.2.8",
77
+ "rolldown": "^1.2.9",
78
78
  "typescript": "^6.0.3",
79
79
  "vite": "^8.3.0",
80
80
  "vitest": "^5.0.1"
@@ -16,9 +16,14 @@ export type {
16
16
  export {connectMessageBus} from '../dashboard/messageBus/bus'
17
17
  export {MessageBusError} from '../dashboard/messageBus/bus'
18
18
  export type {
19
+ ApplicationActivity,
19
20
  ApplicationConfig,
20
21
  ApplicationConfigAppType,
22
+ ApplicationContext,
23
+ Capability,
24
+ CapabilityRecord,
21
25
  DashboardTopics,
26
+ DocumentActivity,
22
27
  EventTopic,
23
28
  EventTopicDef,
24
29
  NavigationLocation,
@@ -33,4 +38,5 @@ export type {
33
38
  Topics,
34
39
  ValueOf,
35
40
  } from '../dashboard/messageBus/topics'
41
+ export {capabilities} from '../dashboard/messageBus/topics'
36
42
  export {type TopicData, TopicError} from '../dashboard/messageBus/topicStore'
@@ -339,24 +339,6 @@ 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
-
360
342
  it('logs auth initialized in logged out state when no token available', () => {
361
343
  instance = createSanityInstance({
362
344
  projectId: 'p',
@@ -191,10 +191,7 @@ export const authStore = defineStore<AuthStoreState>({
191
191
  storageKey: result.storageKey,
192
192
  storageArea: result.storageArea,
193
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,
194
+ oauth: authConfig.oauth,
198
195
  },
199
196
  }
200
197
  },
@@ -5,7 +5,6 @@ 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'
9
8
  import {subscribeToStateAndFetchCurrentUser} from './subscribeToStateAndFetchCurrentUser'
10
9
  import {subscribeToStorageEventsAndSetToken} from './subscribeToStorageEventsAndSetToken'
11
10
  import {getAuthCode, getTokenFromLocation, getTokenFromStorage} from './utils'
@@ -23,11 +22,6 @@ vi.mock('./utils', async (importOriginal) => {
23
22
  vi.mock('./subscribeToStateAndFetchCurrentUser')
24
23
  vi.mock('./subscribeToStorageEventsAndSetToken')
25
24
 
26
- vi.mock('./oauth/oauthActions', async (importOriginal) => {
27
- const original = await importOriginal<typeof import('./oauth/oauthActions')>()
28
- return {...original, handleOAuthCallback: vi.fn()}
29
- })
30
-
31
25
  // Mock logger to prevent actual logging during tests
32
26
  vi.mock('../utils/logger', async (importOriginal) => {
33
27
  const original = await importOriginal<typeof import('../utils/logger')>()
@@ -283,31 +277,4 @@ describe('handleCallback', () => {
283
277
  // Verify logger was called
284
278
  expect(createLogger).toHaveBeenCalled()
285
279
  })
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
- })
313
280
  })
@@ -3,7 +3,6 @@ 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'
7
6
  import {
8
7
  createLoggedInAuthState,
9
8
  getAuthCode,
@@ -13,10 +12,6 @@ import {
13
12
  } from './utils'
14
13
 
15
14
  /**
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
- *
20
15
  * @public
21
16
  */
22
17
  export const handleAuthCallback = bindActionGlobally(
@@ -24,7 +19,7 @@ export const handleAuthCallback = bindActionGlobally(
24
19
  async ({state, instance}, locationHref: string = getDefaultLocation()) => {
25
20
  const logger = getAuthLogger(instance)
26
21
 
27
- const {providedToken, callbackUrl, clientFactory, apiHost, storageArea, storageKey, oauth} =
22
+ const {providedToken, callbackUrl, clientFactory, apiHost, storageArea, storageKey} =
28
23
  state.get().options
29
24
 
30
25
  // If a token is provided, no need to handle callback
@@ -33,11 +28,6 @@ export const handleAuthCallback = bindActionGlobally(
33
28
  return false
34
29
  }
35
30
 
36
- if (oauth) {
37
- logger.debug('OAuth mode - delegating to handleOAuthCallback')
38
- return handleOAuthCallback(instance, locationHref)
39
- }
40
-
41
31
  // Don't handle the callback if already in flight.
42
32
  const {authState} = state.get()
43
33
  if (authState.type === AuthStateType.LOGGING_IN && authState.isExchangingToken) {
@@ -169,7 +169,7 @@ describe('logout', () => {
169
169
  expect(removeItem).toHaveBeenCalledWith('__sanity_auth_token')
170
170
  })
171
171
 
172
- it('revokes OAuth tokens instead of calling /auth/logout in OAuth mode', async () => {
172
+ it('clears the OAuth tokens from the store on logout', async () => {
173
173
  const oauthTokens: OAuthTokens = {
174
174
  accessToken: 'oauth-access',
175
175
  tokenType: 'bearer',
@@ -204,16 +204,8 @@ describe('logout', () => {
204
204
  // Boots logged in with the persisted OAuth tokens exposed on the store.
205
205
  expect(getOAuthTokensState(instance).getCurrent()).toMatchObject({accessToken: 'oauth-access'})
206
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
207
  await logout(instance)
211
- await first
212
208
 
213
- expect(mockRequest).toHaveBeenCalledTimes(1)
214
- expect(mockRequest).toHaveBeenCalledWith(
215
- expect.objectContaining({url: '/auth/oauth/revoke', tag: 'oauth.revoke'}),
216
- )
217
209
  expect(getOAuthTokensState(instance).getCurrent()).toBeNull()
218
210
  expect(getAuthState(instance).getCurrent()).toMatchObject({type: AuthStateType.LOGGED_OUT})
219
211
  expect(map.get(OAUTH_TOKENS_KEY)).toBeUndefined()
@@ -3,22 +3,14 @@ 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} from './authStore'
6
- import {revokeOAuthTokens} from './oauth/oauthActions'
7
6
 
8
7
  /**
9
- * Logs the current user out. In OAuth mode this revokes the OAuth tokens
10
- * instead of calling the legacy `/auth/logout` endpoint; revocation is
11
- * best-effort, so the OAuth path resolves even if the revoke request fails,
12
- * whereas the legacy path rejects when `/auth/logout` fails. Local state and
13
- * storage are cleared in both cases.
14
- *
15
8
  * @public
16
9
  */
17
10
  export const logout = bindActionGlobally(authStore, async ({state, instance}) => {
18
11
  const logger = getAuthLogger(instance)
19
12
 
20
- const {clientFactory, apiHost, providedToken, storageArea, storageKey, oauth} =
21
- state.get().options
13
+ const {clientFactory, apiHost, providedToken, storageArea, storageKey} = state.get().options
22
14
 
23
15
  // If a token is statically provided, logout does nothing
24
16
  if (providedToken) {
@@ -33,15 +25,6 @@ export const logout = bindActionGlobally(authStore, async ({state, instance}) =>
33
25
  logger.debug('Skipping logout - already in progress')
34
26
  return
35
27
  }
36
-
37
- if (oauth) {
38
- logger.debug('OAuth mode - revoking tokens instead of calling /auth/logout')
39
- state.set('loggingOut', {
40
- authState: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: true},
41
- })
42
- await revokeOAuthTokens(instance)
43
- return
44
- }
45
28
  const token = authState.type === AuthStateType.LOGGED_IN && authState.token
46
29
 
47
30
  try {
@@ -9,7 +9,6 @@ import {subscribeToStateAndFetchCurrentUser} from '../subscribeToStateAndFetchCu
9
9
  import {
10
10
  getOAuthTokensState,
11
11
  handleOAuthCallback,
12
- OAUTH_RETURN_TO_KEY,
13
12
  OAUTH_STATE_KEY,
14
13
  OAUTH_VERIFIER_KEY,
15
14
  refreshOAuthTokens,
@@ -43,11 +42,6 @@ const oauthConfig = {
43
42
  organizationId: 'org123',
44
43
  }
45
44
 
46
- const oauthConfigNoOrg = {
47
- clientId: 'client-abc',
48
- redirectUri: 'https://app.example.com/callback',
49
- }
50
-
51
45
  const seededTokens: OAuthTokens = {
52
46
  accessToken: 'stored-access',
53
47
  tokenType: 'bearer',
@@ -99,7 +93,6 @@ interface SetupOptions {
99
93
  sessionSeed?: Record<string, string>
100
94
  initialLocationHref?: string
101
95
  withOAuthConfig?: boolean
102
- oauthConfig?: {clientId: string; redirectUri: string; organizationId?: string}
103
96
  apiHost?: string
104
97
  }
105
98
 
@@ -118,7 +111,7 @@ function setup(options: SetupOptions = {}) {
118
111
  storageArea,
119
112
  initialLocationHref: options.initialLocationHref ?? 'https://app.example.com/',
120
113
  ...(options.apiHost && {apiHost: options.apiHost}),
121
- ...(options.withOAuthConfig === false ? {} : {oauth: options.oauthConfig ?? oauthConfig}),
114
+ ...(options.withOAuthConfig === false ? {} : {oauth: oauthConfig}),
122
115
  },
123
116
  })
124
117
 
@@ -163,39 +156,6 @@ describe('startOAuthAuthorization', () => {
163
156
  expect(url.searchParams.getAll('resource')).toEqual(['urn:io.sanity:organization:org123'])
164
157
  })
165
158
 
166
- it('stashes the current location to return to, keeping app params intact', async () => {
167
- const href = 'https://app.example.com/documents/abc?state=draft&code=x#h'
168
- vi.stubGlobal('window', {location: {assign: vi.fn(), href}})
169
- vi.stubGlobal('location', {href})
170
- const {session} = setup()
171
-
172
- await startOAuthAuthorization(instance!)
173
-
174
- expect(session.getItem(OAUTH_RETURN_TO_KEY)).toBe(href)
175
- })
176
-
177
- it('strips stale OAuth params from the stashed location when already on the callback route', async () => {
178
- const href = 'https://app.example.com/callback?x=1&code=old&state=old'
179
- vi.stubGlobal('window', {location: {assign: vi.fn(), href}})
180
- vi.stubGlobal('location', {href})
181
- const {session} = setup()
182
-
183
- await startOAuthAuthorization(instance!)
184
-
185
- expect(session.getItem(OAUTH_RETURN_TO_KEY)).toBe('https://app.example.com/callback?x=1')
186
- })
187
-
188
- it('omits the resource param when no organizationId is configured', async () => {
189
- const assign = vi.fn()
190
- vi.stubGlobal('window', {location: {assign}})
191
- setup({initialLocationHref: 'https://app.example.com/', oauthConfig: oauthConfigNoOrg})
192
-
193
- await startOAuthAuthorization(instance!)
194
-
195
- const url = new URL(assign.mock.calls[0][0])
196
- expect(url.searchParams.has('resource')).toBe(false)
197
- })
198
-
199
159
  it('throws when OAuth is not configured', async () => {
200
160
  setup({withOAuthConfig: false})
201
161
  await expect(startOAuthAuthorization(instance!)).rejects.toThrow(/OAuth is not configured/)
@@ -243,75 +203,6 @@ describe('handleOAuthCallback', () => {
243
203
  })
244
204
  })
245
205
 
246
- it('returns the stashed same-origin location after a successful exchange', async () => {
247
- const returnTo = 'https://app.example.com/documents/abc?x=1'
248
- const {session} = setup({
249
- sessionSeed: {
250
- [OAUTH_STATE_KEY]: 'state-xyz',
251
- [OAUTH_VERIFIER_KEY]: 'verifier-1',
252
- [OAUTH_RETURN_TO_KEY]: returnTo,
253
- },
254
- })
255
-
256
- expect(await handleOAuthCallback(instance!, callbackHref)).toBe(returnTo)
257
- expect(session.getItem(OAUTH_RETURN_TO_KEY)).toBeNull()
258
- })
259
-
260
- it('ignores a stashed location on a different origin', async () => {
261
- setup({
262
- sessionSeed: {
263
- [OAUTH_STATE_KEY]: 'state-xyz',
264
- [OAUTH_VERIFIER_KEY]: 'verifier-1',
265
- [OAUTH_RETURN_TO_KEY]: 'https://evil.example.com/',
266
- },
267
- })
268
-
269
- expect(await handleOAuthCallback(instance!, callbackHref)).toBe(
270
- 'https://app.example.com/callback',
271
- )
272
- })
273
-
274
- it('ignores a stashed location that is not a URL', async () => {
275
- setup({
276
- sessionSeed: {
277
- [OAUTH_STATE_KEY]: 'state-xyz',
278
- [OAUTH_VERIFIER_KEY]: 'verifier-1',
279
- [OAUTH_RETURN_TO_KEY]: 'not a url',
280
- },
281
- })
282
-
283
- expect(await handleOAuthCallback(instance!, callbackHref)).toBe(
284
- 'https://app.example.com/callback',
285
- )
286
- })
287
-
288
- it('does not restore the stashed location on a state mismatch', async () => {
289
- const {session} = setup({
290
- sessionSeed: {
291
- [OAUTH_STATE_KEY]: 'different',
292
- [OAUTH_VERIFIER_KEY]: 'verifier-1',
293
- [OAUTH_RETURN_TO_KEY]: 'https://app.example.com/deep',
294
- },
295
- })
296
-
297
- expect(await handleOAuthCallback(instance!, callbackHref)).toBe(
298
- 'https://app.example.com/callback',
299
- )
300
- expect(session.getItem(OAUTH_RETURN_TO_KEY)).toBeNull()
301
- })
302
-
303
- it('does not restore the stashed location on an ?error= callback', async () => {
304
- const {session} = setup({sessionSeed: {[OAUTH_RETURN_TO_KEY]: 'https://app.example.com/deep'}})
305
-
306
- const result = await handleOAuthCallback(
307
- instance!,
308
- 'https://app.example.com/callback?error=access_denied',
309
- )
310
-
311
- expect(result).toBe('https://app.example.com/callback')
312
- expect(session.getItem(OAUTH_RETURN_TO_KEY)).toBeNull()
313
- })
314
-
315
206
  it('does not perform a second exchange on a duplicate concurrent invocation', async () => {
316
207
  const pending = deferred<typeof tokenResponse>()
317
208
  const request = vi.fn().mockReturnValue(pending.promise)
@@ -330,31 +221,6 @@ describe('handleOAuthCallback', () => {
330
221
  expect(await first).toBe('https://app.example.com/callback')
331
222
  })
332
223
 
333
- it('does not clobber LOGGED_IN when called again with a stale callback URL', async () => {
334
- const {request} = setup({
335
- sessionSeed: {[OAUTH_STATE_KEY]: 'state-xyz', [OAUTH_VERIFIER_KEY]: 'verifier-1'},
336
- })
337
-
338
- expect(await handleOAuthCallback(instance!, callbackHref)).toBe(
339
- 'https://app.example.com/callback',
340
- )
341
- // Artifacts are now cleared; a repeat with the same URL fails state validation.
342
- // Seed fresh artifacts as if a re-authorization started in this tab: the
343
- // ignored stale callback must not wipe them.
344
- sessionStorage.setItem(OAUTH_STATE_KEY, 'state-next')
345
- sessionStorage.setItem(OAUTH_VERIFIER_KEY, 'verifier-next')
346
- const result = await handleOAuthCallback(instance!, callbackHref)
347
-
348
- expect(result).toBe('https://app.example.com/callback')
349
- expect(request).toHaveBeenCalledTimes(1)
350
- expect(getAuthState(instance!).getCurrent()).toMatchObject({
351
- type: AuthStateType.LOGGED_IN,
352
- token: 'new-access',
353
- })
354
- expect(sessionStorage.getItem(OAUTH_STATE_KEY)).toBe('state-next')
355
- expect(sessionStorage.getItem(OAUTH_VERIFIER_KEY)).toBe('verifier-next')
356
- })
357
-
358
224
  it('surfaces an ?error= callback as ERROR without exchanging', async () => {
359
225
  const {request} = setup()
360
226
 
@@ -368,25 +234,6 @@ describe('handleOAuthCallback', () => {
368
234
  expect(getAuthState(instance!).getCurrent()).toMatchObject({type: AuthStateType.ERROR})
369
235
  })
370
236
 
371
- it('ignores an ?error= callback when a session is already established', async () => {
372
- setup({
373
- storageSeed: {[OAUTH_TOKENS_KEY]: serializeTokens(seededTokens)},
374
- sessionSeed: {[OAUTH_STATE_KEY]: 'state-next', [OAUTH_VERIFIER_KEY]: 'verifier-next'},
375
- })
376
-
377
- const result = await handleOAuthCallback(
378
- instance!,
379
- 'https://app.example.com/callback?error=access_denied',
380
- )
381
-
382
- expect(result).toBe('https://app.example.com/callback')
383
- expect(getAuthState(instance!).getCurrent()).toMatchObject({
384
- type: AuthStateType.LOGGED_IN,
385
- token: seededTokens.accessToken,
386
- })
387
- expect(sessionStorage.getItem(OAUTH_STATE_KEY)).toBe('state-next')
388
- })
389
-
390
237
  it('surfaces an ?error= callback without a description', async () => {
391
238
  setup()
392
239
  await handleOAuthCallback(instance!, 'https://app.example.com/callback?error=access_denied')
@@ -422,18 +269,6 @@ describe('handleOAuthCallback', () => {
422
269
  expect(request).not.toHaveBeenCalled()
423
270
  })
424
271
 
425
- it('omits the resource param when no organizationId is configured', async () => {
426
- const {request} = setup({
427
- oauthConfig: oauthConfigNoOrg,
428
- sessionSeed: {[OAUTH_STATE_KEY]: 'state-xyz', [OAUTH_VERIFIER_KEY]: 'verifier-1'},
429
- })
430
-
431
- await handleOAuthCallback(instance!, callbackHref)
432
-
433
- const body = parseBody(request.mock.calls[0][0].body)
434
- expect(body.has('resource')).toBe(false)
435
- })
436
-
437
272
  it('sets ERROR when the token exchange fails', async () => {
438
273
  const request = vi.fn().mockRejectedValue(new Error('boom'))
439
274
  const {session} = setup({
@@ -468,18 +303,6 @@ describe('refreshOAuthTokens', () => {
468
303
  expect(readStored(storageArea)).toMatchObject({accessToken: 'new-access'})
469
304
  })
470
305
 
471
- it('omits the resource param when no organizationId is configured', async () => {
472
- const {request} = setup({
473
- oauthConfig: oauthConfigNoOrg,
474
- storageSeed: {[OAUTH_TOKENS_KEY]: serializeTokens(seededTokens)},
475
- })
476
-
477
- await refreshOAuthTokens(instance!)
478
-
479
- const body = parseBody(request.mock.calls[0][0].body)
480
- expect(body.has('resource')).toBe(false)
481
- })
482
-
483
306
  it('shares a single in-flight request across concurrent callers', async () => {
484
307
  const pending = deferred<typeof tokenResponse>()
485
308
  const request = vi.fn().mockReturnValue(pending.promise)