@sanity/sdk
Version:
1,136 lines • 176 kB
JavaScript
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 { createClient, CorsOriginError } from "@sanity/client";
import { pick, omit, isEqual, isObject } from "lodash-es";
import { devtools } from "zustand/middleware";
import { createStore } from "zustand/vanilla";
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 { evaluateSync, parse } from "groq-js";
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";
const SOURCE_ID = "__sanity_internal_sourceId";
function datasetSource(projectId, dataset) {
return { [SOURCE_ID]: { projectId, dataset } };
}
function mediaLibrarySource(id) {
return { [SOURCE_ID]: ["media-library", id] };
}
function canvasSource(id) {
return { [SOURCE_ID]: ["canvas", id] };
}
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, key, { name, getInitialState: getInitialState2, initialize }) {
const state = createStoreState(getInitialState2(instance, key), {
enabled: !!getEnv("DEV"),
name: `${name}-${key.name}`
}), dispose = initialize?.({ state, instance, key }), 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 key = keyFn(instance, ...params), compositeKey = storeDefinition.name + (key.name ? `:${key.name}` : "");
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, key, storeDefinition), storeRegistry.set(compositeKey, storeInstance)), action({ instance, state: storeInstance.state, key }, ...params);
};
};
}
const bindActionByDataset = createActionBinder((instance, options) => {
const projectId = options?.projectId ?? instance.config.projectId, dataset = options?.dataset ?? instance.config.dataset;
if (!projectId || !dataset)
throw new Error("This API requires a project ID and dataset configured.");
return { name: `${projectId}.${dataset}`, projectId, dataset };
}), bindActionBySource = createActionBinder((instance, { source }) => {
if (source) {
const id = source[SOURCE_ID];
if (!id) throw new Error("Invalid source (missing ID information)");
return Array.isArray(id) ? { name: id.join(":") } : { name: `${id.projectId}.${id.dataset}` };
}
const { projectId, dataset } = instance.config;
if (!projectId || !dataset)
throw new Error("This API requires a project ID and dataset configured.");
return { name: `${projectId}.${dataset}` };
}), bindActionGlobally = createActionBinder((..._rest) => ({ name: "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;
}
var AuthStateType = /* @__PURE__ */ ((AuthStateType2) => (AuthStateType2.LOGGED_IN = "logged-in", AuthStateType2.LOGGING_IN = "logging-in", AuthStateType2.ERROR = "error", AuthStateType2.LOGGED_OUT = "logged-out", AuthStateType2))(AuthStateType || {});
const DEFAULT_BASE = "http://localhost", AUTH_CODE_PARAM = "sid", DEFAULT_API_VERSION$1 = "2021-06-07", REQUEST_TAG_PREFIX = "sanity.sdk.auth", REFRESH_INTERVAL = 720 * 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), rawHash = loc.hash.startsWith("#") ? loc.hash.slice(1) : loc.hash;
if (rawHash && rawHash.includes("=")) {
const hashParams = new URLSearchParams(rawHash);
hashParams.delete("token"), hashParams.delete("withSid");
const nextHash = hashParams.toString();
loc.hash = nextHash ? `#${nextHash}` : "";
}
return loc.searchParams.delete("sid"), loc.searchParams.delete("url"), loc.toString();
}
function getClientErrorApiBody(error) {
const body = error.response?.body;
return body && typeof body == "object" ? body : void 0;
}
function getClientErrorApiType(error) {
const body = getClientErrorApiBody(error);
return body?.error?.type ?? body?.type;
}
function getClientErrorApiDescription(error) {
const body = getClientErrorApiBody(error);
return body?.error?.description ?? body?.description;
}
function isProjectUserNotFoundClientError(error) {
return getClientErrorApiType(error) === "projectUserNotFoundError";
}
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, storageKey) {
return !storageArea || !storageKey ? null : getTokenFromStorage(storageArea, storageKey) || null;
}
const subscribeToStateAndFetchCurrentUser = ({
state,
instance
}) => {
const { clientFactory, apiHost } = state.get().options, useProjectHostname = !!instance.config.studioMode?.enabled, projectId = instance.config.projectId;
return state.observable.pipe(
map(({ authState, options }) => ({ authState, authMethod: options.authMethod })),
filter(
(value) => value.authState.type === AuthStateType.LOGGED_IN && !value.authState.currentUser
),
map((value) => ({ token: value.authState.token, authMethod: value.authMethod })),
distinctUntilChanged(
(prev, curr) => prev.token === curr.token && prev.authMethod === curr.authMethod
)
).pipe(
map(
({ token, authMethod }) => clientFactory({
apiVersion: DEFAULT_API_VERSION$1,
requestTagPrefix: REQUEST_TAG_PREFIX,
token: authMethod === "cookie" ? void 0 : token,
ignoreBrowserTokenWarning: !0,
useProjectHostname,
useCdn: !1,
...authMethod === "cookie" ? { withCredentials: !0 } : {},
...useProjectHostname && projectId ? { projectId } : {},
...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(
(e) => e.storageArea === storageArea && e.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, storageKey = "__sanity_auth_token";
const studioModeEnabled = instance.config.studioMode?.enabled;
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 || studioModeEnabled) && (storageArea = storageArea ?? getDefaultStorage());
let token, authMethod;
if (studioModeEnabled) {
const studioStorageKey = `__studio_auth_token_${instance.config.projectId ?? ""}`;
storageKey = studioStorageKey, token = getStudioTokenFromLocalStorage(storageArea, studioStorageKey), token && (authMethod = "localstorage");
} else
token = getTokenFromStorage(storageArea, storageKey), token && (authMethod = "localstorage");
let authState;
return providedToken ? authState = { type: AuthStateType.LOGGED_IN, token: providedToken, currentUser: null } : token && studioModeEnabled ? authState = { type: AuthStateType.LOGGED_IN, token: token ?? "", currentUser: null } : getAuthCode(callbackUrl, initialLocationHref) || getTokenFromLocation(initialLocationHref) ? authState = { type: AuthStateType.LOGGING_IN, isExchangingToken: !1 } : token && !isInDashboard && !studioModeEnabled ? 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 = [];
subscriptions.push(subscribeToStateAndFetchCurrentUser(context)), context.state.get().options?.storageArea && subscriptions.push(subscribeToStorageEventsAndSetToken(context));
try {
const { instance, state } = context, studioModeEnabled = !!instance.config.studioMode?.enabled, token = state.get().authState?.type === AuthStateType.LOGGED_IN ? state.get().authState.token : null;
if (studioModeEnabled && !token) {
const projectId = instance.config.projectId, clientFactory = state.get().options.clientFactory;
checkForCookieAuth(projectId, clientFactory).then((isCookieAuthEnabled) => {
isCookieAuthEnabled && state.set("enableCookieAuth", (prev) => ({
options: { ...prev.options, authMethod: "cookie" },
authState: prev.authState.type === AuthStateType.LOGGED_IN ? prev.authState : { type: AuthStateType.LOGGED_IN, token: "", currentUser: null }
}));
});
}
} catch {
}
return 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 }
});
}), 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,
source: 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) => {
if (!options || typeof options != "object")
throw new Error(
'getClient() requires a configuration object with at least an "apiVersion" property. Example: getClient(instance, { apiVersion: "2024-11-12" })'
);
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(), hasSource = !!options.source;
let sourceId = options.source?.[SOURCE_ID], resource;
Array.isArray(sourceId) && (resource = { type: sourceId[0], id: sourceId[1] }, sourceId = void 0);
const 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 || hasSource) && { useProjectHostname: !1 },
token: authMethod === "cookie" ? void 0 : tokenFromState ?? void 0,
...options,
...projectId && { projectId },
...dataset && { dataset },
...apiHost && { apiHost },
...resource && { "~experimental_resource": resource }
};
hasSource && ((options.projectId || options.dataset) && console.warn(
"Both source and explicit projectId/dataset are provided. The source will be used and projectId/dataset will be ignored."
), delete effectiveOptions.projectId, delete effectiveOptions.dataset), 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))
), API_VERSION$6 = "vX";
function agentGenerate(instance, options) {
return getClientState(instance, {
apiVersion: API_VERSION$6,
projectId: instance.config.projectId,
dataset: instance.config.dataset
}).observable.pipe(switchMap((client) => client.observable.agent.action.generate(options)));
}
function agentTransform(instance, options) {
return getClientState(instance, {
apiVersion: API_VERSION$6,
projectId: instance.config.projectId,
dataset: instance.config.dataset
}).observable.pipe(switchMap((client) => client.observable.agent.action.transform(options)));
}
function agentTranslate(instance, options) {
return getClientState(instance, {
apiVersion: API_VERSION$6,
projectId: instance.config.projectId,
dataset: instance.config.dataset
}).observable.pipe(switchMap((client) => client.observable.agent.action.translate(options)));
}
function agentPrompt(instance, options) {
return getClientState(instance, {
apiVersion: API_VERSION$6,
projectId: instance.config.projectId,
dataset: instance.config.dataset
}).observable.pipe(switchMap((client) => from(client.agent.action.prompt(options))));
}
function agentPatch(instance, options) {
return getClientState(instance, {
apiVersion: API_VERSION$6,
projectId: instance.config.projectId,
dataset: instance.config.dataset
}).observable.pipe(switchMap((client) => from(client.agent.action.patch(options))));
}
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 };
}
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((s) => {
const entries = Object.entries(s.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((i) => i !== 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) {
ret