UNPKG

@sanity/sdk

Version:
1,158 lines 229 kB
import { createClient } from "@sanity/client"; import { Observable, share, map, distinctUntilChanged, skip, filter, exhaustMap, from, timer, switchMap, takeWhile, firstValueFrom, fromEvent, EMPTY, defer, asapScheduler, combineLatest, of, concatMap, withLatestFrom, concat, throwError, first as first$1, Subject, takeUntil, partition, merge, shareReplay, tap as tap$1, catchError as catchError$1, startWith as startWith$1, pairwise as pairwise$1, groupBy as groupBy$1, mergeMap as mergeMap$1, throttle, race, NEVER, Subscription, retry, debounceTime as debounceTime$1 } from "rxjs"; import { devtools } from "zustand/middleware"; import { createStore } from "zustand/vanilla"; import { pick, omit, isEqual, isObject } from "lodash-es"; import { first, switchMap as switchMap$1, groupBy, mergeMap, startWith, pairwise, filter as filter$1, map as map$1, delay, tap, catchError, scan, share as share$1, take, debounceTime } from "rxjs/operators"; import { createController, createNode } from "@sanity/comlink"; import { createSelector } from "reselect"; import { SanityEncoder } from "@sanity/mutate"; import { getPublishedId as getPublishedId$1 } from "@sanity/client/csm"; import { jsonMatch, stringifyPath, slicePath, getIndexForKey } from "@sanity/json-match"; import { getIndexForKey as getIndexForKey2, getPathDepth, joinPaths, jsonMatch as jsonMatch2, slicePath as slicePath2, stringifyPath as stringifyPath2 } from "@sanity/json-match"; import { diffValue } from "@sanity/diff-patch"; import { applyPatches, parsePatch } from "@sanity/diff-match-patch"; import { isKeySegment, isKeyedObject } from "@sanity/types"; import { createDocumentLoaderFromClient } from "@sanity/mutate/_unstable_store"; import { SDK_CHANNEL_NAME, SDK_NODE_NAME } from "@sanity/message-protocol"; import { fromUrl } from "@sanity/bifur-client"; var AuthStateType = /* @__PURE__ */ ((AuthStateType2) => (AuthStateType2.LOGGED_IN = "logged-in", AuthStateType2.LOGGING_IN = "logging-in", AuthStateType2.ERROR = "error", AuthStateType2.LOGGED_OUT = "logged-out", AuthStateType2))(AuthStateType || {}); function getPublishedId(id) { const draftsPrefix = "drafts."; return id.startsWith(draftsPrefix) ? id.slice(draftsPrefix.length) : id; } function getDraftId(id) { const draftsPrefix = "drafts."; return id.startsWith(draftsPrefix) ? id : `${draftsPrefix}${id}`; } function insecureRandomId() { return Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join(""); } function createSanityInstance(config = {}) { const instanceId = crypto.randomUUID(), disposeListeners = /* @__PURE__ */ new Map(), disposed = { current: !1 }, instance = { instanceId, config, isDisposed: () => disposed.current, dispose: () => { disposed.current || (disposed.current = !0, disposeListeners.forEach((listener) => listener()), disposeListeners.clear()); }, onDispose: (cb) => { const listenerId = insecureRandomId(); return disposeListeners.set(listenerId, cb), () => { disposeListeners.delete(listenerId); }; }, getParent: () => { }, createChild: (next) => Object.assign( createSanityInstance({ ...config, ...next, ...config.auth === next.auth ? config.auth : config.auth && next.auth && { auth: { ...config.auth, ...next.auth } } }), { getParent: () => instance } ), match: (targetConfig) => { if (Object.entries(pick(targetConfig, "auth", "projectId", "dataset")).every( ([key, value]) => config[key] === value )) return instance; const parent = instance.getParent(); if (parent) return parent.match(targetConfig); } }; return instance; } function getEnv(key) { if (typeof import.meta < "u" && import.meta.env) return import.meta.env[key]; if (typeof process < "u" && process.env) return process.env[key]; if (typeof window < "u" && window.ENV) return window.ENV?.[key]; } function createStoreState(initialState, devToolsOptions) { const store = createStore()(devtools(() => initialState, devToolsOptions)); return { get: store.getState, set: (actionKey, updatedState) => { const currentState = store.getState(), nextState = typeof updatedState == "function" ? updatedState(currentState) : updatedState; currentState !== nextState && store.setState(nextState, !1, actionKey); }, observable: new Observable((observer) => { const emit = () => observer.next(store.getState()); emit(); const unsubscribe = store.subscribe(emit); return () => unsubscribe(); }) }; } function createStoreInstance(instance, { name, getInitialState: getInitialState2, initialize }) { const state = createStoreState(getInitialState2(instance), { enabled: !!getEnv("DEV"), name: `${name}-${instance.config.projectId}.${instance.config.dataset}` }), dispose = initialize?.({ state, instance }), disposed = { current: !1 }; return { state, dispose: () => { disposed.current || (disposed.current = !0, dispose?.()); }, isDisposed: () => disposed.current }; } function createActionBinder(keyFn) { const instanceRegistry = /* @__PURE__ */ new Map(), storeRegistry = /* @__PURE__ */ new Map(); return function(storeDefinition, action) { return function(instance, ...params) { const keySuffix = keyFn(instance.config), compositeKey = storeDefinition.name + (keySuffix ? `:${keySuffix}` : ""); let instances = instanceRegistry.get(compositeKey); instances || (instances = /* @__PURE__ */ new Set(), instanceRegistry.set(compositeKey, instances)), instances.has(instance.instanceId) || (instances.add(instance.instanceId), instance.onDispose(() => { instances.delete(instance.instanceId), instances.size === 0 && (storeRegistry.get(compositeKey)?.dispose(), storeRegistry.delete(compositeKey), instanceRegistry.delete(compositeKey)); })); let storeInstance = storeRegistry.get(compositeKey); return storeInstance || (storeInstance = createStoreInstance(instance, storeDefinition), storeRegistry.set(compositeKey, storeInstance)), action({ instance, state: storeInstance.state }, ...params); }; }; } const bindActionByDataset = createActionBinder(({ projectId, dataset }) => { if (!projectId || !dataset) throw new Error("This API requires a project ID and dataset configured."); return `${projectId}.${dataset}`; }), bindActionGlobally = createActionBinder(() => "global"); function createStateSourceAction(options) { const selector = typeof options == "function" ? options : options.selector, subscribeHandler = options && "onSubscribe" in options ? options.onSubscribe : void 0, isEqual2 = options && "isEqual" in options ? options.isEqual ?? Object.is : Object.is, selectorContextCache = /* @__PURE__ */ new WeakMap(); function stateSourceAction(context, ...params) { const { state, instance } = context, getCurrent = () => { const currentState = state.get(); if (typeof currentState != "object" || currentState === null) throw new Error( `Expected store state to be an object but got "${typeof currentState}" instead` ); let instanceCache = selectorContextCache.get(currentState); instanceCache || (instanceCache = /* @__PURE__ */ new WeakMap(), selectorContextCache.set(currentState, instanceCache)); let selectorContext = instanceCache.get(instance); return selectorContext || (selectorContext = { state: currentState, instance }, instanceCache.set(instance, selectorContext)), selector(selectorContext, ...params); }, subscribe = (onStoreChanged) => { const cleanup = subscribeHandler?.(context, ...params), subscription = state.observable.pipe( // Derive value from current state map(getCurrent), // Filter unchanged values using custom equality check distinctUntilChanged(isEqual2), // Skip initial emission since we only want changes skip(1) ).subscribe({ next: () => onStoreChanged?.(), // Propagate selector errors to both subscription types error: () => onStoreChanged?.() }); return () => { subscription.unsubscribe(), cleanup?.(); }; }, observable = new Observable((observer) => { const emitCurrent = () => { try { observer.next(getCurrent()); } catch (error) { observer.error(error); } }; return emitCurrent(), subscribe(emitCurrent); }).pipe(share()); return { getCurrent, subscribe, observable }; } return stateSourceAction; } const DEFAULT_BASE = "http://localhost", AUTH_CODE_PARAM = "sid", DEFAULT_API_VERSION$1 = "2021-06-07", REQUEST_TAG_PREFIX = "sanity.sdk.auth", REFRESH_INTERVAL = 12 * 60 * 60 * 1e3, LOCK_NAME = "sanity-token-refresh-lock"; function getLastRefreshTime(storageArea, storageKey) { try { const data = storageArea?.getItem(`${storageKey}_last_refresh`), parsed = data ? parseInt(data, 10) : 0; return isNaN(parsed) ? 0 : parsed; } catch { return 0; } } function setLastRefreshTime(storageArea, storageKey) { try { storageArea?.setItem(`${storageKey}_last_refresh`, Date.now().toString()); } catch { } } function getNextRefreshDelay(storageArea, storageKey) { const lastRefresh = getLastRefreshTime(storageArea, storageKey); if (!lastRefresh) return 0; const now = Date.now(), nextRefreshTime = lastRefresh + REFRESH_INTERVAL; return Math.max(0, nextRefreshTime - now); } function createTokenRefreshStream(token, clientFactory, apiHost) { return new Observable((subscriber) => { const subscription = clientFactory({ apiVersion: DEFAULT_API_VERSION$1, requestTagPrefix: "token-refresh", useProjectHostname: !1, useCdn: !1, token, ignoreBrowserTokenWarning: !0, ...apiHost && { apiHost } }).observable.request({ uri: "auth/refresh-token", method: "POST", body: { token } }).subscribe(subscriber); return () => subscription.unsubscribe(); }); } async function acquireTokenRefreshLock(refreshFn, storageArea, storageKey) { if (!navigator.locks) return console.warn("Web Locks API not supported. Proceeding with uncoordinated refresh."), await refreshFn(), setLastRefreshTime(storageArea, storageKey), !0; try { return await navigator.locks.request(LOCK_NAME, { mode: "exclusive" }, async (lock) => { if (!lock) return !1; for (; ; ) { const delay2 = getNextRefreshDelay(storageArea, storageKey); delay2 > 0 && await new Promise((resolve) => setTimeout(resolve, delay2)); try { await refreshFn(), setLastRefreshTime(storageArea, storageKey); } catch (error) { console.error("Token refresh failed within lock:", error); } await new Promise((resolve) => setTimeout(resolve, REFRESH_INTERVAL)); } }) === !0; } catch (error) { return console.error("Failed to request token refresh lock:", error), !1; } } function shouldRefreshToken(lastRefresh) { return lastRefresh ? Date.now() - lastRefresh >= REFRESH_INTERVAL : !0; } const refreshStampedToken = ({ state }) => { const { clientFactory, apiHost, storageArea, storageKey } = state.get().options; return state.observable.pipe( map((storeState) => ({ authState: storeState.authState, dashboardContext: storeState.dashboardContext })), filter( (storeState) => storeState.authState.type === AuthStateType.LOGGED_IN ), distinctUntilChanged( (prev, curr) => prev.authState.type === curr.authState.type && prev.authState.token === curr.authState.token && // Only care about token for distinctness here prev.dashboardContext === curr.dashboardContext ), // Make distinctness check explicit filter((storeState) => storeState.authState.token.includes("-st")), // Ensure we only try to refresh stamped tokens exhaustMap((storeState) => { const performRefresh = async () => { const currentState = state.get(); if (currentState.authState.type !== AuthStateType.LOGGED_IN) throw new Error("User logged out before refresh could complete"); const currentToken = currentState.authState.token, response = await firstValueFrom( createTokenRefreshStream(currentToken, clientFactory, apiHost) ); state.set("setRefreshStampedToken", (prev) => ({ authState: prev.authState.type === AuthStateType.LOGGED_IN ? { ...prev.authState, token: response.token } : prev.authState })), storageArea?.setItem(storageKey, JSON.stringify({ token: response.token })); }; return storeState.dashboardContext ? new Observable((subscriber) => { const visibilityHandler = () => { const currentState = state.get(); document.visibilityState === "visible" && currentState.authState.type === AuthStateType.LOGGED_IN && shouldRefreshToken(currentState.authState.lastTokenRefresh) && createTokenRefreshStream( currentState.authState.token, clientFactory, apiHost ).subscribe({ next: (response) => { state.set("setRefreshStampedToken", (prev) => ({ authState: prev.authState.type === AuthStateType.LOGGED_IN ? { ...prev.authState, token: response.token, lastTokenRefresh: Date.now() } : prev.authState })), subscriber.next(response); }, error: (error) => subscriber.error(error) }); }, timerSubscription = timer(REFRESH_INTERVAL, REFRESH_INTERVAL).pipe( filter(() => document.visibilityState === "visible"), switchMap(() => { const currentState = state.get().authState; if (currentState.type !== AuthStateType.LOGGED_IN) throw new Error("User logged out before refresh could complete"); return createTokenRefreshStream(currentState.token, clientFactory, apiHost); }) ).subscribe({ next: (response) => { state.set("setRefreshStampedToken", (prev) => ({ authState: prev.authState.type === AuthStateType.LOGGED_IN ? { ...prev.authState, token: response.token, lastTokenRefresh: Date.now() } : prev.authState })), subscriber.next(response); }, error: (error) => subscriber.error(error) }); return document.addEventListener("visibilitychange", visibilityHandler), () => { document.removeEventListener("visibilitychange", visibilityHandler), timerSubscription.unsubscribe(); }; }).pipe( takeWhile(() => state.get().authState.type === AuthStateType.LOGGED_IN), map((response) => ({ token: response.token })) ) : from(acquireTokenRefreshLock(performRefresh, storageArea, storageKey)).pipe( filter((hasLock) => hasLock), map(() => { const currentState = state.get().authState; if (currentState.type !== AuthStateType.LOGGED_IN) throw new Error("User logged out before refresh could complete"); return { token: currentState.token }; }) ); }) ).subscribe({ next: (response) => { state.set("setRefreshStampedToken", (prev) => ({ authState: prev.authState.type === AuthStateType.LOGGED_IN ? { ...prev.authState, token: response.token, lastTokenRefresh: Date.now() } : prev.authState })), storageArea?.setItem(storageKey, JSON.stringify({ token: response.token })); }, error: (error) => { state.set("setRefreshStampedTokenError", { authState: { type: AuthStateType.ERROR, error } }); } }); }; function getAuthCode(callbackUrl, locationHref) { const loc = new URL(locationHref, DEFAULT_BASE), callbackLocation = callbackUrl ? new URL(callbackUrl, DEFAULT_BASE) : void 0, callbackLocationMatches = callbackLocation ? loc.pathname.toLowerCase().startsWith(callbackLocation.pathname.toLowerCase()) : !0; let authCode = new URLSearchParams(loc.hash.slice(1)).get(AUTH_CODE_PARAM) || new URLSearchParams(loc.search).get(AUTH_CODE_PARAM); if (!authCode) { const contextParam = new URLSearchParams(loc.search).get("_context"); if (contextParam) try { const parsedContext = JSON.parse(contextParam); parsedContext && typeof parsedContext == "object" && typeof parsedContext.sid == "string" && parsedContext.sid && (authCode = parsedContext.sid); } catch { } } return authCode && callbackLocationMatches ? authCode : null; } function getTokenFromLocation(locationHref) { const loc = new URL(locationHref); return new URLSearchParams(loc.hash.slice(1)).get("token") || null; } function getTokenFromStorage(storageArea, storageKey) { if (!storageArea) return null; const item = storageArea.getItem(storageKey); if (item === null) return null; try { const parsed = JSON.parse(item); if (typeof parsed != "object" || parsed === null || !("token" in parsed) || typeof parsed.token != "string") throw new Error("Invalid stored auth data structure"); return parsed.token; } catch { return storageArea.removeItem(storageKey), null; } } function getStorageEvents() { return typeof window < "u" && typeof window.addEventListener == "function" ? fromEvent(window, "storage") : EMPTY; } function getDefaultStorage() { try { return typeof localStorage < "u" && typeof localStorage.getItem == "function" ? localStorage : void 0; } catch { return; } } function getDefaultLocation() { try { return typeof location > "u" ? DEFAULT_BASE : typeof location.href == "string" ? location.href : DEFAULT_BASE; } catch { return DEFAULT_BASE; } } function getCleanedUrl(locationUrl) { const loc = new URL(locationUrl); return loc.hash = "", loc.searchParams.delete("sid"), loc.searchParams.delete("url"), loc.toString(); } async function checkForCookieAuth(projectId, clientFactory) { if (!projectId) return !1; try { return typeof (await clientFactory({ projectId, useCdn: !1 }).request({ uri: "/users/me", withCredentials: !0, tag: "users.get-current" }))?.id == "string"; } catch { return !1; } } function getStudioTokenFromLocalStorage(storageArea, projectId) { if (!storageArea || !projectId) return null; const studioStorageKey = `__studio_auth_token_${projectId}`; return getTokenFromStorage(storageArea, studioStorageKey) || null; } const subscribeToStateAndFetchCurrentUser = ({ state }) => { const { clientFactory, apiHost } = state.get().options; return state.observable.pipe( map(({ authState }) => authState), filter( (authState) => authState.type === AuthStateType.LOGGED_IN && !authState.currentUser ), map((authState) => authState.token), distinctUntilChanged() ).pipe( map( (token) => clientFactory({ apiVersion: DEFAULT_API_VERSION$1, requestTagPrefix: REQUEST_TAG_PREFIX, token, ignoreBrowserTokenWarning: !0, useProjectHostname: !1, useCdn: !1, ...apiHost && { apiHost } }) ), switchMap( (client) => client.observable.request({ uri: "/users/me", method: "GET" }) ) ).subscribe({ next: (currentUser) => { state.set("setCurrentUser", (prev) => ({ authState: prev.authState.type === AuthStateType.LOGGED_IN ? { ...prev.authState, currentUser } : prev.authState })); }, error: (error) => { state.set("setError", { authState: { type: AuthStateType.ERROR, error } }); } }); }, subscribeToStorageEventsAndSetToken = ({ state }) => { const { storageArea, storageKey } = state.get().options; return defer(getStorageEvents).pipe( filter( (e3) => e3.storageArea === storageArea && e3.key === storageKey ), map(() => getTokenFromStorage(storageArea, storageKey)), distinctUntilChanged() ).subscribe((token) => { state.set("updateTokenFromStorageEvent", { authState: token ? { type: AuthStateType.LOGGED_IN, token, currentUser: null } : { type: AuthStateType.LOGGED_OUT, isDestroyingSession: !1 } }); }); }; let tokenRefresherRunning = !1; const authStore = { name: "Auth", getInitialState(instance) { const { apiHost, callbackUrl, providers: customProviders, token: providedToken, clientFactory = createClient, initialLocationHref = getDefaultLocation() } = instance.config.auth ?? {}; let storageArea = instance.config.auth?.storageArea; const storageKey = "__sanity_auth_token"; let loginDomain = "https://www.sanity.io"; try { apiHost && new URL(apiHost).hostname.endsWith(".sanity.work") && (loginDomain = "https://www.sanity.work"); } catch { } const loginUrl = new URL("/login", loginDomain); loginUrl.searchParams.set("origin", getCleanedUrl(initialLocationHref)), loginUrl.searchParams.set("type", "stampedToken"), loginUrl.searchParams.set("withSid", "true"); let dashboardContext = {}, isInDashboard = !1; try { const contextParam = new URL(initialLocationHref).searchParams.get("_context"); if (contextParam) { const parsedContext = JSON.parse(contextParam); parsedContext && typeof parsedContext == "object" && Object.keys(parsedContext).length > 0 && (delete parsedContext.sid, dashboardContext = parsedContext, isInDashboard = !0); } } catch (err) { console.error("Failed to parse dashboard context from initial location:", err); } isInDashboard || (storageArea = storageArea ?? getDefaultStorage()); let token, authMethod; instance.config.studioMode?.enabled ? (token = getStudioTokenFromLocalStorage(storageArea, instance.config.projectId), token ? authMethod = "localstorage" : checkForCookieAuth(instance.config.projectId, clientFactory).then((isCookieAuthEnabled) => { isCookieAuthEnabled && (authMethod = "cookie"); })) : (token = getTokenFromStorage(storageArea, storageKey), token && (authMethod = "localstorage")); let authState; return providedToken ? authState = { type: AuthStateType.LOGGED_IN, token: providedToken, currentUser: null } : getAuthCode(callbackUrl, initialLocationHref) || getTokenFromLocation(initialLocationHref) ? authState = { type: AuthStateType.LOGGING_IN, isExchangingToken: !1 } : token && !isInDashboard ? authState = { type: AuthStateType.LOGGED_IN, token, currentUser: null } : authState = { type: AuthStateType.LOGGED_OUT, isDestroyingSession: !1 }, { authState, dashboardContext, options: { apiHost, loginUrl: loginUrl.toString(), callbackUrl, customProviders, providedToken, clientFactory, initialLocationHref, storageKey, storageArea, authMethod } }; }, initialize(context) { const subscriptions = []; return subscriptions.push(subscribeToStateAndFetchCurrentUser(context)), context.state.get().options?.storageArea && subscriptions.push(subscribeToStorageEventsAndSetToken(context)), tokenRefresherRunning || (tokenRefresherRunning = !0, subscriptions.push(refreshStampedToken(context))), () => { for (const subscription of subscriptions) subscription.unsubscribe(); }; } }, getCurrentUserState = bindActionGlobally( authStore, createStateSourceAction( ({ state: { authState } }) => authState.type === AuthStateType.LOGGED_IN ? authState.currentUser : null ) ), getTokenState = bindActionGlobally( authStore, createStateSourceAction( ({ state: { authState } }) => authState.type === AuthStateType.LOGGED_IN ? authState.token : null ) ), getAuthMethodState = bindActionGlobally( authStore, createStateSourceAction(({ state: { options } }) => options.authMethod) ), getLoginUrlState = bindActionGlobally( authStore, createStateSourceAction(({ state: { options } }) => options.loginUrl) ), getAuthState = bindActionGlobally( authStore, createStateSourceAction(({ state: { authState } }) => authState) ), getDashboardOrganizationId$1 = bindActionGlobally( authStore, createStateSourceAction(({ state: { dashboardContext } }) => dashboardContext?.orgId) ), getIsInDashboardState = bindActionGlobally( authStore, createStateSourceAction( ({ state: { dashboardContext } }) => ( // Check if dashboardContext exists and is not empty !!dashboardContext && Object.keys(dashboardContext).length > 0 ) ) ), setAuthToken = bindActionGlobally(authStore, ({ state }, token) => { const currentAuthState = state.get().authState; token ? (currentAuthState.type !== AuthStateType.LOGGED_IN || currentAuthState.token !== token) && state.set("setToken", { authState: { type: AuthStateType.LOGGED_IN, token, // Keep existing user or set to null? Setting to null forces refetch. // Keep existing user to avoid unnecessary refetches if user data is still valid. currentUser: currentAuthState.type === AuthStateType.LOGGED_IN ? currentAuthState.currentUser : null } }) : currentAuthState.type !== AuthStateType.LOGGED_OUT && state.set("setToken", { authState: { type: AuthStateType.LOGGED_OUT, isDestroyingSession: !1 } }); }); function compareProjectOrganization(projectId, projectOrganizationId, currentDashboardOrgId) { return projectOrganizationId !== currentDashboardOrgId ? { error: `Project ${projectId} belongs to Organization ${projectOrganizationId ?? "unknown"}, but the Dashboard has Organization ${currentDashboardOrgId} selected` } : { error: null }; } const DEFAULT_API_VERSION = "2024-11-12", DEFAULT_REQUEST_TAG_PREFIX = "sanity.sdk", allowedKeys = Object.keys({ apiHost: null, useCdn: null, token: null, perspective: null, proxy: null, withCredentials: null, timeout: null, maxRetries: null, dataset: null, projectId: null, scope: null, apiVersion: null, requestTagPrefix: null, useProjectHostname: null, "~experimental_resource": null }), DEFAULT_CLIENT_CONFIG = { apiVersion: DEFAULT_API_VERSION, useCdn: !1, ignoreBrowserTokenWarning: !0, allowReconfigure: !1, requestTagPrefix: DEFAULT_REQUEST_TAG_PREFIX }, clientStore = { name: "clientStore", getInitialState: (instance) => ({ clients: {}, token: getTokenState(instance).getCurrent() }), initialize(context) { const subscription = listenToToken(context), authMethodSubscription = listenToAuthMethod(context); return () => { subscription.unsubscribe(), authMethodSubscription.unsubscribe(); }; } }, listenToToken = ({ instance, state }) => getTokenState(instance).observable.subscribe((token) => { state.set("setTokenAndResetClients", { token, clients: {} }); }), listenToAuthMethod = ({ instance, state }) => getAuthMethodState(instance).observable.subscribe((authMethod) => { state.set("setAuthMethod", { authMethod }); }), getClientConfigKey = (options) => JSON.stringify(pick(options, ...allowedKeys)), getClient = bindActionGlobally( clientStore, ({ state, instance }, options) => { const disallowedKeys = Object.keys(options).filter((key2) => !allowedKeys.includes(key2)); if (disallowedKeys.length > 0) { const listFormatter = new Intl.ListFormat("en", { style: "long", type: "conjunction" }); throw new Error( `The client options provided contains unsupported properties: ${listFormatter.format(disallowedKeys)}. Allowed keys are: ${listFormatter.format(allowedKeys)}.` ); } const tokenFromState = state.get().token, { clients, authMethod } = state.get(), projectId = options.projectId ?? instance.config.projectId, dataset = options.dataset ?? instance.config.dataset, apiHost = options.apiHost ?? instance.config.auth?.apiHost, effectiveOptions = { ...DEFAULT_CLIENT_CONFIG, ...(options.scope === "global" || !projectId) && { useProjectHostname: !1 }, token: authMethod === "cookie" ? void 0 : tokenFromState ?? void 0, ...options, ...projectId && { projectId }, ...dataset && { dataset }, ...apiHost && { apiHost } }; effectiveOptions.token === null || typeof effectiveOptions.token > "u" ? (delete effectiveOptions.token, authMethod === "cookie" && (effectiveOptions.withCredentials = !0)) : delete effectiveOptions.withCredentials; const key = getClientConfigKey(effectiveOptions); if (clients[key]) return clients[key]; const client = createClient(effectiveOptions); return state.set("addClient", (prev) => ({ clients: { ...prev.clients, [key]: client } })), client; } ), getClientState = bindActionGlobally( clientStore, createStateSourceAction(({ instance }, options) => getClient(instance, options)) ); function createFetcherStore({ name, fetcher: getObservable, getKey, fetchThrottleInternal = 1e3, stateExpirationDelay = 5e3 }) { const store = { name, getInitialState: () => ({ stateByParams: {} }), initialize: (context) => { const subscription = subscribeToSubscriptionsAndFetch(context); return () => subscription.unsubscribe(); } }, subscribeToSubscriptionsAndFetch = ({ state }) => state.observable.pipe( // Map the state to an array of [serialized, entry] pairs. switchMap$1((s2) => { const entries = Object.entries(s2.stateByParams); return entries.length > 0 ? from(entries) : EMPTY; }), // Group by the serialized key. groupBy(([key]) => key), mergeMap( (group$) => group$.pipe( // Emit an initial value for pairwise comparisons. startWith([group$.key, void 0]), pairwise(), // Trigger only when the subscriptions array grows. filter$1(([[, prevEntry], [, currEntry]]) => { const prevSubs = prevEntry?.subscriptions ?? []; return (currEntry?.subscriptions ?? []).length > prevSubs.length; }), map$1(([, [, currEntry]]) => currEntry), // Only trigger if we haven't fetched recently. filter$1((entry) => { const lastFetch = entry?.lastFetchInitiatedAt; return lastFetch ? Date.now() - new Date(lastFetch).getTime() >= fetchThrottleInternal : !0; }), switchMap$1((entry) => entry ? (state.set("setLastFetchInitiatedAt", (prev) => ({ stateByParams: { ...prev.stateByParams, [entry.key]: { ...entry, ...prev.stateByParams[entry.key], lastFetchInitiatedAt: (/* @__PURE__ */ new Date()).toISOString() } } })), getObservable(entry.instance)(...entry.params).pipe( // the `createStateSourceAction` util requires the update // to delay(0, asapScheduler), tap( (data) => state.set("setData", (prev) => ({ stateByParams: { ...prev.stateByParams, [entry.key]: { ...omit(entry, "error"), ...omit(prev.stateByParams[entry.key], "error"), data } } })) ), catchError((error) => (state.set("setError", (prev) => ({ stateByParams: { ...prev.stateByParams, [entry.key]: { ...entry, ...prev.stateByParams[entry.key], error } } })), EMPTY)) )) : EMPTY) ) ) ).subscribe({ error: (error) => state.set("setError", { error }) }), getState = bindActionGlobally( store, createStateSourceAction({ selector: ({ instance, state: { stateByParams, error } }, ...params) => { if (error) throw error; const key = getKey(instance, ...params), entry = stateByParams[key]; if (entry?.error) throw entry.error; return entry?.data; }, onSubscribe: ({ instance, state }, ...params) => { const subscriptionId = insecureRandomId(), key = getKey(instance, ...params); return state.set("addSubscription", (prev) => ({ stateByParams: { ...prev.stateByParams, [key]: { ...prev.stateByParams[key], instance, key, params: prev.stateByParams[key]?.params || params, subscriptions: [...prev.stateByParams[key]?.subscriptions || [], subscriptionId] } } })), () => { setTimeout(() => { state.set("removeSubscription", (prev) => { const entry = prev.stateByParams[key]; if (!entry) return prev; const newSubs = (entry.subscriptions || []).filter((id) => id !== subscriptionId); return newSubs.length === 0 ? { stateByParams: omit(prev.stateByParams, key) } : { stateByParams: { ...prev.stateByParams, [key]: { ...entry, subscriptions: newSubs } } }; }); }, stateExpirationDelay); }; } }) ), resolveState = bindActionGlobally( store, ({ instance }, ...params) => firstValueFrom(getState(instance, ...params).observable.pipe(first((i2) => i2 !== void 0))) ); return { getState, resolveState }; } const API_VERSION$5 = "v2025-02-19", project = createFetcherStore({ name: "Project", getKey: (instance, options) => { const projectId = options?.projectId ?? instance.config.projectId; if (!projectId) throw new Error("A projectId is required to use the project API."); return projectId; }, fetcher: (instance) => (options = {}) => { const projectId = options.projectId ?? instance.config.projectId; return getClientState(instance, { apiVersion: API_VERSION$5, scope: "global", projectId }).observable.pipe( switchMap( (client) => client.observable.projects.getById( // non-null assertion is fine with the above throwing projectId ?? instance.config.projectId ) ) ); } }), getProjectState = project.getState, resolveProject = project.resolveState, getDashboardOrganizationId = bindActionGlobally( authStore, createStateSourceAction(({ state: { dashboardContext } }) => dashboardContext?.orgId) ); function observeOrganizationVerificationState(instance, projectIds) { const dashboardOrgId$ = getDashboardOrganizationId(instance).observable.pipe(distinctUntilChanged()), projectOrgIdObservables = projectIds.map( (id) => getProjectState(instance, { projectId: id }).observable.pipe( map((project2) => ({ projectId: id, orgId: project2?.organizationId ?? null })), // Ensure we only proceed if the orgId is loaded, distinct prevents unnecessary checks distinctUntilChanged((prev, curr) => prev.orgId === curr.orgId) ) ), allProjectOrgIds$ = projectOrgIdObservables.length > 0 ? combineLatest(projectOrgIdObservables) : of([]); return combineLatest([dashboardOrgId$, allProjectOrgIds$]).pipe( switchMap(([dashboardOrgId, projectOrgDataArray]) => { if (!dashboardOrgId || projectOrgDataArray.length === 0) return of({ error: null }); for (const projectData of projectOrgDataArray) { if (!projectData.orgId) continue; const result = compareProjectOrganization( projectData.projectId, projectData.orgId, dashboardOrgId ); if (result.error) return of(result); } return of({ error: null }); }), // Only emit when the overall error status actually changes distinctUntilChanged((prev, curr) => prev.error === curr.error) ); } const handleAuthCallback = bindActionGlobally( authStore, async ({ state }, locationHref = getDefaultLocation()) => { const { providedToken, callbackUrl, clientFactory, apiHost, storageArea, storageKey } = state.get().options; if (providedToken) return !1; const { authState } = state.get(); if (authState.type === AuthStateType.LOGGING_IN && authState.isExchangingToken) return !1; const cleanedUrl = getCleanedUrl(locationHref), tokenFromUrl = getTokenFromLocation(locationHref); if (tokenFromUrl) return state.set("setTokenFromUrl", { authState: { type: AuthStateType.LOGGED_IN, token: tokenFromUrl, currentUser: null } }), cleanedUrl; const authCode = getAuthCode(callbackUrl, locationHref); if (!authCode) return !1; const parsedUrl = new URL(locationHref); let dashboardContext = {}; try { const contextParam = parsedUrl.searchParams.get("_context"); if (contextParam) { const parsedContext = JSON.parse(contextParam); parsedContext && typeof parsedContext == "object" && (delete parsedContext.sid, dashboardContext = parsedContext); } } catch (err) { console.error("Failed to parse dashboard context:", err); } state.set("exchangeSessionForToken", { authState: { type: AuthStateType.LOGGING_IN, isExchangingToken: !0 }, dashboardContext }); try { const client = clientFactory({ apiVersion: DEFAULT_API_VERSION$1, requestTagPrefix: REQUEST_TAG_PREFIX, useProjectHostname: !1, useCdn: !1, ...apiHost && { apiHost } }), { token } = await client.request({ method: "GET", uri: "/auth/fetch", query: { sid: authCode }, tag: "fetch-token" }); return storageArea?.setItem(storageKey, JSON.stringify({ token })), state.set("setToken", { authState: { type: AuthStateType.LOGGED_IN, token, currentUser: null } }), cleanedUrl; } catch (error) { return state.set("exchangeSessionForTokenError", { authState: { type: AuthStateType.ERROR, error } }), cleanedUrl; } } ), logout = bindActionGlobally(authStore, async ({ state }) => { const { clientFactory, apiHost, providedToken, storageArea, storageKey } = state.get().options; if (providedToken) return; const { authState } = state.get(); if (authState.type === AuthStateType.LOGGED_OUT && authState.isDestroyingSession) return; const token = authState.type === AuthStateType.LOGGED_IN && authState.token; try { token && (state.set("loggingOut", { authState: { type: AuthStateType.LOGGED_OUT, isDestroyingSession: !0 } }), await clientFactory({ token, requestTagPrefix: REQUEST_TAG_PREFIX, apiVersion: DEFAULT_API_VERSION$1, ...apiHost && { apiHost }, useProjectHostname: !1, useCdn: !1 }).request({ uri: "/auth/logout", method: "POST" })); } finally { state.set("logoutSuccess", { authState: { type: AuthStateType.LOGGED_OUT, isDestroyingSession: !1 } }), storageArea?.removeItem(storageKey), storageArea?.removeItem(`${storageKey}_last_refresh`); } }), destroyController$1 = ({ state }) => { const { controller } = state.get(); controller && (controller.destroy(), state.set("destroyController", { controller: null, channels: /* @__PURE__ */ new Map() })); }, getOrCreateChannel$1 = ({ state }, options) => { const controller = state.get().controller; if (!controller) throw new Error("Controller must be initialized before using or creating channels"); const channels = state.get().channels, existing = channels.get(options.name); if (existing) { if (!isEqual(existing.options, options)) throw new Error(`Channel "${options.name}" already exists with different options`); return state.set("incrementChannelRefCount", { channels: new Map(channels).set(options.name, { ...existing, refCount: existing.refCount + 1 }) }), existing.channel.start(), existing.channel; } const channel = controller.createChannel(options); return channel.start(), state.set("createChannel", { channels: new Map(channels).set(options.name, { channel, options, refCount: 1 }) }), channel; }, getOrCreateController$1 = ({ state, instance }, targetOrigin) => { const { controller, controllerOrigin } = state.get(); if (controller && controllerOrigin === targetOrigin) return controller; controller && destroyController$1({ state }); const newController = createController({ targetOrigin }); return state.set("initializeController", { controllerOrigin: targetOrigin, controller: newController }), newController; }, releaseChannel$1 = ({ state }, name) => { const channels = state.get().channels, channelEntry = channels.get(name); if (channelEntry) { const newRefCount = channelEntry.refCount === 0 ? 0 : channelEntry.refCount - 1; newRefCount === 0 ? (channelEntry.channel.stop(), channels.delete(name), state.set("releaseChannel", { channels: new Map(channels) })) : state.set("releaseChannel", { channels: new Map(channels).set(name, { ...channelEntry, refCount: newRefCount }) }); } }, comlinkControllerStore = { name: "connectionStore", getInitialState: () => ({ controller: null, controllerOrigin: null, channels: /* @__PURE__ */ new Map() }), initialize({ instance }) { return () => { destroyController(instance); }; } }, destroyController = bindActionGlobally( comlinkControllerStore, destroyController$1 ), getOrCreateChannel = bindActionGlobally( comlinkControllerStore, getOrCreateChannel$1 ), getOrCreateController = bindActionGlobally( comlinkControllerStore, getOrCreateController$1 ), releaseChannel = bindActionGlobally(comlinkControllerStore, releaseChannel$1), getOrCreateNode$1 = ({ state }, options) => { const nodes = state.get().nodes, existing = nodes.get(options.name); if (existing) { if (!isEqual(existing.options, options)) throw new Error(`Node "${options.name}" already exists with different options`); return existing.node.start(), existing.node; } const node = createNode(options); node.start(); const statusUnsub = node.onStatus((status) => { const currentNodes = state.get().nodes, currentEntry = currentNodes.get(options.name); if (!currentEntry) return; const updatedEntry = { ...currentEntry, status }; state.set("updateNodeStatus", { nodes: new Map(currentNodes).set(options.name, updatedEntry) }); }), entry = { node, options, status: "idle", statusUnsub }; return nodes.set(options.name, entry), state.set("createNode", { nodes }), node; }, releaseNode$1 = ({ state }, name) => { const nodes = state.get().nodes, existing = nodes.get(name); if (existing) { existing.statusUnsub && existing.statusUnsub(), existing.node.stop(), nodes.delete(name), state.set("removeNode", { nodes }); return; } }, comlinkNodeStore = { name: "nodeStore", getInitialState: () => ({ nodes: /* @__PURE__ */ new Map(), subscriptions: /* @__PURE__ */ new Map() }), initialize({ state }) { return () => { state.get().nodes.forEach(({ node }) => { node.stop(); }); }; } }, releaseNode = bindActionGlobally(comlinkNodeStore, releaseNode$1), getOrCreateNode = bindActionGlobally(comlinkNodeStore, getOrCreateNode$1), NODE_RELEASE_TIME = 5e3, selectNode = (context, nodeInput) => context.state.nodes.get(nodeInput.name), getNodeState = bindActionGlobally( comlinkNodeStore, createStateSourceAction({ selector: createSelector([selectNode], (nodeEntry) => nodeEntry?.status === "connected" ? { node: nodeEntry.node, status: nodeEntry.status } : void 0), onSubscribe: ({ state, instance }, nodeInput) => { const nodeName = nodeInput.name, subscriberId = Symbol("comlink-node-subscriber"); getOrCreateNode(instance, nodeInput); let subs = state.get().subscriptions.get(nodeName); return subs || (subs = /* @__PURE__ */ new Set(), state.get().subscriptions.set(nodeName, subs)), subs.add(subscriberId), () => { setTimeout(() => { const activeSubs = state.get().subscriptions.get(nodeName); activeSubs && (activeSubs.delete(subscriberId), activeSubs.size === 0 && (state.get().subscriptions.delete(nodeName), releaseNode(instance, nodeName))); }, NODE_RELEASE_TIME); }; } }) ); function createDocumentHandle(handle) { return handle; } function createDocumentTypeHandle(handle) { return handle; } function createProjectHandle(handle) { return handle; } function createDatasetHandle(handle) { return handle; } const API_VERSION$4 = "v2025-02-19", datasets = createFetcherStore({ name: "Datasets", getKey: (instance, options) => { const projectId = options?.projectId ?? instance.config.projectId; if (!projectId) throw new Error("A projectId is required to use the project API."); return projectId; }, fetcher: (instance) => (options) => getClientState(instance, { apiVersion: API_VERSION$4, // non-null assertion is fine because we check above projectId: options?.projectId ?? instance.config.projectId, useProjectHostname: !0 }).observable.pipe(switchMap((client) => client.observable.datasets.list())) }), getDatasetsState = datasets.getState, resolveDatasets = datasets.resolveState, isSanityMutatePatch = (value) => !(typeof value != "object" || !value || !("type" in value) || typeof value.type != "string" || value.type !== "patch" || !("id" in value) || typeof value.id != "string" || !("patches" in value) || !Array.isArray(value.patches)); function createDocument(doc) { return { type: "document.create", ...doc, ...doc.documentId && { documentId: getPublishedId(doc.documentId) } }; } function deleteDocument(doc) { return { type: "document.delete", ...doc, documentId: getPublishedId(doc.documentId) }; } function convertSanityMutatePatch(sanityPatchMutation) { return SanityEncoder.encode(sanityPatchMutation).map((i2) => { const copy = { ...i2.patch }; return "id" in copy && delete copy.id, copy; }); } function editDocument(doc, patches) { if (isSanityMutatePatch(patches)) { const converted = convertSanityMutatePatch(patches) ?? []; return { ...doc, type: "document.edit", documentId: getPublishedId(doc.documentId), patches: converted }; } return { ...doc, type: "document.edit", documentId: getPublishedId(doc.documentId), ...patches && { patches: Array.isArray(patches) ? patches : [patches] } }; } function publishDocument(doc) { return { type: "document.publish", ...doc, documentId: getPublishedId(doc.documentId) }; } function unpublishDocument(doc) { return { type: "document.unpublish", ...doc, documentId: getPublishedId(doc.documentId) }; } function discardDocument(doc) { return { type: "document.discard", ...doc, documentId: getPublishedId(doc.documentId) }; } const DOCUMENT_STATE_CLEAR_DELAY = 1e3, INITIAL_OUTGOING_THROTTLE_TIME = 1e3, API_VERSION$3 = "v2025-05-06"; function generateArrayKey(length = 12) { const numBytes = Math.ceil(length / 2), bytes = crypto.getRandomValues(new Uint8Array(numBytes)); return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, length); } function memoize(fn) { const cache = /* @__PURE__ */ new WeakMap(); return (input) => { if (!input || typeof input != "object") return fn(input); const cached = cache.get(input); if (cached) return cached; const result = fn(input); return cache.set(input, result), result; }; } const ensureArrayKeysDeep = memoize((input) => { if (!input || typeof input != "object") return input; if (Array.isArray(input)) return !input.length || typeof input[0] != "object" || input.every(isKeyedObject) ? input : input.map((item) => !item || typeof item != "object" ? item : isKeyedObject(item) ? ensureArrayKeysDeep(item) : { ...ensureArrayKeysDeep(item), _key: generateArrayKey() }); const entries = Object.entries(input).map(([key, value]) => [key, ensureArrayKeysDeep(value)]); return entries.every(([key, value]) => input[key] === value) ? input : Object.fromEntries(entries); }); function set(input, pathExpressionValues) { const result = Object.entries(pathExpressionValues).flatMap( ([pathExpression, replacementValue]) => Array.from(jsonMatch(input, pathExpression)).map((matchEntry) => ({ ...matchEntry, replacementValue })) ).reduce((acc, { path, replacementValue }) => setDeep(acc, path, replacementValue), input); return ensureArrayKeysDeep(result); } function setIfMissing(input, pathExpressionValues) { const result = Object.entries(pathExpressionValues).flatMap(([pathExpression, replacementValue]) => Array.from(jsonMatch(input, pathExpression)).map((matchEntry) => ({ ...matchEntry, replacementValue }))).filter((matchEntry) => matchEntry.value === null || matchEntry.value === void 0).reduce((acc, { path, replacementValue }) => setDeep(acc, path, replacementValue), input); return ensureArrayKeysDeep(result); } function unset(input, pathExpressions) { const result = pathExpressions.flatMap((pathExpression) => Array.from(jsonMatch(input, pathExpression))).reverse().reduce((acc, { path }) => unsetDeep(acc, path), input); return ensureArrayKeysDeep(result); } function insert(input, { items, ...insertPatch }) { let operation, pathExpression; if ("before" in insertPatch ? (operation = "before", pathExpression = insertPatch.before) : "after" in insertPatch ? (operation = "after", pathExpression = insertPatch.after) : "replace" in insertPatch && (operation = "replace", pathExpression = insertPatch.replace), !operation || typeof pathExpression != "string" || !pathExpression.length) return input; const arrayPath = slicePath(pathExpression, 0, -1)