@sanity/sdk 3.3.0-rc.0 → 3.3.0-rc.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk",
3
- "version": "3.3.0-rc.0",
3
+ "version": "3.3.0-rc.1",
4
4
  "private": false,
5
5
  "description": "Sanity SDK",
6
6
  "keywords": [
@@ -43,6 +43,11 @@ const oauthConfig = {
43
43
  organizationId: 'org123',
44
44
  }
45
45
 
46
+ const oauthConfigNoOrg = {
47
+ clientId: 'client-abc',
48
+ redirectUri: 'https://app.example.com/callback',
49
+ }
50
+
46
51
  const seededTokens: OAuthTokens = {
47
52
  accessToken: 'stored-access',
48
53
  tokenType: 'bearer',
@@ -94,6 +99,7 @@ interface SetupOptions {
94
99
  sessionSeed?: Record<string, string>
95
100
  initialLocationHref?: string
96
101
  withOAuthConfig?: boolean
102
+ oauthConfig?: {clientId: string; redirectUri: string; organizationId?: string}
97
103
  apiHost?: string
98
104
  }
99
105
 
@@ -112,7 +118,7 @@ function setup(options: SetupOptions = {}) {
112
118
  storageArea,
113
119
  initialLocationHref: options.initialLocationHref ?? 'https://app.example.com/',
114
120
  ...(options.apiHost && {apiHost: options.apiHost}),
115
- ...(options.withOAuthConfig === false ? {} : {oauth: oauthConfig}),
121
+ ...(options.withOAuthConfig === false ? {} : {oauth: options.oauthConfig ?? oauthConfig}),
116
122
  },
117
123
  })
118
124
 
@@ -179,6 +185,17 @@ describe('startOAuthAuthorization', () => {
179
185
  expect(session.getItem(OAUTH_RETURN_TO_KEY)).toBe('https://app.example.com/callback?x=1')
180
186
  })
181
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
+
182
199
  it('throws when OAuth is not configured', async () => {
183
200
  setup({withOAuthConfig: false})
184
201
  await expect(startOAuthAuthorization(instance!)).rejects.toThrow(/OAuth is not configured/)
@@ -405,6 +422,18 @@ describe('handleOAuthCallback', () => {
405
422
  expect(request).not.toHaveBeenCalled()
406
423
  })
407
424
 
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
+
408
437
  it('sets ERROR when the token exchange fails', async () => {
409
438
  const request = vi.fn().mockRejectedValue(new Error('boom'))
410
439
  const {session} = setup({
@@ -439,6 +468,18 @@ describe('refreshOAuthTokens', () => {
439
468
  expect(readStored(storageArea)).toMatchObject({accessToken: 'new-access'})
440
469
  })
441
470
 
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
+
442
483
  it('shares a single in-flight request across concurrent callers', async () => {
443
484
  const pending = deferred<typeof tokenResponse>()
444
485
  const request = vi.fn().mockReturnValue(pending.promise)
@@ -48,6 +48,19 @@ function getResourceIndicator(organizationId: string): string {
48
48
  return `urn:io.sanity:organization:${organizationId}`
49
49
  }
50
50
 
51
+ /**
52
+ * Appends the RFC 8707 `resource` indicator to `params` when an organisation
53
+ * id is configured. Without one, the request is left unscoped.
54
+ */
55
+ function appendResourceIndicator(
56
+ params: URLSearchParams,
57
+ oauth: NonNullable<AuthOptions['oauth']>,
58
+ ): void {
59
+ if (oauth.organizationId) {
60
+ params.append('resource', getResourceIndicator(oauth.organizationId))
61
+ }
62
+ }
63
+
51
64
  /**
52
65
  * Serialises tokens for storage, converting `expiresAt` to an ISO string.
53
66
  *
@@ -89,6 +102,21 @@ function createOAuthClient(options: AuthOptions): SanityClient {
89
102
  })
90
103
  }
91
104
 
105
+ /** POSTs form-encoded params to the OAuth token endpoint. */
106
+ function postTokenRequest(
107
+ client: SanityClient,
108
+ params: URLSearchParams,
109
+ tag: string,
110
+ ): Promise<TokenEndpointResponse> {
111
+ return client.request<TokenEndpointResponse>({
112
+ method: 'POST',
113
+ url: '/auth/oauth/token',
114
+ headers: {'content-type': 'application/x-www-form-urlencoded'},
115
+ body: params.toString(),
116
+ tag,
117
+ })
118
+ }
119
+
92
120
  /** Converts a token endpoint response to the {@link OAuthTokens} shape. */
93
121
  function toOAuthTokens(response: TokenEndpointResponse): OAuthTokens {
94
122
  return {
@@ -140,7 +168,7 @@ export const startOAuthAuthorization = bindActionGlobally(authStore, async ({sta
140
168
  authorizeUrl.searchParams.set('state', oauthState)
141
169
  authorizeUrl.searchParams.set('code_challenge', codeChallenge)
142
170
  authorizeUrl.searchParams.set('code_challenge_method', 'S256')
143
- authorizeUrl.searchParams.append('resource', getResourceIndicator(options.oauth.organizationId))
171
+ appendResourceIndicator(authorizeUrl.searchParams, options.oauth)
144
172
 
145
173
  logger.info('Starting OAuth authorization')
146
174
  if (typeof window !== 'undefined' && typeof window.location?.assign === 'function') {
@@ -240,15 +268,9 @@ export const handleOAuthCallback = bindActionGlobally(
240
268
  code_verifier: codeVerifier,
241
269
  redirect_uri: options.oauth.redirectUri,
242
270
  client_id: options.oauth.clientId,
243
- resource: getResourceIndicator(options.oauth.organizationId),
244
- })
245
- const response = await client.request<TokenEndpointResponse>({
246
- method: 'POST',
247
- url: '/auth/oauth/token',
248
- headers: {'content-type': 'application/x-www-form-urlencoded'},
249
- body: params.toString(),
250
- tag: 'oauth.token',
251
271
  })
272
+ appendResourceIndicator(params, options.oauth)
273
+ const response = await postTokenRequest(client, params, 'oauth.token')
252
274
 
253
275
  const tokens = toOAuthTokens(response)
254
276
  options.storageArea?.setItem(options.storageKey, serializeTokens(tokens))
@@ -329,15 +351,9 @@ async function doRefreshOAuthTokens({
329
351
  grant_type: 'refresh_token',
330
352
  refresh_token: current.refreshToken,
331
353
  client_id: options.oauth.clientId,
332
- resource: getResourceIndicator(options.oauth.organizationId),
333
- })
334
- const response = await client.request<TokenEndpointResponse>({
335
- method: 'POST',
336
- url: '/auth/oauth/token',
337
- headers: {'content-type': 'application/x-www-form-urlencoded'},
338
- body: params.toString(),
339
- tag: 'oauth.refresh',
340
354
  })
355
+ appendResourceIndicator(params, options.oauth)
356
+ const response = await postTokenRequest(client, params, 'oauth.refresh')
341
357
 
342
358
  const tokens = toOAuthTokens(response)
343
359
  if (!tokens.refreshToken) tokens.refreshToken = current.refreshToken
@@ -90,8 +90,11 @@ export interface AuthConfig {
90
90
  redirectUri: string
91
91
 
92
92
  /**
93
- * Organisation id used for RFC 8707 resource scoping.
93
+ * Organisation id used for RFC 8707 resource scoping. Optional: when
94
+ * omitted, the `resource` parameter is left off the authorize and token
95
+ * requests, so the issued tokens are not restricted to a single
96
+ * organisation.
94
97
  */
95
- organizationId: string
98
+ organizationId?: string
96
99
  }
97
100
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"createSanityInstance-BQIqyGLX.d.ts","names":[],"sources":["../src/config/authConfig.ts","../src/config/sanityConfig.ts","../src/store/createSanityInstance.ts"],"mappings":";;;;;UAMiB;;;;EAIf;;;;EAKA;;;;EAKA;;;;EAKA;;;;;;;UAQe;;;;;EAKf;;;;;EAMA,iBAAiB,QAAQ,iBAAiB;;;;;;EAO1C,YAAY,mBAAmB,MAAM,mBAAmB,iBAAiB,QAAQ;;;;;EAMjF;;;;;EAMA,cAAc;;;;;;EAOd;;;;;;EAOA;EAEA;;;;IAIE;;;;;IAMA;;;;IAKA;;;;;;;;;;;UClFa;;EAEf,UAAU;IAAW,OAAO;;IAAkC;;;;;;;;;;UAU/C;;;;;;EAMf;;EAEA;;;;;;;;IAQE,QAAQ;;;;;;;UAQK,cAAc;EAC7B,YAAY;;;;;KAMF;EACV;EACA,uBAAuB;;;;;UAMR;EACf,cAAc,oBAAoB;;;;;UAMnB,cAAc,kCAAkC,4CACvD,cAAc,aAAa;EACnC,UAAU;;;;;;;EAOV,WAAW;;;;;;;;UASI,mBACf,uCACA,kCACA,4CACQ,cAAc,UAAU;EAChC;EACA,cAAc;;;;;;EAMd;;;;;;;;UASe,eACf,uCACA,kCACA,4CACQ,mBAAmB,eAAe,UAAU;EACpD;;;;;;;;;;UAWe,sBAAsB;EACrC;;;;;;UAOe,qBAAqB,eAAe;;;;EAInD,OAAO;;;;;;;;;EASP,SAAS;;;;;;;;KASC,mBAAmB,kBAAkB,uBAAuB;;;;KAK5D;EAAmB;EAAmB;;;;;KAKtC;EAAwB;;;;;KAKxB;EAAkB;;;;;iBAKd,kBAAkB,UAAU,mBAAmB,YAAY;;;;iBAO3D,uBACd,UAAU,mBACT,YAAY;;;;iBAOC,iBAAiB,UAAU,mBAAmB,YAAY;;;;;;UCtLzD;;;;;WAKN;;;;WAKA,QAAQ;;;;;EAMjB;;;;;EAMA;;;;;;EAOA,UAAU;;;;;;;;;iBAUI,qBAAqB,SAAQ,eAAoB"}