@invisiblecities/sanity-edge-fetcher
Version:
Lightweight, Edge Runtime-compatible Sanity client for Next.js and Vercel Edge Functions
1 lines • 63.6 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../node_modules/.pnpm/@vercel+stega@0.1.2/node_modules/@vercel/stega/dist/index.mjs","../src/stega.ts","../src/core.ts","../src/enhanced.ts","../src/cache.ts"],"sourcesContent":["/**\n * @file index.ts\n * @description Sanity Edge Fetcher - Next.js-native Sanity client for edge runtime\n * @author Invisible Cities Agency\n * @license MIT\n */\n\n// Primary Next.js-aware exports (use these!)\nexport {\n sanityFetch, // Auto-detects draft mode\n sanityFetchWithFallback, // Smart fallback to drafts\n sanityFetchStatic, // Always CDN, no auth\n sanityFetchAuthenticated, // Always authenticated\n sanityFetchHybrid, // Presentation-aware hybrid (stega when needed)\n detectStegaRequest, // Utility: detect Studio/Presentation requests\n} from './core';\n\n// Core functionality (for advanced use cases)\nexport {\n edgeSanityFetch,\n createEdgeSanityFetcher,\n type EdgeSanityFetchOptions,\n type QueryParams\n} from './core';\n\n// Stega support for visual editing\nexport {\n type StegaConfig,\n buildStegaConfig,\n shouldEnableStega,\n stegaClean\n} from './stega';\n\n// Enhanced functionality\nexport {\n edgeSanityFetchWithRetry,\n createCachedSanityFetcher,\n createSanityEventSource,\n batchSanityFetch\n} from './enhanced';\n\n// Caching functionality\nexport {\n cachedSanityFetch,\n createCachedFetcher,\n clearSanityCache,\n warmSanityCache,\n getCacheStatus\n} from './cache';\n\n// Default export - the smart Next.js-aware fetcher\nexport { sanityFetch as default } from './core';","var s={0:8203,1:8204,2:8205,3:8290,4:8291,5:8288,6:65279,7:8289,8:119155,9:119156,a:119157,b:119158,c:119159,d:119160,e:119161,f:119162},c={0:8203,1:8204,2:8205,3:65279},u=new Array(4).fill(String.fromCodePoint(c[0])).join(\"\"),m=String.fromCharCode(0);function E(t){let e=JSON.stringify(t);return`${u}${Array.from(e).map(r=>{let n=r.charCodeAt(0);if(n>255)throw new Error(`Only ASCII edit info can be encoded. Error attempting to encode ${e} on character ${r} (${n})`);return Array.from(n.toString(4).padStart(4,\"0\")).map(o=>String.fromCodePoint(c[o])).join(\"\")}).join(\"\")}`}function y(t){let e=JSON.stringify(t);return Array.from(e).map(r=>{let n=r.charCodeAt(0);if(n>255)throw new Error(`Only ASCII edit info can be encoded. Error attempting to encode ${e} on character ${r} (${n})`);return Array.from(n.toString(16).padStart(2,\"0\")).map(o=>String.fromCodePoint(s[o])).join(\"\")}).join(\"\")}function I(t){return!Number.isNaN(Number(t))||/[a-z]/i.test(t)&&!/\\d+(?:[-:\\/]\\d+){2}(?:T\\d+(?:[-:\\/]\\d+){1,2}(\\.\\d+)?Z?)?/.test(t)?!1:Boolean(Date.parse(t))}function T(t){try{new URL(t,t.startsWith(\"/\")?\"https://acme.com\":void 0)}catch{return!1}return!0}function C(t,e,r=\"auto\"){return r===!0||r===\"auto\"&&(I(t)||T(t))?t:`${t}${E(e)}`}var x=Object.fromEntries(Object.entries(c).map(t=>t.reverse())),g=Object.fromEntries(Object.entries(s).map(t=>t.reverse())),S=`${Object.values(s).map(t=>`\\\\u{${t.toString(16)}}`).join(\"\")}`,f=new RegExp(`[${S}]{4,}`,\"gu\");function G(t){let e=t.match(f);if(!!e)return h(e[0],!0)[0]}function $(t){let e=t.match(f);if(!!e)return e.map(r=>h(r)).flat()}function h(t,e=!1){let r=Array.from(t);if(r.length%2===0){if(r.length%4||!t.startsWith(u))return A(r,e)}else throw new Error(\"Encoded data has invalid length\");let n=[];for(let o=r.length*.25;o--;){let p=r.slice(o*4,o*4+4).map(d=>x[d.codePointAt(0)]).join(\"\");n.unshift(String.fromCharCode(parseInt(p,4)))}if(e){n.shift();let o=n.indexOf(m);return o===-1&&(o=n.length),[JSON.parse(n.slice(0,o).join(\"\"))]}return n.join(\"\").split(m).filter(Boolean).map(o=>JSON.parse(o))}function A(t,e){var d;let r=[];for(let i=t.length*.5;i--;){let a=`${g[t[i*2].codePointAt(0)]}${g[t[i*2+1].codePointAt(0)]}`;r.unshift(String.fromCharCode(parseInt(a,16)))}let n=[],o=[r.join(\"\")],p=10;for(;o.length;){let i=o.shift();try{if(n.push(JSON.parse(i)),e)return n}catch(a){if(!p--)throw a;let l=+((d=a.message.match(/\\sposition\\s(\\d+)$/))==null?void 0:d[1]);if(!l)throw a;o.unshift(i.substring(0,l),i.substring(l))}}return n}function _(t){var e;return{cleaned:t.replace(f,\"\"),encoded:((e=t.match(f))==null?void 0:e[0])||\"\"}}function O(t){return t&&JSON.parse(_(JSON.stringify(t)).cleaned)}export{f as VERCEL_STEGA_REGEX,y as legacyStegaEncode,O as vercelStegaClean,C as vercelStegaCombine,G as vercelStegaDecode,$ as vercelStegaDecodeAll,E as vercelStegaEncode,_ as vercelStegaSplit};\n","/**\n * @file stega.ts\n * @description Stega encoding support for visual editing with optional @vercel/stega dependency\n * @author Invisible Cities Agency\n * @license MIT\n */\n\n// Edge-safe ESM import for @vercel/stega (tiny, browser/edge-friendly)\n// This package has no Node dependencies and is safe in Edge bundles.\nimport { vercelStegaEncode as _vercelStegaEncode, vercelStegaCombine as _vercelStegaCombine, vercelStegaClean as _vercelStegaClean } from '@vercel/stega';\n// NOTE: Library API: vercelStegaEncode(json) -> invisible suffix, vercelStegaCombine(text, json, skip?) -> combined string\nconst vercelStegaEncode: ((metadata: any) => string) | undefined = _vercelStegaEncode as any;\nconst vercelStegaCombine: ((value: string, metadata: any, skip?: 'auto' | boolean) => string) | undefined = _vercelStegaCombine as any;\nconst vercelStegaClean: (<T = any>(value: T) => T) | undefined = _vercelStegaClean as any;\n\n/**\n * Stega configuration options\n */\nexport interface StegaConfig {\n enabled: boolean;\n studioUrl?: string;\n basePath?: string;\n filter?: (path: string) => boolean;\n projectId?: string;\n dataset?: string;\n}\n\n/**\n * Inline stega implementation for when @vercel/stega is not available\n * Uses Unicode code points to encode invisible metadata\n */\nconst STEGA_CODES = {\n // Tuple ensures index access returns number (not possibly undefined)\n base4: [8203, 8204, 8205, 65279] as const,\n hex: {\n 0: 8203, 1: 8204, 2: 8205, 3: 8290, 4: 8291, 5: 8288,\n 6: 65279, 7: 8289, 8: 119155, 9: 119156,\n a: 119157, b: 119158, c: 119159, d: 119160, e: 119161, f: 119162\n } as Record<string | number, number>\n};\n\nconst STEGA_PREFIX = new Array(4).fill(String.fromCodePoint(STEGA_CODES.base4[0])).join('');\n\n/**\n * Encode data as invisible Unicode characters (base4 encoding)\n */\nfunction encodeInvisibleBase4(data: any): string {\n const jsonStr = JSON.stringify(data);\n const encoded = Array.from(jsonStr).map(char => {\n const charCode = char.charCodeAt(0);\n if (charCode > 255) {\n throw new Error(`Only ASCII can be encoded. Error on character ${char} (${charCode})`);\n }\n // Convert to base-4 and encode as invisible characters\n return Array.from(charCode.toString(4).padStart(4, '0'))\n .map(digit => {\n // digit is one of '0' | '1' | '2' | '3'\n const idx = parseInt(digit, 10) as 0 | 1 | 2 | 3;\n return String.fromCodePoint(STEGA_CODES.base4[idx]);\n })\n .join('');\n }).join('');\n \n return `${STEGA_PREFIX}${encoded}`;\n}\n\n/**\n * Check if a value should skip stega encoding\n */\nfunction shouldSkipEncoding(value: string): boolean {\n // Skip dates and URLs as they shouldn't be encoded\n const isDate = /\\d+(?:[-:\\/]\\d+){2}(?:T\\d+(?:[-:\\/]\\d+){1,2}(\\.\\d+)?Z?)?/.test(value);\n const isUrl = (() => {\n try {\n new URL(value, value.startsWith('/') ? 'https://example.com' : undefined);\n return true;\n } catch {\n return false;\n }\n })();\n // Skip Sanity asset/file id strings like image-<hash>-<WxH>-<ext>\n const isSanityAssetId = /^(image|file)-[A-Za-z0-9]+-\\d+x\\d+-[A-Za-z0-9]+$/.test(value);\n \n return isDate || isUrl || isSanityAssetId;\n}\n\n/**\n * Determine if the current path points to structured fields that must not be stega-encoded\n */\nfunction isStructuredPath(path: Array<string | number>): boolean {\n if (!path.length) return false;\n const last = String(path[path.length - 1]);\n // Any Sanity meta or reference/slug/asset/urlish fields\n const disallowed = new Set([\n '_id', '_ref', '_key', '_type',\n 'slug', 'current',\n 'asset', 'path',\n 'href', 'url', 'src'\n ]);\n if (disallowed.has(last)) return true;\n // If parent key is 'asset', skip child values as well\n if (path.length >= 2 && String(path[path.length - 2]) === 'asset') return true;\n return false;\n}\n\n/**\n * Encode stega metadata into a string value\n */\nfunction encodeStegaString(value: string, metadata: any, config: StegaConfig): string {\n if (!config.enabled || !metadata) {\n return value;\n }\n \n // Skip encoding for dates and URLs\n if (shouldSkipEncoding(value)) {\n return value;\n }\n \n // Skip encoding when there is no visible content to preserve\n if (typeof value !== 'string' || value.trim().length === 0) {\n return value;\n }\n\n // Skip if already stega-encoded to avoid double payloads\n try {\n if (vercelStegaClean && vercelStegaClean(value) !== value) {\n return value;\n }\n } catch {\n // ignore and continue\n }\n \n // Use @vercel/stega if available, otherwise use inline implementation\n if (vercelStegaEncode) {\n // Prefer combine when available; fallback to appending encoded invisible suffix\n if (vercelStegaCombine) return vercelStegaCombine(value, metadata, 'auto');\n return `${value}${vercelStegaEncode(metadata)}`;\n }\n \n // Inline implementation\n return `${value}${encodeInvisibleBase4(metadata)}`;\n}\n\n/**\n * Recursively encode stega metadata into result data\n */\nfunction encodeStegaInResult(\n data: any,\n sourceMap: any,\n config: StegaConfig,\n path: Array<string | number> = []\n): any {\n if (!config.enabled || !sourceMap) {\n return data;\n }\n \n // Handle null/undefined\n if (data == null) {\n return data;\n }\n \n // Handle strings\n if (typeof data === 'string') {\n // Never encode structured/system fields\n if (isStructuredPath(path)) return data;\n const metadata = resolveSourceMapForPath(sourceMap, path, config);\n // Only encode mapped leaf values\n if (metadata && (metadata.type === undefined || metadata.type === 'value')) {\n return encodeStegaString(data, metadata, config);\n }\n return data;\n }\n \n // Handle arrays\n if (Array.isArray(data)) {\n return data.map((item, index) => \n encodeStegaInResult(item, sourceMap, config, [...path, index])\n );\n }\n \n // Handle objects\n if (typeof data === 'object') {\n const result: any = {};\n for (const key in data) {\n if (data.hasOwnProperty(key)) {\n result[key] = encodeStegaInResult(\n data[key],\n sourceMap,\n config,\n [...path, key]\n );\n }\n }\n return result;\n }\n \n // Return primitives as-is\n return data;\n}\n\n/**\n * Resolve source map metadata for a given path\n */\nfunction resolveSourceMapForPath(sourceMap: any, path: Array<string | number>, config?: StegaConfig): any {\n if (!sourceMap?.mappings) {\n return null;\n }\n \n // Convert path to JSONPath format for lookup\n const jsonPath = `$${path.map(segment => \n typeof segment === 'number' ? `[${segment}]` : `['${segment}']`\n ).join('')}`;\n \n // Look for exact match or closest parent\n if (sourceMap.mappings[jsonPath]) {\n const mapping = sourceMap.mappings[jsonPath];\n const studioUrl = sourceMap.studioUrl || config?.studioUrl;\n\n // Attempt to build a Studio Edit Intent URL: /intent/edit/id=<docId>;path=<fieldPath>\n let href: string | undefined;\n try {\n const src = mapping?.source;\n const docId = typeof src?.document === 'number' ? sourceMap.documents?.[src.document]?._id : undefined;\n const fieldPath = typeof src?.path === 'number' ? sourceMap.paths?.[src.path] : undefined;\n\n if (studioUrl && docId) {\n // Normalize studio base to exclude trailing /presentation if present\n const studioBase = String(studioUrl).replace(/\\/?presentation\\/?$/, '').replace(/\\/$/, '');\n const pathParam = fieldPath ? `;path=${encodeURIComponent(fieldPath)}` : '';\n href = `${studioBase}/intent/edit/id=${encodeURIComponent(docId)}${pathParam}`;\n }\n } catch {\n // ignore href build errors\n }\n\n return {\n // Canonical hints for overlay decoders\n _origin: 'sanity',\n projectId: config?.projectId,\n dataset: config?.dataset,\n studioUrl,\n path: jsonPath,\n source: sourceMap.source,\n href,\n ...mapping,\n };\n }\n \n // Try to find parent paths\n let currentPath = jsonPath;\n while (currentPath.includes('[') || currentPath.includes('.')) {\n const lastIndex = Math.max(\n currentPath.lastIndexOf('['),\n currentPath.lastIndexOf('.')\n );\n if (lastIndex === -1) break;\n \n currentPath = currentPath.substring(0, lastIndex);\n if (sourceMap.mappings[currentPath]) {\n return {\n _origin: 'sanity',\n projectId: config?.projectId,\n dataset: config?.dataset,\n studioUrl: sourceMap.studioUrl,\n source: sourceMap.source,\n path: currentPath,\n ...sourceMap.mappings[currentPath]\n };\n }\n }\n \n return null;\n}\n\n/**\n * Get the studio URL from environment or config\n */\nfunction getStudioUrl(config?: Partial<StegaConfig>): string | undefined {\n if (config?.studioUrl) {\n return config.studioUrl;\n }\n \n // Try environment variables\n const envStudioUrl = process.env.NEXT_PUBLIC_SANITY_STUDIO_URL || \n process.env.SANITY_STUDIO_URL;\n \n if (envStudioUrl) {\n return envStudioUrl;\n }\n \n // Default based on environment\n if (process.env.NODE_ENV === 'development') {\n // Prefer HTTPS proxy for cookie/iframe parity with Presentation\n return 'https://localhost:3334/presentation';\n }\n \n return undefined;\n}\n\n/**\n * Check if stega should be enabled\n */\nexport function shouldEnableStega(isDraftMode: boolean, config?: Partial<StegaConfig>): boolean {\n // Explicit config takes precedence\n if (config?.enabled !== undefined) {\n return config.enabled;\n }\n \n // Default: enable in draft mode or development\n return isDraftMode || process.env.NODE_ENV === 'development';\n}\n\n/**\n * Process Sanity response to handle stega encoding\n */\nexport function processStegaResponse(\n response: any,\n isDraftMode: boolean,\n config?: Partial<StegaConfig> | StegaConfig\n): any {\n // If stega is not enabled, return just the result\n if (!shouldEnableStega(isDraftMode, config)) {\n return response.result;\n }\n \n // When stega is enabled, we need to encode the source map into the result\n const result = response.result;\n const sourceMap = response.resultSourceMap;\n \n if (sourceMap && config?.enabled) {\n // Add studio URL to source map for visual editing\n const studioUrl = getStudioUrl(config);\n if (studioUrl && sourceMap) {\n sourceMap.studioUrl = studioUrl;\n }\n \n // Encode stega metadata into the result\n const fullConfig = buildStegaConfig(isDraftMode, config);\n return encodeStegaInResult(result, sourceMap, fullConfig);\n }\n \n return result;\n}\n\n/**\n * Build stega configuration from environment and options\n */\nexport function buildStegaConfig(\n isDraftMode: boolean,\n options?: Partial<StegaConfig>\n): StegaConfig {\n const config: Partial<StegaConfig> = options || {};\n const enabled = shouldEnableStega(isDraftMode, config);\n \n const studioUrl = getStudioUrl(config);\n const projectId = config.projectId || process.env.NEXT_PUBLIC_SANITY_PROJECT_ID;\n const dataset = config.dataset || process.env.NEXT_PUBLIC_SANITY_DATASET;\n\n return {\n enabled,\n ...(studioUrl !== undefined ? { studioUrl } : {}),\n ...(config.basePath !== undefined ? { basePath: config.basePath } : {}),\n ...(config.filter !== undefined ? { filter: config.filter } : {}),\n ...(projectId !== undefined ? { projectId } : {}),\n ...(dataset !== undefined ? { dataset } : {}),\n } as StegaConfig;\n}\n\n/**\n * Clean stega-encoded strings (remove invisible characters)\n * Useful for comparing values or using in business logic\n */\nexport function stegaClean<T = any>(value: T): T {\n if (vercelStegaClean) {\n return vercelStegaClean(value);\n }\n \n // Inline implementation - remove all stega Unicode characters\n const stegaChars = Object.values(STEGA_CODES.hex)\n .map(code => `\\\\u{${code.toString(16)}}`)\n .join('');\n const stegaRegex = new RegExp(`[${stegaChars}]{4,}`, 'gu');\n \n const cleanString = (str: string) => str.replace(stegaRegex, '');\n \n // Recursively clean all strings in the value\n if (typeof value === 'string') {\n return cleanString(value) as T;\n }\n \n if (Array.isArray(value)) {\n return value.map(item => stegaClean(item)) as T;\n }\n \n if (value && typeof value === 'object') {\n const cleaned: any = {};\n for (const key in value) {\n if ((value as any).hasOwnProperty(key)) {\n cleaned[key] = stegaClean((value as any)[key]);\n }\n }\n return cleaned;\n }\n \n return value;\n}","/**\n * @file core.ts\n * @description Next.js-native, edge-compatible Sanity data fetcher with stega support\n * @author Invisible Cities Agency\n * @license MIT\n */\n\nimport { buildStegaConfig, processStegaResponse, type StegaConfig } from './stega';\n\n// Helper to check if draft mode is enabled in Next.js\nasync function isDraftModeEnabled(): Promise<boolean> {\n try {\n // Dynamic import to avoid build issues in non-Next.js environments\n const { draftMode } = await import('next/headers');\n const draft = await draftMode();\n return draft.isEnabled;\n } catch {\n // Not in Next.js or draft mode not available\n return false;\n }\n}\n\n// Detect if current request should enable stega/visual editing overlays\n// Uses Next.js headers/cookies if available, but degrades gracefully when not running in Next\nexport async function detectStegaRequest(options?: {\n /** Feature flag cookie name set by Studio or a toggle endpoint (default: 'ic_stega') */\n cookieName?: string;\n /** Optional custom header to allow one-shot enablement (default checks common names) */\n headerName?: string;\n /** Studio URL or origin to validate Referer against; defaults to NEXT_PUBLIC_SANITY_STUDIO_URL */\n studioUrl?: string;\n /** Force enable regardless of environment signals */\n forceEnable?: boolean;\n /** Force disable regardless of environment signals */\n forceDisable?: boolean;\n}): Promise<boolean> {\n if (options?.forceEnable) return true;\n if (options?.forceDisable) return false;\n\n // Defaults\n const cookieName = options?.cookieName ?? 'ic_stega';\n const studioEnv = options?.studioUrl || process.env.NEXT_PUBLIC_SANITY_STUDIO_URL || process.env.SANITY_STUDIO_URL;\n\n try {\n // Dynamic import to avoid hard Next.js dependency\n const mod = await import('next/headers');\n const cookies = (mod as any).cookies?.bind(mod);\n const headers = (mod as any).headers?.bind(mod);\n\n const draft = (await ((mod as any).draftMode?.() ?? { isEnabled: false }));\n if (draft?.isEnabled) return true;\n\n // Check cookie flag\n const hasCookie = typeof cookies === 'function' ? Boolean(cookies().get(cookieName)?.value) : false;\n if (hasCookie) return true;\n\n // Check custom or common headers\n const hdrs = typeof headers === 'function' ? headers() : undefined;\n const headerName = options?.headerName;\n const headerCandidates = [headerName, 'x-ic-stega', 'x-sanity-present', 'x-sanity-preview'].filter(Boolean) as string[];\n if (hdrs) {\n for (const name of headerCandidates) {\n const v = hdrs.get(name);\n if (v && v !== '0' && v.toLowerCase() !== 'false') return true;\n }\n }\n\n // Referer origin check against Studio origin\n if (hdrs && studioEnv) {\n const ref = hdrs.get('referer');\n if (ref) {\n try {\n const refererOrigin = new URL(ref).origin;\n const studioOrigin = new URL(studioEnv).origin;\n if (refererOrigin === studioOrigin) return true;\n } catch {\n // ignore URL parse errors\n }\n }\n }\n } catch {\n // Not in Next.js environment; fall through\n }\n\n // Fallback: enable in development if explicitly configured via env\n if (process.env.NEXT_PUBLIC_ENABLE_STEGA === '1') return true;\n return false;\n}\n\n// Get config from environment variables\nconst getProjectId = () => {\n const id = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID;\n if (!id) {\n throw new Error('NEXT_PUBLIC_SANITY_PROJECT_ID environment variable is required');\n }\n return id;\n};\n\nconst apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2025-02-10';\n\n// Get the viewer token - check multiple possible env vars\nconst getViewerToken = () => {\n return process.env.SANITY_VIEWER_TOKEN || \n process.env.SANITY_API_READ_TOKEN ||\n process.env.NEXT_PUBLIC_SANITY_VIEWER_TOKEN;\n};\n\nexport type QueryParams = Record<string, string | number | boolean | null | undefined | Array<string | number | boolean>>;\n\nexport interface EdgeSanityFetchOptions {\n /** Sanity dataset to query (e.g., 'production', 'staging') */\n dataset: string;\n /** GROQ query string */\n query: string;\n /** Optional query parameters for GROQ placeholders */\n params?: QueryParams;\n /** Whether to use Sanity's CDN (faster but no auth) */\n useCdn?: boolean;\n /** Whether to include auth token for draft preview access */\n useAuth?: boolean;\n /** Stega configuration for visual editing */\n stega?: Partial<StegaConfig>;\n}\n\n/**\n * Simple rate limiter to prevent 429 errors\n * @internal\n */\nclass EdgeRateLimiter {\n private lastRequest = 0;\n private readonly minInterval = 100; // 10 req/sec max\n\n async throttle(): Promise<void> {\n const now = Date.now();\n const timeSinceLastRequest = now - this.lastRequest;\n\n if (timeSinceLastRequest < this.minInterval) {\n const delay = this.minInterval - timeSinceLastRequest;\n await new Promise(resolve => setTimeout(resolve, delay));\n }\n\n this.lastRequest = Date.now();\n }\n}\n\nconst rateLimiter = new EdgeRateLimiter();\n\n/**\n * Fetches data from Sanity using native fetch API\n * Compatible with Edge Runtime and static generation\n */\nexport async function edgeSanityFetch<T>({\n dataset,\n query,\n params = {},\n useCdn = false,\n useAuth = false,\n stega\n}: EdgeSanityFetchOptions): Promise<T> {\n // Apply rate limiting\n await rateLimiter.throttle();\n\n // Build the query URL\n const projectId = getProjectId();\n\n // Determine if we need source maps for stega (before choosing base URL)\n const isDraftMode = useAuth;\n const stegaConfig = buildStegaConfig(isDraftMode, stega);\n\n // When stega is enabled, force non-CDN API to ensure resultSourceMap is returned\n const useCdnEffective = stegaConfig.enabled ? false : useCdn;\n const baseUrl = useCdnEffective\n ? `https://${projectId}.apicdn.sanity.io`\n : `https://${projectId}.api.sanity.io`;\n\n const url = new URL(`${baseUrl}/v${apiVersion}/data/query/${dataset}`);\n url.searchParams.set('query', query);\n \n if (useAuth) {\n // Use 'previewDrafts' perspective to see draft documents merged with published\n url.searchParams.set('perspective', 'previewDrafts');\n }\n \n // Request source maps when stega is enabled\n if (stegaConfig.enabled) {\n url.searchParams.set('resultSourceMap', 'true');\n }\n\n // Add parameters\n Object.entries(params).forEach(([key, value]) => {\n url.searchParams.set(`$${key}`, JSON.stringify(value));\n });\n\n // Build headers\n const headers: Record<string, string> = {\n 'Accept': 'application/json',\n };\n\n // Use env var for auth to maintain static generation compatibility\n if (useAuth) {\n const envToken = getViewerToken();\n if (envToken) {\n headers['Authorization'] = `Bearer ${envToken}`;\n }\n }\n \n const response = await fetch(url.toString(), {\n method: 'GET',\n headers,\n });\n\n if (!response.ok) {\n await response.text(); // Consume the body to prevent memory leak\n throw new Error(`Sanity fetch failed: ${response.status} ${response.statusText}`);\n }\n\n const data = await response.json();\n \n // Process response with stega support (reuse stegaConfig from above)\n return processStegaResponse(data, isDraftMode, stegaConfig);\n}\n\n/**\n * Factory function to create a typed Sanity fetcher for a given dataset\n */\nexport function createEdgeSanityFetcher(dataset: string, useAuth = false, stega?: Partial<StegaConfig>) {\n return <T>(query: string, params?: QueryParams) => {\n const options: EdgeSanityFetchOptions = {\n dataset,\n query,\n useAuth,\n ...(stega !== undefined ? { stega } : {}),\n ...(params !== undefined ? { params } : {}),\n };\n return edgeSanityFetch<T>(options);\n };\n}\n\n/**\n * Next.js-aware Sanity fetcher that automatically handles draft mode\n * This is the primary fetcher for Next.js applications\n * \n * @example\n * const data = await sanityFetch('*[_type == \"post\"][0]');\n */\nexport async function sanityFetch<T = unknown>(\n query: string,\n params?: QueryParams,\n options?: {\n dataset?: string;\n /** Override automatic draft mode detection */\n forceAuth?: boolean;\n /** Stega configuration for visual editing */\n stega?: Partial<StegaConfig>;\n }\n): Promise<T> {\n const dataset = options?.dataset || process.env.NEXT_PUBLIC_SANITY_DATASET || 'production';\n const useAuth = options?.forceAuth ?? await isDraftModeEnabled();\n \n return edgeSanityFetch<T>({\n dataset,\n query,\n ...(params !== undefined ? { params } : {}),\n useCdn: !useAuth, // Use CDN when not authenticated\n useAuth,\n stega: options?.stega || { enabled: useAuth }, // Enable stega in draft mode by default\n });\n}\n\n/**\n * Presentation-aware hybrid fetcher\n * Automatically enables authenticated fetch + stega overlays when a Studio/Presentation\n * signal is detected (draftMode cookie, feature flag cookie, referer from Studio, or header).\n * Otherwise uses fast CDN fetch with no stega.\n */\nexport async function sanityFetchHybrid<T = unknown>(\n query: string,\n params?: QueryParams,\n options?: {\n dataset?: string;\n /** Optional stega options to merge with defaults */\n stega?: Partial<StegaConfig>;\n /** Detection overrides */\n cookieName?: string;\n headerName?: string;\n studioUrl?: string;\n forceEnableStega?: boolean;\n forceDisableStega?: boolean;\n }\n): Promise<T> {\n const dataset = options?.dataset || process.env.NEXT_PUBLIC_SANITY_DATASET || 'production';\n const enableStega = await detectStegaRequest({\n ...(options?.cookieName !== undefined ? { cookieName: options.cookieName } : {}),\n ...(options?.headerName !== undefined ? { headerName: options.headerName } : {}),\n ...(options?.studioUrl !== undefined ? { studioUrl: options.studioUrl } : {}),\n ...(options?.forceEnableStega !== undefined ? { forceEnable: options.forceEnableStega } : {}),\n ...(options?.forceDisableStega !== undefined ? { forceDisable: options.forceDisableStega } : {}),\n });\n\n return edgeSanityFetch<T>({\n dataset,\n query,\n ...(params !== undefined ? { params } : {}),\n useCdn: !enableStega,\n useAuth: enableStega,\n stega: { enabled: enableStega, ...(options?.stega || {}) },\n });\n}\n\n/**\n * Sanity fetcher with automatic draft fallback\n * Tries to fetch published content first, falls back to drafts if empty\n * Perfect for singleton documents that might only exist as drafts\n * \n * @example\n * const page = await sanityFetchWithFallback('*[_type == \"page\" && slug.current == $slug][0]', { slug });\n */\nexport async function sanityFetchWithFallback<T = unknown>(\n query: string,\n params?: QueryParams,\n options?: {\n dataset?: string;\n /** Log when falling back to drafts */\n logFallback?: boolean;\n /** Stega configuration for visual editing */\n stega?: Partial<StegaConfig>;\n }\n): Promise<T> {\n const dataset = options?.dataset || process.env.NEXT_PUBLIC_SANITY_DATASET || 'production';\n const isNextDraftMode = await isDraftModeEnabled();\n \n // If already in draft mode, just use authenticated fetch\n if (isNextDraftMode) {\n return edgeSanityFetch<T>({\n dataset,\n query,\n ...(params !== undefined ? { params } : {}),\n useCdn: false,\n useAuth: true,\n stega: options?.stega || { enabled: true },\n });\n }\n \n // Try published content first\n const publishedResult = await edgeSanityFetch<T>({\n dataset,\n query,\n ...(params !== undefined ? { params } : {}),\n useCdn: true,\n useAuth: false,\n });\n \n // If we got content, return it\n if (publishedResult) {\n return publishedResult;\n }\n \n // No published content, try drafts\n if (options?.logFallback !== false && process.env.NODE_ENV !== 'production') {\n console.warn('[sanityFetchWithFallback] No published content found, checking for drafts...');\n }\n \n const draftResult = await edgeSanityFetch<T>({\n dataset,\n query,\n ...(params !== undefined ? { params } : {}),\n useCdn: false,\n useAuth: true,\n stega: options?.stega || { enabled: true },\n });\n \n if (draftResult && options?.logFallback !== false && process.env.NODE_ENV !== 'production') {\n console.warn('[sanityFetchWithFallback] Draft content found and returned');\n }\n \n return draftResult;\n}\n\n/**\n * Static content fetcher - always uses CDN, never authenticates\n * Use for global settings and content that rarely changes\n * \n * @example\n * const settings = await sanityFetchStatic('*[_type == \"siteSettings\"][0]');\n */\nexport async function sanityFetchStatic<T = unknown>(\n query: string,\n params?: QueryParams,\n dataset?: string\n): Promise<T> {\n return edgeSanityFetch<T>({\n dataset: dataset || process.env.NEXT_PUBLIC_SANITY_DATASET || 'production',\n query,\n ...(params !== undefined ? { params } : {}),\n useCdn: true,\n useAuth: false,\n });\n}\n\n/**\n * Authenticated fetcher - always uses authentication\n * Use when you need to ensure draft content is visible\n * \n * @example\n * const drafts = await sanityFetchAuthenticated('*[_type == \"post\" && _id in path(\"drafts.**\")]');\n */\nexport async function sanityFetchAuthenticated<T = unknown>(\n query: string,\n params?: QueryParams,\n dataset?: string\n): Promise<T> {\n return edgeSanityFetch<T>({\n dataset: dataset || process.env.NEXT_PUBLIC_SANITY_DATASET || 'production',\n query,\n ...(params !== undefined ? { params } : {}),\n useCdn: false,\n useAuth: true,\n });\n}","/**\n * @file enhanced.ts\n * @description Enhanced Sanity fetcher with retry and real-time capabilities\n * @author Invisible Cities Agency\n * @license MIT\n */\n\nimport { edgeSanityFetch, type EdgeSanityFetchOptions, type QueryParams } from './core';\n\n// Type for p-retry module\ninterface PRetryModule {\n default: <T>(\n input: () => Promise<T>,\n options?: {\n retries?: number;\n minTimeout?: number;\n maxTimeout?: number;\n onFailedAttempt?: (error: { attemptNumber: number; retriesLeft: number }) => void;\n }\n ) => Promise<T>;\n}\n\n// Dynamic imports for optional dependencies\nlet pRetryModule: PRetryModule | null = null;\n\n// Lazy load optional dependencies\nconst loadPRetry = async (): Promise<PRetryModule | null> => {\n if (pRetryModule) return pRetryModule;\n try {\n pRetryModule = await import('p-retry');\n return pRetryModule;\n } catch {\n return null;\n }\n};\n\n/**\n * Fetches data from Sanity with automatic retry support\n * Falls back to single attempt if p-retry not installed\n */\nexport async function edgeSanityFetchWithRetry<T>(\n options: EdgeSanityFetchOptions,\n retryOptions?: {\n retries?: number;\n minTimeout?: number;\n maxTimeout?: number;\n }\n): Promise<T> {\n // Try to load p-retry if available\n const retry = await loadPRetry();\n \n // If p-retry not available, fall back to basic fetch\n if (!retry) {\n return edgeSanityFetch<T>(options);\n }\n\n const defaultRetryOptions = {\n retries: 3,\n minTimeout: 100,\n maxTimeout: 2000,\n onFailedAttempt: (error: { attemptNumber: number; retriesLeft: number }) => {\n // Sanity fetch attempt failed, will retry\n void error.attemptNumber;\n void error.retriesLeft;\n }\n };\n\n return retry.default(\n () => edgeSanityFetch<T>(options),\n { ...defaultRetryOptions, ...retryOptions }\n );\n}\n\n/**\n * Creates a cached Sanity fetcher\n * Note: Use cachedSanityFetch from cache.ts for full caching support\n */\nexport function createCachedSanityFetcher(\n dataset: string, \n _revalidate = 60,\n _tags?: string[]\n) {\n // Return basic fetcher - for caching use cachedSanityFetch from cache.ts\n return <T>(query: string, params?: QueryParams) => \n edgeSanityFetch<T>({\n dataset,\n query,\n ...(params !== undefined ? { params } : {}),\n useCdn: true\n });\n}\n\n/**\n * Creates an EventSource connection for real-time Sanity updates\n * Requires a server endpoint to handle SSE (see examples/vercel-sse.ts)\n */\nexport function createSanityEventSource(\n query: string,\n dataset = 'production',\n options?: {\n endpoint?: string;\n onMessage?: (data: unknown) => void;\n onError?: (error: Event) => void;\n }\n) {\n const endpoint = options?.endpoint || '/api/sanity-updates';\n const url = new URL(endpoint, window.location.origin);\n url.searchParams.set('query', query);\n url.searchParams.set('dataset', dataset);\n \n const eventSource = new EventSource(url.toString());\n \n if (options?.onMessage) {\n eventSource.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data);\n if (options.onMessage) {\n options.onMessage(data);\n }\n } catch {\n // Failed to parse SSE data\n }\n };\n }\n \n if (options?.onError) {\n eventSource.onerror = options.onError;\n }\n \n return eventSource;\n}\n\n/**\n * Result type for batch fetching\n */\nexport type BatchResult<T extends Record<string, unknown>> = {\n [K in keyof T]: T[K] | null;\n};\n\n/**\n * Batch fetcher for multiple queries in a single request\n * Reduces API calls and improves performance\n */\nexport async function batchSanityFetch<T extends Record<string, unknown>>(\n queries: Record<string, { query: string; params?: QueryParams }>,\n dataset: string,\n options?: { useAuth?: boolean; useCdn?: boolean }\n): Promise<BatchResult<T>> {\n const results: Record<string, unknown> = {};\n \n // Use Promise.all for parallel fetching\n await Promise.all(\n Object.entries(queries).map(async ([key, { query, params }]) => {\n try {\n results[key] = await edgeSanityFetch({\n dataset,\n query,\n ...(params !== undefined ? { params } : {}),\n ...options\n });\n } catch {\n // Failed to fetch this query\n results[key] = null;\n }\n })\n );\n \n return results as BatchResult<T>;\n}","/**\n * @file cache.ts\n * @description Multi-layer caching for Sanity Edge Fetcher\n * @author Invisible Cities Agency\n * @license MIT\n */\n\nimport { edgeSanityFetch, type EdgeSanityFetchOptions, type QueryParams } from './core';\nimport { Redis } from '@upstash/redis';\n\n// Check if Upstash Redis is configured\nconst REDIS_URL = process.env.KV_REST_API_URL || process.env.UPSTASH_REDIS_REST_URL;\nconst REDIS_TOKEN = process.env.KV_REST_API_TOKEN || process.env.UPSTASH_REDIS_REST_TOKEN;\nconst REDIS_READ_ONLY_TOKEN = process.env.KV_REST_API_READ_ONLY_TOKEN;\n\nconst isRedisConfigured = !!(REDIS_URL && (REDIS_TOKEN || REDIS_READ_ONLY_TOKEN));\n\n// Initialize Redis client if configured\nlet redis: Redis | null = null;\nlet redisWriter: Redis | null = null;\n\nif (isRedisConfigured && REDIS_URL && (REDIS_TOKEN || REDIS_READ_ONLY_TOKEN)) {\n try {\n redis = new Redis({\n url: REDIS_URL,\n token: (REDIS_READ_ONLY_TOKEN || REDIS_TOKEN) as string,\n automaticDeserialization: true,\n });\n \n // Separate writer client if write token available\n if (REDIS_TOKEN) {\n redisWriter = new Redis({\n url: REDIS_URL,\n token: REDIS_TOKEN,\n automaticDeserialization: true,\n });\n } else {\n redisWriter = redis;\n }\n } catch {\n // Failed to initialize Redis client\n redis = null;\n redisWriter = null;\n }\n}\n\ninterface CacheEntry<T> {\n value: T;\n timestamp: number;\n validUntil: number;\n}\n\ninterface CachedFetchOptions extends EdgeSanityFetchOptions {\n /** Cache configuration */\n cache?: {\n /** Time to live in seconds */\n ttl?: number;\n /** Cache key prefix */\n prefix?: string;\n /** Force cache refresh */\n force?: boolean;\n /** Enable Redis caching if available */\n useRedis?: boolean;\n /** Enable Next.js cache */\n useNextCache?: boolean;\n };\n}\n\n/**\n * Generate cache key from query and params\n */\nfunction generateCacheKey(\n dataset: string,\n query: string,\n params?: QueryParams\n): string {\n const baseKey = `sanity:${dataset}:${query}`;\n if (!params || Object.keys(params).length === 0) {\n return baseKey;\n }\n \n // Sort params for consistent key generation\n const sortedParams = Object.keys(params)\n .sort()\n .map(key => `${key}=${JSON.stringify(params[key])}`)\n .join('&');\n \n return `${baseKey}:${sortedParams}`;\n}\n\n/**\n * In-memory LRU cache for edge runtime\n */\nclass MemoryCache {\n private cache = new Map<string, CacheEntry<unknown>>();\n private maxSize = 100;\n \n get<T>(key: string): T | null {\n const entry = this.cache.get(key);\n if (!entry) return null;\n \n if (Date.now() > entry.validUntil) {\n this.cache.delete(key);\n return null;\n }\n \n // Move to end (LRU)\n this.cache.delete(key);\n this.cache.set(key, entry);\n \n return entry.value as T;\n }\n \n set<T>(key: string, value: T, ttl: number): void {\n // Evict oldest if at capacity\n if (this.cache.size >= this.maxSize && !this.cache.has(key)) {\n const firstKey = this.cache.keys().next().value;\n if (firstKey !== undefined) {\n this.cache.delete(firstKey);\n }\n }\n \n this.cache.set(key, {\n value,\n timestamp: Date.now(),\n validUntil: Date.now() + (ttl * 1000)\n });\n }\n \n delete(key: string): void {\n this.cache.delete(key);\n }\n \n clear(): void {\n this.cache.clear();\n }\n \n size(): number {\n return this.cache.size;\n }\n}\n\n// Global memory cache instance\nconst memoryCache = new MemoryCache();\n\n/**\n * Fetches data from Sanity with multi-layer caching\n * \n * Cache layers (in order):\n * 1. In-memory LRU cache (fastest, ~1ms)\n * 2. Upstash Redis (if configured, ~10-30ms)\n * 3. Origin fetch with Next.js cache\n */\nexport async function cachedSanityFetch<T>(\n options: CachedFetchOptions\n): Promise<T> {\n const {\n dataset,\n query,\n params,\n cache = {}\n } = options;\n \n const {\n ttl = 60, // 1 minute default\n prefix = '',\n force = false,\n useRedis = true\n } = cache;\n \n const cacheKey = prefix + generateCacheKey(dataset, query, params);\n \n // Layer 1: Memory cache (unless force refresh)\n if (!force) {\n const memoryResult = memoryCache.get<T>(cacheKey);\n if (memoryResult !== null) {\n // Cache hit from memory\n return memoryResult;\n }\n }\n \n // Layer 2: Redis cache (if configured and enabled)\n if (!force && useRedis && redis) {\n try {\n const redisEntry = await redis.get<CacheEntry<T>>(cacheKey);\n if (redisEntry && Date.now() <= redisEntry.validUntil) {\n // Cache hit from Redis\n \n // Populate memory cache\n memoryCache.set(cacheKey, redisEntry.value, ttl);\n \n return redisEntry.value;\n }\n } catch {\n // Redis cache read error, continue to fetch from origin\n // Continue to fetch from origin\n }\n }\n \n // Layer 3: Fetch from origin\n // Cache miss, fetching from origin\n \n // Build safe options for edge fetch (remove cache, avoid undefined params)\n const { cache: _cache, params: _params, ...rest } = options;\n const edgeOptions = (params !== undefined)\n ? { ...rest, params }\n : { ...rest };\n \n // Fetch from origin (Next.js cache removed for edge compatibility)\n const result = await edgeSanityFetch<T>(edgeOptions as EdgeSanityFetchOptions);\n \n // Populate caches\n memoryCache.set(cacheKey, result, ttl);\n \n if (useRedis && redisWriter) {\n try {\n const entry: CacheEntry<T> = {\n value: result,\n timestamp: Date.now(),\n validUntil: Date.now() + (ttl * 1000)\n };\n await redisWriter.set(cacheKey, entry, { ex: ttl });\n } catch {\n // Redis cache write error, continue without caching\n // Continue without caching\n }\n }\n \n return result;\n}\n\n/**\n * Create a cached fetcher with default options\n */\nexport function createCachedFetcher(\n dataset: string,\n defaultCacheOptions?: CachedFetchOptions['cache']\n) {\n return <T>(\n query: string,\n params?: QueryParams,\n cacheOverrides?: CachedFetchOptions['cache']\n ) => {\n return cachedSanityFetch<T>({\n dataset,\n query,\n ...(params !== undefined ? { params } : {}),\n cache: { ...(defaultCacheOptions || {}), ...(cacheOverrides || {}) }\n });\n };\n}\n\n/**\n * Clear caches for a specific dataset or pattern\n */\nexport async function clearSanityCache(options?: {\n dataset?: string;\n pattern?: string;\n clearMemory?: boolean;\n clearRedis?: boolean;\n}): Promise<void> {\n const {\n dataset,\n pattern,\n clearMemory = true,\n clearRedis = true\n } = options || {};\n \n // Clear memory cache\n if (clearMemory) {\n if (!dataset && !pattern) {\n memoryCache.clear();\n } else {\n // Note: Memory cache doesn't support pattern matching\n // Would need to iterate all keys for pattern support\n // Pattern-based memory cache clearing not implemented\n }\n }\n \n // Clear Redis cache\n if (clearRedis && redisWriter && redis) {\n try {\n const keyPattern = pattern || (dataset ? `sanity:${dataset}:*` : 'sanity:*');\n const keys = await redis.keys(keyPattern);\n if (keys.length > 0) {\n await redisWriter.del(...keys);\n }\n } catch {\n // Failed to clear Redis cache\n }\n }\n}\n\n/**\n * Warm cache by pre-fetching common queries\n */\nexport async function warmSanityCache(\n queries: Array<{\n dataset: string;\n query: string;\n params?: QueryParams;\n ttl?: number;\n }>\n): Promise<void> {\n await Promise.all(\n queries.map(({ dataset, query, params, ttl }) =>\n cachedSanityFetch({\n dataset,\n query,\n ...(params !== undefined ? { params } : {}),\n cache: { ...(ttl !== undefined ? { ttl } : {}) }\n }).catch(() => {\n // Failed to warm cache for query\n })\n )\n );\n}\n\n// Export cache status utility\nexport function getCacheStatus() {\n return {\n memory: {\n available: true,\n size: memoryCache.size()\n },\n redis: {\n available: isRedisConfigured && redis !== null,\n configured: isRedisConfigured,\n url: REDIS_URL ? new URL(REDIS_URL).hostname : null\n },\n nextCache: {\n available: typeof window === 'undefined'\n }\n };\n}"],"mappings":"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