@sanity/client 7.22.0 → 7.22.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.
@@ -1134,9 +1134,9 @@ declare interface ContentSourceMapValueMapping_2 {
1134
1134
 
1135
1135
  /** @public */
1136
1136
  export declare class CorsOriginError extends Error {
1137
- projectId: string
1137
+ projectId?: string
1138
1138
  addOriginUrl?: URL
1139
- constructor({projectId}: {projectId: string})
1139
+ constructor({projectId, credentials}?: {projectId?: string; credentials?: boolean})
1140
1140
  }
1141
1141
 
1142
1142
  /**
@@ -1134,9 +1134,9 @@ declare interface ContentSourceMapValueMapping_2 {
1134
1134
 
1135
1135
  /** @public */
1136
1136
  export declare class CorsOriginError extends Error {
1137
- projectId: string
1137
+ projectId?: string
1138
1138
  addOriginUrl?: URL
1139
- constructor({projectId}: {projectId: string})
1139
+ constructor({projectId, credentials}?: {projectId?: string; credentials?: boolean})
1140
1140
  }
1141
1141
 
1142
1142
  /**
package/dist/stega.d.cts CHANGED
@@ -1134,9 +1134,9 @@ declare interface ContentSourceMapValueMapping_2 {
1134
1134
 
1135
1135
  /** @public */
1136
1136
  export declare class CorsOriginError extends Error {
1137
- projectId: string
1137
+ projectId?: string
1138
1138
  addOriginUrl?: URL
1139
- constructor({projectId}: {projectId: string})
1139
+ constructor({projectId, credentials}?: {projectId?: string; credentials?: boolean})
1140
1140
  }
1141
1141
 
1142
1142
  /**
package/dist/stega.d.ts CHANGED
@@ -1134,9 +1134,9 @@ declare interface ContentSourceMapValueMapping_2 {
1134
1134
 
1135
1135
  /** @public */
1136
1136
  export declare class CorsOriginError extends Error {
1137
- projectId: string
1137
+ projectId?: string
1138
1138
  addOriginUrl?: URL
1139
- constructor({projectId}: {projectId: string})
1139
+ constructor({projectId, credentials}?: {projectId?: string; credentials?: boolean})
1140
1140
  }
1141
1141
 
1142
1142
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/client",
3
- "version": "7.22.0",
3
+ "version": "7.22.1",
4
4
  "description": "Client for retrieving, creating and patching data from Sanity.io",
5
5
  "keywords": [
6
6
  "sanity",
package/src/data/live.ts CHANGED
@@ -1,4 +1,4 @@
1
- import {catchError, mergeMap, Observable, of} from 'rxjs'
1
+ import {catchError, mergeMap, Observable, of, throwError} from 'rxjs'
2
2
  import {finalize, map} from 'rxjs/operators'
3
3
 
4
4
  import {CorsOriginError} from '../http/errors'
@@ -122,16 +122,10 @@ export class LiveClient {
122
122
  'goaway',
123
123
  ])
124
124
 
125
- const checkCors = fetchObservable(url, {
126
- method: 'OPTIONS',
127
- mode: 'cors',
128
- credentials: esOptions.withCredentials ? 'include' : 'omit',
129
- headers: esOptions.headers,
130
- }).pipe(
131
- catchError(() => {
132
- // If the request fails, then we assume it was due to CORS, and we rethrow a special error that allows special handling in userland
133
- throw new CorsOriginError({projectId: projectId!})
134
- }),
125
+ const checkCors = checkCorsObservable(
126
+ new URL(this.#client.getUrl('/check/cors', false)),
127
+ projectId,
128
+ esOptions.withCredentials === true,
135
129
  )
136
130
 
137
131
  const observable = events
@@ -145,6 +139,12 @@ export class LiveClient {
145
139
  return of(event)
146
140
  }),
147
141
  catchError((err) => {
142
+ // If a prior `reconnect` already ran the CORS probe and produced a
143
+ // `CorsOriginError`, just rethrow it instead of calling `/check/cors`
144
+ // a second time only to get the same answer.
145
+ if (err instanceof CorsOriginError) {
146
+ return throwError(() => err)
147
+ }
148
148
  return checkCors.pipe(
149
149
  mergeMap(() => {
150
150
  // rethrow the original error if checkCors passed
@@ -172,21 +172,92 @@ export class LiveClient {
172
172
  }
173
173
  }
174
174
 
175
- function fetchObservable(url: URL, init: RequestInit) {
176
- return new Observable((observer) => {
175
+ /**
176
+ * Probes the `/check/cors` endpoint to confirm whether the current origin is
177
+ * allowed by the project's CORS configuration. EventSource failures are opaque,
178
+ * so we use this side-channel purely to tell "the server actively rejected our
179
+ * origin" apart from every other class of failure.
180
+ *
181
+ * Errors with `CorsOriginError` when either:
182
+ *
183
+ * - `requireCredentials` is `true` (the EventSource was about to send
184
+ * credentials) and `/check/cors` reports `result.withCredentials === false`.
185
+ * The credentialed request would fail due to a missing
186
+ * `access-control-allow-credentials` header. The resulting error carries
187
+ * `credentials: true` so its `addOriginUrl` deep-link pre-selects the
188
+ * "Allow credentials" toggle in the Sanity management form.
189
+ * - `/check/cors` reports `result.allowed === false` (origin is not on the
190
+ * project's CORS allow-list). The error carries `credentials: requireCredentials`
191
+ * so the deep-link still pre-selects credentials when the caller needed them.
192
+ *
193
+ * Every other outcome is intentionally treated as "we don't know": the
194
+ * observable emits a single `void` value and then completes, so downstream
195
+ * `mergeMap(() => ...)` consumers can continue. No error is surfaced for any
196
+ * of these cases:
197
+ *
198
+ * - `allowed: true` (with credentials satisfied if required) or an
199
+ * unrecognised body shape: the server did not confirm a CORS rejection.
200
+ * - Non-2xx HTTP response from `/check/cors`: same - no signal either way, and
201
+ * a 5xx on the probe shouldn't poison the EventSource's original error.
202
+ * - `fetch` / network / JSON parse failures: indistinguishable from ordinary
203
+ * connectivity hiccups (offline, DNS, certs, transient outages). Reporting
204
+ * those as CORS errors is exactly the false-positive class this helper
205
+ * exists to prevent.
206
+ * - The subscription was aborted: nothing to emit and nothing to complete.
207
+ *
208
+ * In all of those cases the caller's original underlying error from the
209
+ * EventSource is allowed to propagate unchanged.
210
+ */
211
+ function checkCorsObservable(
212
+ url: URL,
213
+ projectId: string | undefined,
214
+ requireCredentials: boolean,
215
+ ): Observable<void> {
216
+ return new Observable<void>((observer) => {
177
217
  const controller = new AbortController()
178
- const signal = controller.signal
179
- fetch(url, {...init, signal: controller.signal}).then(
180
- (response) => {
181
- observer.next(response)
182
- observer.complete()
183
- },
184
- (err) => {
185
- if (!signal.aborted) {
186
- observer.error(err)
218
+ const {signal} = controller
219
+ fetch(url, {method: 'GET', mode: 'cors', credentials: 'omit', signal})
220
+ .then((response) => {
221
+ // Aborted or non-2xx: not a confirmed CORS rejection. Fall through with
222
+ // an undefined body so the next step takes the silent-completion path.
223
+ if (signal.aborted || !response.ok) return
224
+ return response.json() as Promise<{
225
+ result?: {allowed?: boolean; withCredentials?: boolean}
226
+ }>
227
+ })
228
+ .then((body) => {
229
+ if (signal.aborted) return
230
+ // Check the credentialed case first: if the EventSource was about to
231
+ // send credentials but the project's CORS config doesn't permit them,
232
+ // the credentialed request would fail with a missing
233
+ // `access-control-allow-credentials` header. Surface this as a CORS
234
+ // rejection with `credentials: true` so the deep-link pre-selects the
235
+ // "Allow credentials" toggle.
236
+ if (requireCredentials && body?.result?.withCredentials === false) {
237
+ observer.error(new CorsOriginError({projectId, credentials: true}))
238
+ return
187
239
  }
188
- },
189
- )
240
+ // Generic case: the server actively rejected this origin. Propagate
241
+ // `credentials: requireCredentials` so the deep-link still pre-selects
242
+ // credentials when the caller needed them.
243
+ if (body?.result?.allowed === false) {
244
+ observer.error(new CorsOriginError({projectId, credentials: requireCredentials}))
245
+ return
246
+ }
247
+ // Anything else (allowed + credentials satisfied, unrecognised body)
248
+ // is treated as "not a confirmed CORS rejection" - let the caller's
249
+ // original error surface instead.
250
+ observer.next()
251
+ observer.complete()
252
+ })
253
+ // Fetch/network/JSON parse errors are intentionally ignored - see the
254
+ // helper's docblock for the rationale. We still need to settle the
255
+ // observer so downstream `mergeMap(checkCors, ...)` consumers can proceed.
256
+ .catch(() => {
257
+ if (signal.aborted || observer.closed) return
258
+ observer.next()
259
+ observer.complete()
260
+ })
190
261
  return () => controller.abort()
191
262
  })
192
263
  }
@@ -256,23 +256,33 @@ function sliceWithEllipsis(str: string, max: number) {
256
256
 
257
257
  /** @public */
258
258
  export class CorsOriginError extends Error {
259
- projectId: string
259
+ projectId?: string
260
260
  addOriginUrl?: URL
261
261
 
262
- constructor({projectId}: {projectId: string}) {
262
+ constructor({projectId, credentials}: {projectId?: string; credentials?: boolean} = {}) {
263
263
  super('CorsOriginError')
264
264
  this.name = 'CorsOriginError'
265
265
  this.projectId = projectId
266
266
 
267
- const url = new URL(`https://sanity.io/manage/project/${projectId}/api`)
268
- if (typeof location !== 'undefined') {
267
+ // Only build a deep-link when we know which project the user needs to
268
+ // configure - without `projectId` the management URL can't actually route
269
+ // them anywhere useful.
270
+ if (projectId && typeof location !== 'undefined') {
271
+ const url = new URL(`https://sanity.io/manage/project/${projectId}/api`)
269
272
  const {origin} = location
270
273
  url.searchParams.set('cors', 'add')
271
274
  url.searchParams.set('origin', origin)
275
+ if (credentials) {
276
+ // Pre-selects the "Allow credentials (token-based auth)" toggle in
277
+ // the Sanity management CORS form.
278
+ url.searchParams.set('credentials', '')
279
+ }
272
280
  this.addOriginUrl = url
273
281
  this.message = `The current origin is not allowed to connect to the Live Content API. Add it here: ${url}`
282
+ } else if (projectId) {
283
+ this.message = `The current origin is not allowed to connect to the Live Content API. Change your configuration here: https://sanity.io/manage/project/${projectId}/api`
274
284
  } else {
275
- this.message = `The current origin is not allowed to connect to the Live Content API. Change your configuration here: ${url}`
285
+ this.message = `The current origin is not allowed to connect to the Live Content API.`
276
286
  }
277
287
  }
278
288
  }
@@ -2148,14 +2148,11 @@ ${codeFrame(query, { start, end }, description)}${withTag}${withTraceId}`;
2148
2148
  class CorsOriginError extends Error {
2149
2149
  projectId;
2150
2150
  addOriginUrl;
2151
- constructor({ projectId: projectId2 }) {
2152
- super("CorsOriginError"), this.name = "CorsOriginError", this.projectId = projectId2;
2153
- const url = new URL(`https://sanity.io/manage/project/${projectId2}/api`);
2154
- if (typeof location < "u") {
2155
- const { origin } = location;
2156
- url.searchParams.set("cors", "add"), url.searchParams.set("origin", origin), this.addOriginUrl = url, this.message = `The current origin is not allowed to connect to the Live Content API. Add it here: ${url}`;
2157
- } else
2158
- this.message = `The current origin is not allowed to connect to the Live Content API. Change your configuration here: ${url}`;
2151
+ constructor({ projectId: projectId2, credentials } = {}) {
2152
+ if (super("CorsOriginError"), this.name = "CorsOriginError", this.projectId = projectId2, projectId2 && typeof location < "u") {
2153
+ const url = new URL(`https://sanity.io/manage/project/${projectId2}/api`), { origin } = location;
2154
+ url.searchParams.set("cors", "add"), url.searchParams.set("origin", origin), credentials && url.searchParams.set("credentials", ""), this.addOriginUrl = url, this.message = `The current origin is not allowed to connect to the Live Content API. Add it here: ${url}`;
2155
+ } else projectId2 ? this.message = `The current origin is not allowed to connect to the Live Content API. Change your configuration here: https://sanity.io/manage/project/${projectId2}/api` : this.message = "The current origin is not allowed to connect to the Live Content API.";
2159
2156
  }
2160
2157
  }
2161
2158
  const httpError = {
@@ -3472,19 +3469,14 @@ ${selectionOpts}`);
3472
3469
  "welcome",
3473
3470
  "reconnect",
3474
3471
  "goaway"
3475
- ]), checkCors = fetchObservable(url, {
3476
- method: "OPTIONS",
3477
- mode: "cors",
3478
- credentials: esOptions.withCredentials ? "include" : "omit",
3479
- headers: esOptions.headers
3480
- }).pipe(
3481
- catchError(() => {
3482
- throw new CorsOriginError({ projectId: projectId2 });
3483
- })
3472
+ ]), checkCors = checkCorsObservable(
3473
+ new URL(this.#client.getUrl("/check/cors", false)),
3474
+ projectId2,
3475
+ esOptions.withCredentials === true
3484
3476
  ), observable2 = events.pipe(
3485
3477
  reconnectOnConnectionFailure(),
3486
3478
  mergeMap((event) => event.type === "reconnect" ? checkCors.pipe(mergeMap(() => of(event))) : of(event)),
3487
- catchError((err) => checkCors.pipe(
3479
+ catchError((err) => err instanceof CorsOriginError ? throwError(() => err) : checkCors.pipe(
3488
3480
  mergeMap(() => {
3489
3481
  throw err;
3490
3482
  })
@@ -3505,17 +3497,27 @@ ${selectionOpts}`);
3505
3497
  return eventsCache.set(key, observable2), observable2;
3506
3498
  }
3507
3499
  }
3508
- function fetchObservable(url, init) {
3500
+ function checkCorsObservable(url, projectId2, requireCredentials) {
3509
3501
  return new Observable((observer) => {
3510
- const controller = new AbortController(), signal = controller.signal;
3511
- return fetch(url, { ...init, signal: controller.signal }).then(
3512
- (response) => {
3513
- observer.next(response), observer.complete();
3514
- },
3515
- (err) => {
3516
- signal.aborted || observer.error(err);
3502
+ const controller = new AbortController(), { signal } = controller;
3503
+ return fetch(url, { method: "GET", mode: "cors", credentials: "omit", signal }).then((response) => {
3504
+ if (!(signal.aborted || !response.ok))
3505
+ return response.json();
3506
+ }).then((body) => {
3507
+ if (!signal.aborted) {
3508
+ if (requireCredentials && body?.result?.withCredentials === false) {
3509
+ observer.error(new CorsOriginError({ projectId: projectId2, credentials: true }));
3510
+ return;
3511
+ }
3512
+ if (body?.result?.allowed === false) {
3513
+ observer.error(new CorsOriginError({ projectId: projectId2, credentials: requireCredentials }));
3514
+ return;
3515
+ }
3516
+ observer.next(), observer.complete();
3517
3517
  }
3518
- ), () => controller.abort();
3518
+ }).catch(() => {
3519
+ signal.aborted || observer.closed || (observer.next(), observer.complete());
3520
+ }), () => controller.abort();
3519
3521
  });
3520
3522
  }
3521
3523
  const eventsCache = /* @__PURE__ */ new Map();