UNPKG

@frak-labs/core-sdk

Version:

Core SDK of the Frak wallet, low level library to interact directly with the frak ecosystem.

1 lines 60.8 kB
{"version":3,"file":"frakContext-BGx4IVAK.cjs","names":[],"sources":["../src/config/clientId.ts","../src/utils/cache/lruMap.ts","../src/utils/cache/withCache.ts","../src/config/backendUrl.ts","../src/config/sdkConfigStore.ts","../src/utils/compression/b64.ts","../src/utils/compression/compress.ts","../src/utils/sso/sso.ts","../src/actions/openSso.ts","../src/utils/analytics/trackEvent.ts","../src/utils/url/queryParams.ts","../src/context/address.ts","../src/types/context.ts","../src/context/frakContextV2Codec.ts","../src/context/frakContext.ts"],"sourcesContent":["/**\n * Client ID utilities for anonymous tracking\n * Generates and persists a UUID fingerprint for referral attribution\n */\n\nconst CLIENT_ID_KEY = \"frak-client-id\";\n\n/**\n * Generate a UUID v4\n * Uses crypto.randomUUID if available, otherwise falls back to a manual implementation\n */\nfunction generateUUID(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n // Fallback for older browsers\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Get the client ID from localStorage, creating one if it doesn't exist\n * @returns The client ID (UUID format)\n */\nexport function getClientId(): string {\n if (typeof window === \"undefined\" || !window.localStorage) {\n // SSR or no localStorage - generate ephemeral ID\n console.warn(\n \"[Frak SDK] No Window / localStorage available to save the clientId\"\n );\n return generateUUID();\n }\n\n let clientId = localStorage.getItem(CLIENT_ID_KEY);\n if (!clientId) {\n clientId = generateUUID();\n localStorage.setItem(CLIENT_ID_KEY, clientId);\n }\n return clientId;\n}\n","/**\n * Map with a LRU (Least Recently Used) eviction policy.\n *\n * When the map exceeds `maxSize`, the least recently accessed entry is removed.\n * Accessing a key via `get()` promotes it to \"most recently used\".\n *\n * Adapted from viem's LruMap utility.\n * @link https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU\n */\nexport class LruMap<TValue = unknown> extends Map<string, TValue> {\n maxSize: number;\n\n constructor(size: number) {\n super();\n this.maxSize = size;\n }\n\n override get(key: string) {\n const value = super.get(key);\n if (super.has(key)) {\n // Move to end (most recently used)\n super.delete(key);\n super.set(key, value as TValue);\n }\n return value;\n }\n\n override set(key: string, value: TValue) {\n if (super.has(key)) super.delete(key);\n super.set(key, value);\n // Evict least recently used if over capacity\n if (this.maxSize && this.size > this.maxSize) {\n const firstKey = super.keys().next().value;\n if (firstKey !== undefined) super.delete(firstKey);\n }\n return this;\n }\n}\n","import { LruMap } from \"./lruMap\";\n\ntype CacheEntry<TData> = {\n data: TData;\n created: number;\n};\n\n/** Default cache time: 30 seconds */\nexport const DEFAULT_CACHE_TIME = 30_000;\n\n/** Short negative cache to avoid flooding on transient failures */\nconst NEGATIVE_CACHE_TIME = 1_000;\n\ntype CacheStore = {\n /** In-flight promises (dedup concurrent calls) */\n promiseCache: LruMap<Promise<unknown>>;\n /** Resolved responses (TTL-based) */\n responseCache: LruMap<CacheEntry<unknown>>;\n /** Recently failed keys to avoid request floods */\n failureCache: LruMap<number>;\n};\n\n// Allocate the three LruMaps lazily on first use rather than at module init.\n// Eager top-level allocation is a side effect at odds with `sideEffects: false`\n// and runs for every consumer that merely imports this module.\nlet store: CacheStore | undefined;\nfunction caches(): CacheStore {\n if (!store) {\n store = {\n promiseCache: new LruMap<Promise<unknown>>(1024),\n responseCache: new LruMap<CacheEntry<unknown>>(1024),\n failureCache: new LruMap<number>(1024),\n };\n }\n return store;\n}\n\ntype WithCacheOptions = {\n /** The key to cache the data against */\n cacheKey: string;\n /** Time in ms that cached data will remain valid. Default: 30_000 (30s). Set to 0 to disable caching. */\n cacheTime?: number;\n};\n\n/**\n * Returns the result of a given promise, and caches the result for\n * subsequent invocations against a provided cache key.\n *\n * Also deduplicates concurrent calls — if multiple callers request the same\n * cache key while the promise is pending, they share the same promise.\n *\n * @example\n * ```ts\n * // First call fetches, subsequent calls return cached data for 30s\n * const data = await withCache(\n * () => client.request({ method: \"frak_getMerchantInformation\" }),\n * { cacheKey: \"merchantInfo\", cacheTime: 30_000 }\n * );\n * ```\n */\nexport async function withCache<TData>(\n fn: () => Promise<TData>,\n { cacheKey, cacheTime = DEFAULT_CACHE_TIME }: WithCacheOptions\n): Promise<TData> {\n const { promiseCache, responseCache, failureCache } = caches();\n // Check response cache — return immediately if fresh\n if (cacheTime > 0) {\n const cached = responseCache.get(cacheKey) as\n | CacheEntry<TData>\n | undefined;\n if (cached) {\n const age = Date.now() - cached.created;\n if (age < cacheTime) return cached.data;\n }\n }\n\n // Check if this key recently failed — back off briefly\n const lastFailure = failureCache.get(cacheKey);\n if (lastFailure && Date.now() - lastFailure < NEGATIVE_CACHE_TIME) {\n throw new Error(`Cache: ${cacheKey} recently failed, backing off`);\n }\n\n // Check if there's already a pending promise (dedup concurrent calls)\n let promise = promiseCache.get(cacheKey) as Promise<TData> | undefined;\n if (!promise) {\n promise = fn();\n promiseCache.set(cacheKey, promise);\n }\n\n try {\n const data = await promise;\n // Store the response with a timestamp\n responseCache.set(cacheKey, { data, created: Date.now() });\n // Clear any previous failure\n failureCache.delete(cacheKey);\n return data;\n } catch (error) {\n // Record the failure timestamp\n failureCache.set(cacheKey, Date.now());\n throw error;\n } finally {\n // Clear the promise cache so subsequent calls can re-fetch after TTL\n promiseCache.delete(cacheKey);\n }\n}\n\n/**\n * Get a cache handle for a specific key, useful for manual invalidation.\n *\n * @example\n * ```ts\n * // Invalidate merchant info cache after a mutation\n * getCache(\"frak_getMerchantInformation\").clear();\n * ```\n */\nexport function getCache(cacheKey: string) {\n return {\n /** Clear both the pending promise and the cached response */\n clear: () => {\n const { promiseCache, responseCache } = caches();\n promiseCache.delete(cacheKey);\n responseCache.delete(cacheKey);\n },\n /** Check if a non-expired response exists */\n has: (cacheTime: number = DEFAULT_CACHE_TIME) => {\n const { responseCache } = caches();\n const cached = responseCache.get(cacheKey);\n if (!cached) return false;\n return Date.now() - cached.created < cacheTime;\n },\n };\n}\n\n/**\n * Clear all cached data (both pending promises and resolved responses).\n * Called automatically when the client is destroyed.\n */\nexport function clearAllCache() {\n if (!store) return;\n store.promiseCache.clear();\n store.responseCache.clear();\n store.failureCache.clear();\n}\n","/**\n * Default production backend URL\n */\nconst DEFAULT_BACKEND_URL = \"https://backend.frak.id\";\n\n/**\n * Check if wallet URL is local development (port 3000 or 3010)\n */\nfunction isLocalDevelopment(walletUrl: string): boolean {\n return (\n walletUrl.includes(\"localhost:3000\") ||\n walletUrl.includes(\"localhost:3010\")\n );\n}\n\n/**\n * Derive backend URL from wallet URL\n * Maps wallet URLs to their corresponding backend URLs\n */\nfunction deriveBackendUrl(walletUrl: string): string {\n if (isLocalDevelopment(walletUrl)) {\n return \"https://localhost:3030\";\n }\n // Dev environment\n if (\n walletUrl.includes(\"wallet-dev.frak.id\") ||\n walletUrl.includes(\"wallet.gcp-dev.frak.id\")\n ) {\n return \"https://backend.gcp-dev.frak.id\";\n }\n // Production\n return DEFAULT_BACKEND_URL;\n}\n\n/**\n * Get the backend URL for API calls\n * Tries to derive from SDK config, falls back to production\n *\n * @param walletUrl - Optional wallet URL to derive from (overrides global config)\n */\nexport function getBackendUrl(walletUrl?: string): string {\n // If explicit walletUrl provided, derive from it\n if (walletUrl) {\n return deriveBackendUrl(walletUrl);\n }\n\n // Try to get from global FrakSetup config\n if (typeof window !== \"undefined\") {\n const configWalletUrl = (\n window as {\n FrakSetup?: { client?: { config?: { walletUrl?: string } } };\n }\n ).FrakSetup?.client?.config?.walletUrl;\n\n if (configWalletUrl) {\n return deriveBackendUrl(configWalletUrl);\n }\n }\n\n // Fallback to production\n return DEFAULT_BACKEND_URL;\n}\n","/**\n * SDK config store — reactive singleton for the resolved merchant config.\n *\n * State lives directly on `window.__frakSdkConfig`.\n * Reactivity is handled via the `frak:config` CustomEvent on `window`.\n * Resolved configs are cached in localStorage (30 s TTL, stale-while-revalidate).\n *\n * Backend fetch responses are cached and deduplicated via `withCache`.\n * Also owns the `frak-merchant-id` sessionStorage compatibility key.\n */\n\nimport type { Language } from \"../types/config\";\nimport type {\n MerchantConfigResponse,\n SdkResolvedConfig,\n} from \"../types/resolvedConfig\";\nimport { clearAllCache, withCache } from \"../utils/cache\";\nimport { getBackendUrl } from \"./backendUrl\";\n\nconst GLOBAL_KEY = \"__frakSdkConfig\";\nconst CACHE_TTL = 30_000; // 30 seconds\nconst DEFAULT_CACHE_KEY = \"frak-config-cache\";\nconst MERCHANT_ID_KEY = \"frak-merchant-id\";\n\nconst cacheState = { key: DEFAULT_CACHE_KEY };\n\nconst isBrowser = typeof window !== \"undefined\";\n\ntype CacheEntry = { config: SdkResolvedConfig; timestamp: number };\n\ndeclare global {\n interface Window {\n [GLOBAL_KEY]?: SdkResolvedConfig;\n }\n interface WindowEventMap {\n \"frak:config\": CustomEvent<SdkResolvedConfig>;\n }\n}\n\nfunction freshEmptyConfig(): SdkResolvedConfig {\n return { isResolved: false, merchantId: \"\" };\n}\n\n// ---------------------------------------------------------------------------\n// localStorage cache (with in-memory parsed copy)\n// ---------------------------------------------------------------------------\n\nlet memoryEntry: CacheEntry | null = null;\n\nfunction loadCacheEntry(): CacheEntry | null {\n if (!isBrowser) return null;\n try {\n const raw = localStorage.getItem(cacheState.key);\n if (!raw) return null;\n const entry: CacheEntry = JSON.parse(raw);\n if (!entry.config?.isResolved) return null;\n memoryEntry = entry;\n return entry;\n } catch {\n return null;\n }\n}\n\nfunction readCache(): SdkResolvedConfig | undefined {\n return (memoryEntry ?? loadCacheEntry())?.config;\n}\n\nfunction isCacheFresh(): boolean {\n const entry = memoryEntry ?? loadCacheEntry();\n if (!entry) return false;\n return Date.now() - entry.timestamp < CACHE_TTL;\n}\n\nfunction writeCache(config: SdkResolvedConfig): void {\n if (!isBrowser || !config.isResolved) return;\n try {\n const entry: CacheEntry = { config, timestamp: Date.now() };\n localStorage.setItem(cacheState.key, JSON.stringify(entry));\n memoryEntry = entry;\n } catch {}\n}\n\nfunction removeCache(): void {\n if (!isBrowser) return;\n memoryEntry = null;\n try {\n localStorage.removeItem(cacheState.key);\n } catch {}\n}\n\n// ---------------------------------------------------------------------------\n// Initialise window-backed config (lazily, on first access)\n// ---------------------------------------------------------------------------\n\n// Seed `window.__frakSdkConfig` from the localStorage cache. Do NOT call this at\n// module top-level: that side effect contradicts `sideEffects: false` (a bundler\n// may legally drop it) and forces a localStorage read on every consumer at import.\n// It is invoked lazily from `getConfig` instead.\nfunction initConfig(): void {\n if (!isBrowser) return;\n if (window[GLOBAL_KEY]) return;\n window[GLOBAL_KEY] = readCache() ?? freshEmptyConfig();\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction getConfig(): SdkResolvedConfig {\n if (!isBrowser) return freshEmptyConfig();\n initConfig();\n return window[GLOBAL_KEY] ?? freshEmptyConfig();\n}\n\nfunction dispatch(config: SdkResolvedConfig): void {\n if (!isBrowser) return;\n window.dispatchEvent(new CustomEvent(\"frak:config\", { detail: config }));\n}\n\nfunction getTargetDomain(domain?: string): string {\n return domain ?? (isBrowser ? window.location.hostname : \"\");\n}\n\n// ---------------------------------------------------------------------------\n// Merchant config fetching (resolve)\n// ---------------------------------------------------------------------------\n\nasync function fetchFromBackend(\n targetDomain: string,\n walletUrl?: string,\n lang?: Language\n): Promise<MerchantConfigResponse | undefined> {\n try {\n const backendUrl = getBackendUrl(walletUrl);\n const langParam = lang ? `&lang=${encodeURIComponent(lang)}` : \"\";\n const response = await fetch(\n `${backendUrl}/user/merchant/resolve?domain=${encodeURIComponent(targetDomain)}${langParam}`\n );\n\n if (!response.ok) {\n console.warn(\n `[Frak SDK] Merchant lookup failed for domain ${targetDomain}: ${response.status}`\n );\n return undefined;\n }\n\n const data = (await response.json()) as MerchantConfigResponse;\n\n // Write compatibility sessionStorage key\n if (isBrowser) {\n try {\n sessionStorage.setItem(MERCHANT_ID_KEY, data.merchantId);\n } catch {}\n }\n\n return data;\n } catch (error) {\n console.warn(\"[Frak SDK] Failed to fetch merchant config:\", error);\n return undefined;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nexport const sdkConfigStore = {\n getConfig,\n\n get isResolved(): boolean {\n return getConfig().isResolved;\n },\n\n get isCacheFresh(): boolean {\n return isCacheFresh();\n },\n\n setCacheScope(domain: string, lang?: string): void {\n const suffix = `${domain}:${lang ?? \"\"}`;\n cacheState.key = `${DEFAULT_CACHE_KEY}:${suffix}`;\n memoryEntry = null;\n },\n\n setConfig(config: SdkResolvedConfig): void {\n if (isBrowser) window[GLOBAL_KEY] = config;\n writeCache(config);\n dispatch(config);\n\n // Keep sessionStorage merchantId in sync\n if (isBrowser && config.merchantId) {\n try {\n sessionStorage.setItem(MERCHANT_ID_KEY, config.merchantId);\n } catch {}\n }\n },\n\n reset(): void {\n const next = readCache() ?? freshEmptyConfig();\n if (isBrowser) window[GLOBAL_KEY] = next;\n dispatch(next);\n },\n\n clearCache(): void {\n removeCache();\n clearAllCache();\n if (isBrowser) {\n try {\n sessionStorage.removeItem(MERCHANT_ID_KEY);\n } catch {}\n }\n },\n\n resolve(\n domain?: string,\n walletUrl?: string,\n lang?: Language\n ): Promise<MerchantConfigResponse | undefined> {\n const targetDomain = getTargetDomain(domain);\n if (!targetDomain) {\n return Promise.resolve(undefined);\n }\n\n const cacheKey = `sdkConfig:${targetDomain}:${lang ?? \"\"}`;\n\n return withCache(\n async () => {\n const result = await fetchFromBackend(\n targetDomain,\n walletUrl,\n lang\n );\n // Throw on failure so withCache doesn't cache undefined\n if (!result) {\n throw new Error(\"Config resolution returned empty\");\n }\n return result;\n },\n { cacheKey, cacheTime: Number.POSITIVE_INFINITY }\n ).catch(() => undefined);\n },\n\n getMerchantId(): string | undefined {\n const config = getConfig();\n if (config.isResolved && config.merchantId) {\n return config.merchantId;\n }\n\n if (isBrowser) {\n try {\n return sessionStorage.getItem(MERCHANT_ID_KEY) ?? undefined;\n } catch {}\n }\n return undefined;\n },\n\n async resolveMerchantId(\n domain?: string,\n walletUrl?: string\n ): Promise<string | undefined> {\n const fast = sdkConfigStore.getMerchantId();\n if (fast) return fast;\n\n const config = await sdkConfigStore.resolve(domain, walletUrl);\n return config?.merchantId;\n },\n};\n","/**\n * Encode a buffer to a base64url encoded string\n * @param buffer The buffer to encode\n * @returns The encoded string\n */\nexport function base64urlEncode(buffer: Uint8Array): string {\n return btoa(Array.from(buffer, (b) => String.fromCharCode(b)).join(\"\"))\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\n/**\n * Decode a base64url encoded string\n * @param value The value to decode\n * @returns The decoded value\n */\nexport function base64urlDecode(value: string): Uint8Array {\n const m = value.length % 4;\n return Uint8Array.from(\n atob(\n value\n .replace(/-/g, \"+\")\n .replace(/_/g, \"/\")\n .padEnd(value.length + (m === 0 ? 0 : 4 - m), \"=\")\n ),\n (c) => c.charCodeAt(0)\n );\n}\n","import { jsonEncode } from \"@frak-labs/frame-connector\";\nimport { base64urlEncode } from \"./b64\";\n\n/**\n * Compress json data\n * @param data\n * @ignore\n */\nexport function compressJsonToB64(data: unknown): string {\n return base64urlEncode(jsonEncode(data));\n}\n","import type { Hex } from \"viem\";\nimport type { PrepareSsoParamsType, SsoMetadata } from \"../../types\";\nimport { compressJsonToB64 } from \"../compression/compress\";\n\nexport type AppSpecificSsoMetadata = SsoMetadata & {\n name?: string;\n css?: string;\n};\n\n/**\n * The full SSO params that will be used for compression\n */\nexport type FullSsoParams = Omit<PrepareSsoParamsType, \"metadata\"> & {\n metadata: AppSpecificSsoMetadata;\n merchantId: string;\n clientId: string;\n};\n\n/**\n * Generate SSO URL with compressed parameters\n * This mirrors the wallet's getOpenSsoLink() function\n *\n * @param walletUrl - Base wallet URL (e.g., \"https://wallet.frak.id\")\n * @param params - SSO parameters\n * @param merchantId - Merchant identifier\n * @param name - Application name\n * @param clientId - Client identifier for identity tracking\n * @param css - Optional custom CSS\n * @returns Complete SSO URL ready to open in popup or redirect\n *\n * @example\n * ```ts\n * const ssoUrl = generateSsoUrl(\n * \"https://wallet.frak.id\",\n * { metadata: { logoUrl: \"...\" }, directExit: true },\n * \"0x123...\",\n * \"My App\"\n * );\n * // Returns: https://wallet.frak.id/sso?p=<compressed_base64>\n * ```\n */\nexport function generateSsoUrl(\n walletUrl: string,\n params: PrepareSsoParamsType,\n merchantId: string,\n name: string | undefined,\n clientId: string,\n css?: string\n): string {\n // Build full params with app-specific metadata\n const fullParams: FullSsoParams = {\n redirectUrl: params.redirectUrl,\n directExit: params.directExit,\n lang: params.lang,\n merchantId,\n metadata: {\n name,\n css,\n logoUrl: params.metadata?.logoUrl,\n homepageLink: params.metadata?.homepageLink,\n },\n clientId,\n };\n\n // Compress params to minimal format\n const compressedParam = ssoParamsToCompressed(fullParams);\n\n // Encode to base64url\n const compressedString = compressJsonToB64(compressedParam);\n\n // Build URL matching wallet's expected format: /sso?p=<compressed>\n const ssoUrl = new URL(walletUrl);\n ssoUrl.pathname = \"/sso\";\n ssoUrl.searchParams.set(\"p\", compressedString);\n\n return ssoUrl.toString();\n}\n\n/**\n * Map full sso params to compressed sso params\n * @param params\n */\nfunction ssoParamsToCompressed(params: FullSsoParams): CompressedSsoData {\n return {\n r: params.redirectUrl,\n cId: params.clientId,\n d: params.directExit,\n l: params.lang,\n m: params.merchantId,\n md: {\n n: params.metadata?.name,\n css: params.metadata?.css,\n l: params.metadata?.logoUrl,\n h: params.metadata?.homepageLink,\n },\n };\n}\n\n/**\n * Type of compressed the sso data\n */\nexport type CompressedSsoData = {\n // Potential id from backend\n id?: Hex;\n // Client id\n cId: string;\n // redirect url\n r?: string;\n // direct exit\n d?: boolean;\n // language\n l?: \"en\" | \"fr\";\n // merchant id\n m: string;\n // metadata\n md: {\n n?: string;\n css?: string;\n l?: string;\n h?: string;\n };\n};\n","import { getClientId } from \"../config/clientId\";\nimport { sdkConfigStore } from \"../config/sdkConfigStore\";\nimport type {\n FrakClient,\n OpenSsoParamsType,\n OpenSsoReturnType,\n} from \"../types\";\nimport { generateSsoUrl } from \"../utils/sso/sso\";\n\n// SSO popup configuration\nexport const ssoPopupFeatures =\n \"menubar=no,status=no,scrollbars=no,fullscreen=no,width=500, height=800\";\nexport const ssoPopupName = \"frak-sso\";\n\n/**\n * Function used to open the SSO\n * @param client - The current Frak Client\n * @param args - The SSO parameters\n *\n * @description Two SSO flow modes:\n *\n * **Redirect Mode** (openInSameWindow: true):\n * - Wallet generates URL and triggers redirect\n * - Used when redirectUrl is provided\n *\n * **Popup Mode** (openInSameWindow: false/omitted):\n * - SDK generates URL client-side (or uses provided ssoPopupUrl)\n * - Opens popup synchronously (prevents popup blockers)\n * - Waits for SSO completion via postMessage\n *\n * @example\n * First we build the sso metadata\n * ```ts\n * // Build the metadata\n * const metadata: SsoMetadata = {\n * logoUrl: \"https://my-app.com/logo.png\",\n * homepageLink: \"https://my-app.com\",\n * };\n * ```\n *\n * Then, either use it with direct exit (and so user is directly redirected to your website), or a custom redirect URL\n * :::code-group\n * ```ts [Popup (default)]\n * // Opens in popup, SDK generates URL automatically\n * await openSso(frakConfig, {\n * directExit: true,\n * metadata,\n * });\n * ```\n * ```ts [Redirect]\n * // Opens in same window with redirect\n * await openSso(frakConfig, {\n * redirectUrl: \"https://my-app.com/frak-sso\",\n * metadata,\n * openInSameWindow: true,\n * });\n * ```\n * ```ts [Custom popup URL]\n * // Advanced: provide custom SSO URL\n * const { ssoUrl } = await prepareSso(frakConfig, { metadata });\n * await openSso(frakConfig, {\n * metadata,\n * ssoPopupUrl: `${ssoUrl}&custom=param`,\n * });\n * ```\n * :::\n */\nexport async function openSso(\n client: FrakClient,\n inputArgs: OpenSsoParamsType\n): Promise<OpenSsoReturnType> {\n const { metadata, customizations, walletUrl } = client.config;\n\n // Apply default: when no redirectUrl is provided we want the SSO popup\n // to close itself after completion. Without this default the popup\n // sticks on the success screen and the \"Redirect now\" button is a no-op.\n const args: OpenSsoParamsType = {\n ...inputArgs,\n directExit: inputArgs.directExit ?? !inputArgs.redirectUrl,\n };\n\n // Check if redirect mode (default to true if redirectUrl present)\n const isRedirectMode = args.openInSameWindow ?? !!args.redirectUrl;\n if (isRedirectMode) {\n // Redirect flow: Wallet generates URL and triggers redirect via lifecycle event\n // This must happen on wallet side because only the iframe can trigger the redirect\n return await client.request({\n method: \"frak_openSso\",\n params: [args, metadata.name, customizations?.css],\n });\n }\n\n // Popup flow: Generate URL on SDK side and open synchronously\n // This ensures window.open() is called in same tick as user gesture (no popup blocker)\n\n // Step 1: Generate or use provided SSO URL\n const ssoUrl =\n args.ssoPopupUrl ??\n generateSsoUrl(\n walletUrl ?? \"https://wallet.frak.id\",\n args,\n (await sdkConfigStore.resolveMerchantId()) ?? \"\",\n metadata.name,\n getClientId(),\n customizations?.css\n );\n\n // Step 2: Open popup synchronously (critical for popup blocker prevention)\n const popup = window.open(ssoUrl, ssoPopupName, ssoPopupFeatures);\n if (!popup) {\n throw new Error(\n \"Popup was blocked. Please allow popups for this site.\"\n );\n }\n popup.focus();\n\n // Step 3: Wait for SSO completion via RPC\n // The wallet iframe will resolve this when SSO page sends sso_complete message\n const result = await client.request({\n method: \"frak_openSso\",\n params: [args, metadata.name, customizations?.css],\n });\n\n return result ?? {};\n}\n","import type { FrakClient } from \"../../types\";\nimport type { SdkEventMap } from \"./events\";\n\n/**\n * Track an analytics event via the SDK's OpenPanel instance.\n * Fire-and-forget — silently catches errors so analytics never break a\n * partner integration.\n *\n * The client must be passed explicitly because the OpenPanel instance is\n * scoped to each `FrakClient` (a partner site may hold multiple iframes).\n *\n * @param client - The Frak client instance (no-op if undefined)\n * @param event - Typed event name from the SDK event map\n * @param properties - Typed properties for the given event\n */\nexport function trackEvent<K extends keyof SdkEventMap>(\n client: FrakClient | undefined,\n event: K,\n properties?: SdkEventMap[K]\n): void {\n if (!client) {\n console.debug(\"[Frak] No client provided, skipping event tracking\");\n return;\n }\n\n try {\n client.openPanel?.track(\n event as string,\n properties as Record<string, unknown> | undefined\n );\n } catch (e) {\n console.debug(\"[Frak] Failed to track event:\", event, e);\n }\n}\n","/**\n * Case-insensitive helpers for reading URL query parameters.\n *\n * Some email tools (Klaviyo, Omnisend, Customer.io …) and a few browsers\n * lowercase the entire URL before the recipient opens it, so a mixed-case key\n * authored as `frakAction` or `fCtx` can arrive as `frakaction` / `fctx`. A\n * plain `searchParams.get(\"frakAction\")` would miss it. Matching the key\n * ignoring case keeps UI-triggering and referral params working regardless of\n * how the link was mangled in transit.\n *\n * Note: only the key is normalised. An encoded value (base64url, tokens) is not\n * recoverable if the same channel also lowercased the value itself.\n */\n\n/**\n * Read a query parameter, matching its key case-insensitively.\n *\n * An exact-case match wins when present, so a canonical link is never shadowed\n * by a mangled duplicate (`?fctx=stale&fCtx=real` resolves to `real`). Only when\n * the exact key is absent do we scan for a case-folded variant.\n *\n * @returns the param value, or `null` when no key matches.\n */\nexport function getQueryParamCaseInsensitive(\n searchParams: URLSearchParams,\n key: string\n): string | null {\n const exact = searchParams.get(key);\n if (exact !== null) return exact;\n\n const target = key.toLowerCase();\n for (const [paramKey, value] of searchParams) {\n if (paramKey.toLowerCase() === target) {\n return value;\n }\n }\n return null;\n}\n\n/**\n * Delete every query parameter whose key matches `key` case-insensitively.\n *\n * Keys are collected before deletion because mutating a `URLSearchParams`\n * while iterating it skips entries.\n */\nexport function deleteQueryParamCaseInsensitive(\n searchParams: URLSearchParams,\n key: string\n): void {\n const target = key.toLowerCase();\n const matchingKeys = [...searchParams.keys()].filter(\n (paramKey) => paramKey.toLowerCase() === target\n );\n for (const paramKey of matchingKeys) {\n searchParams.delete(paramKey);\n }\n}\n","import type { Address } from \"viem\";\n\n/**\n * Address utilities — minimal, dependency-free replacements for the subset of\n * `viem` helpers we used to import. Keeping these in-house lets the SDK ship\n * without pulling viem's checksum/keccak/error chain into the bundle.\n *\n * Scope is intentionally narrow:\n * - `isAddress`: shape-only validation (no EIP-55 checksum)\n * - `areAddressesEqual`: case-insensitive equality\n * - `addressToBytes` / `bytesToAddress`: fixed 20-byte conversion\n */\n\n/** Matches a 0x-prefixed 40-char hex string regardless of case. */\nconst ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;\n\n/**\n * Check whether a value is a syntactically valid Ethereum address.\n *\n * This intentionally skips EIP-55 checksum validation: the SDK never produces\n * checksum-cased payloads, and downstream consumers (wallet, indexer) treat\n * addresses case-insensitively. Avoiding the checksum path drops keccak256 +\n * @noble/hashes from the bundle.\n */\nexport function isAddress(value: unknown): value is Address {\n return typeof value === \"string\" && ADDRESS_REGEX.test(value);\n}\n\n/**\n * Case-insensitive equality check for two Ethereum addresses.\n *\n * Both inputs are assumed to be syntactically valid addresses; callers that\n * receive untrusted input should validate via {@link isAddress} first.\n */\nexport function areAddressesEqual(a: Address, b: Address): boolean {\n return a.toLowerCase() === b.toLowerCase();\n}\n\n/**\n * Decode a 20-byte Ethereum address into a fixed-size Uint8Array(20).\n *\n * Throws when the input is not exactly `0x` + 40 hex chars — callers wrap\n * the call in try/catch (see {@link FrakContextManager.compress}) so any\n * malformed input degrades to a graceful undefined return.\n */\nexport function addressToBytes(address: Address): Uint8Array {\n const bytes = new Uint8Array(20);\n for (let i = 0; i < 20; i++) {\n const byte = Number.parseInt(\n address.substring(2 + i * 2, 4 + i * 2),\n 16\n );\n if (Number.isNaN(byte)) {\n throw new Error(`Invalid address: ${address}`);\n }\n bytes[i] = byte;\n }\n return bytes;\n}\n\n/** Lookup table avoids `padStart` overhead in the hot encode loop. */\nconst HEX_BYTE = /*#__PURE__*/ Array.from({ length: 256 }, (_, i) =>\n i.toString(16).padStart(2, \"0\")\n);\n\n/**\n * Encode a 20-byte Uint8Array (or a 20-byte subarray view) into a lowercase\n * hex Ethereum address. The caller MUST guarantee `bytes.length === 20`.\n */\nexport function bytesToAddress(bytes: Uint8Array): Address {\n let out = \"0x\";\n for (let i = 0; i < 20; i++) {\n out += HEX_BYTE[bytes[i]];\n }\n return out as Address;\n}\n","import type { Address } from \"viem\";\n\n/**\n * V1 (legacy) Frak Context — contains only the referrer wallet address.\n * Used for backward compatibility with old sharing links.\n * @ignore\n */\nexport type FrakContextV1 = {\n /** Referrer wallet address */\n r: Address;\n};\n\n/**\n * V2 Frak Context — anonymous-first referral context with optional wallet.\n *\n * Carries merchant context (`m`) and creation timestamp (`t`) unconditionally.\n * Identifies the sharer via either the anonymous clientId (`c`) or, when the\n * sharer is authenticated, the stronger wallet identifier (`w`). A valid V2\n * context MUST contain at least one of `c` or `w`; both may be present when\n * a logged-in user shares a link (best attribution signal).\n *\n * `w` takes precedence as the source of truth because the wallet is bound to\n * the user's WebAuthn credential, survives localStorage clears, and is global\n * across merchants — unlike `c`, which is a per-browser UUID.\n *\n * @ignore\n */\nexport type FrakContextV2 = {\n /** Version discriminator */\n v: 2;\n /** Merchant ID (UUID) */\n m: string;\n /** Link creation timestamp (epoch seconds) */\n t: number;\n /** Sharer's anonymous clientId (UUID from localStorage). Optional when `w` is provided. */\n c?: string;\n /** Sharer's wallet address. Preferred source of truth when the sharer is authenticated. Optional when `c` is provided. */\n w?: Address;\n};\n\n/**\n * The current Frak Context — union of all versions.\n *\n * - No `v` field → V1 (legacy wallet address)\n * - `v: 2` → V2 (anonymous clientId-based)\n *\n * @ignore\n */\nexport type FrakContext = FrakContextV1 | FrakContextV2;\n\n/**\n * Type guard: check if a context is V1 (legacy wallet address).\n * @param ctx - The Frak context to check\n * @returns True if the context is a V1 context\n */\nexport function isV1Context(ctx: FrakContext): ctx is FrakContextV1 {\n return \"r\" in ctx && !(\"v\" in ctx);\n}\n\n/**\n * Type guard: check if a context is V2 (anonymous clientId-based).\n * @param ctx - The Frak context to check\n * @returns True if the context is a V2 context\n */\nexport function isV2Context(ctx: FrakContext): ctx is FrakContextV2 {\n return \"v\" in ctx && ctx.v === 2;\n}\n","/**\n * Binary codec for {@link FrakContextV2}.\n *\n * Produces a compact, URL-safe byte layout (~65% smaller than the previous\n * JSON+base64url format). See the layout below.\n *\n * ## Wire layout\n *\n * ```text\n * byte 0: header\n * bits 0-3 version (= 2)\n * bit 4 has_c flag\n * bit 5 has_w flag\n * bits 6-7 reserved (must be 0)\n * bytes 1..16: merchant UUID (16 bytes, mandatory)\n * bytes 17..20: timestamp (uint32 big-endian, Unix seconds)\n * bytes 21..36: client UUID (16 bytes, only when has_c is set)\n * bytes 37..56: wallet address (20 bytes, only when has_w is set)\n * ```\n *\n * Size variants (before base64url):\n * - has_c only: 37 bytes\n * - has_w only: 41 bytes\n * - has_c + has_w: 57 bytes\n *\n * V1 payloads are exactly 20 bytes (raw wallet address); the byte lengths never\n * overlap, so the outer decoder can disambiguate purely on length.\n *\n * @ignore\n */\nimport type { Address } from \"viem\";\nimport type { FrakContextV2 } from \"../types\";\nimport { addressToBytes, bytesToAddress, isAddress } from \"./address\";\n\nconst VERSION_V2 = 0x02;\nconst VERSION_MASK = 0x0f;\nconst FLAG_HAS_C = 1 << 4;\nconst FLAG_HAS_W = 1 << 5;\nconst RESERVED_MASK = 0xc0;\n\nconst UUID_BYTES = 16;\nconst TIMESTAMP_BYTES = 4;\nconst ADDRESS_BYTES = 20;\nconst HEADER_BYTES = 1;\n\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** Strict lower-case UUID validation (RFC 4122 shape, any version/variant). */\nfunction isUuid(value: unknown): value is string {\n return typeof value === \"string\" && UUID_RE.test(value);\n}\n\n/** Parse a canonical UUID string into 16 raw bytes. */\nfunction uuidToBytes(uuid: string): Uint8Array {\n const hex = uuid.replace(/-/g, \"\");\n const out = new Uint8Array(UUID_BYTES);\n for (let i = 0; i < UUID_BYTES; i++) {\n out[i] = Number.parseInt(hex.substring(i * 2, i * 2 + 2), 16);\n }\n return out;\n}\n\n/** Format 16 raw bytes as a canonical 8-4-4-4-12 UUID string. */\nfunction bytesToUuid(bytes: Uint8Array): string {\n let hex = \"\";\n for (let i = 0; i < UUID_BYTES; i++) {\n hex += bytes[i].toString(16).padStart(2, \"0\");\n }\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;\n}\n\n/**\n * Encode a {@link FrakContextV2} into its binary wire format.\n *\n * Returns `null` when the context fails runtime validation (missing fields,\n * malformed UUIDs, timestamp outside uint32 range, invalid wallet).\n */\nexport function encodeFrakContextV2(ctx: FrakContextV2): Uint8Array | null {\n if (!isUuid(ctx.m)) return null;\n if (!Number.isInteger(ctx.t) || ctx.t < 0 || ctx.t > 0xff_ff_ff_ff)\n return null;\n\n const hasC = typeof ctx.c === \"string\" && ctx.c.length > 0;\n const hasW = typeof ctx.w === \"string\" && isAddress(ctx.w);\n if (!hasC && !hasW) return null;\n if (hasC && !isUuid(ctx.c)) return null;\n\n const size =\n HEADER_BYTES +\n UUID_BYTES +\n TIMESTAMP_BYTES +\n (hasC ? UUID_BYTES : 0) +\n (hasW ? ADDRESS_BYTES : 0);\n\n const buf = new Uint8Array(size);\n const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);\n let offset = 0;\n\n buf[offset++] =\n VERSION_V2 | (hasC ? FLAG_HAS_C : 0) | (hasW ? FLAG_HAS_W : 0);\n\n buf.set(uuidToBytes(ctx.m), offset);\n offset += UUID_BYTES;\n\n view.setUint32(offset, ctx.t, false);\n offset += TIMESTAMP_BYTES;\n\n if (hasC) {\n buf.set(uuidToBytes(ctx.c as string), offset);\n offset += UUID_BYTES;\n }\n\n if (hasW) {\n buf.set(addressToBytes(ctx.w as Address), offset);\n offset += ADDRESS_BYTES;\n }\n\n return buf;\n}\n\n/**\n * Decode a binary {@link FrakContextV2} payload.\n *\n * Returns `null` when:\n * - the header version nibble is not V2\n * - reserved header bits are set (guards against future-version payloads)\n * - neither flag is set (invalid: V2 must carry `c` and/or `w`)\n * - the byte length does not match the length implied by the header flags\n * - the decoded wallet does not pass `isAddress` (defense-in-depth against\n * crafted payloads that round-trip by length but carry junk)\n */\nexport function decodeFrakContextV2(buf: Uint8Array): FrakContextV2 | null {\n if (buf.length < HEADER_BYTES + UUID_BYTES + TIMESTAMP_BYTES) return null;\n\n const header = buf[0];\n if ((header & VERSION_MASK) !== VERSION_V2) return null;\n if ((header & RESERVED_MASK) !== 0) return null;\n\n const hasC = (header & FLAG_HAS_C) !== 0;\n const hasW = (header & FLAG_HAS_W) !== 0;\n if (!hasC && !hasW) return null;\n\n const expected =\n HEADER_BYTES +\n UUID_BYTES +\n TIMESTAMP_BYTES +\n (hasC ? UUID_BYTES : 0) +\n (hasW ? ADDRESS_BYTES : 0);\n if (buf.length !== expected) return null;\n\n let offset = HEADER_BYTES;\n\n const m = bytesToUuid(buf.subarray(offset, offset + UUID_BYTES));\n offset += UUID_BYTES;\n\n const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);\n const t = view.getUint32(offset, false);\n offset += TIMESTAMP_BYTES;\n\n const out: FrakContextV2 = { v: 2, m, t };\n\n if (hasC) {\n out.c = bytesToUuid(buf.subarray(offset, offset + UUID_BYTES));\n offset += UUID_BYTES;\n }\n\n if (hasW) {\n const walletHex = bytesToAddress(\n buf.subarray(offset, offset + ADDRESS_BYTES)\n );\n if (!isAddress(walletHex)) return null;\n out.w = walletHex;\n offset += ADDRESS_BYTES;\n }\n\n return out;\n}\n\n/**\n * Quick length-based probe to tell V1 (20-byte wallet address) apart from a V2\n * binary payload. Exposed so the outer decoder can branch without re-parsing.\n */\nexport function isV2BinaryLength(byteLength: number): boolean {\n return (\n byteLength ===\n HEADER_BYTES + UUID_BYTES + TIMESTAMP_BYTES + UUID_BYTES ||\n byteLength ===\n HEADER_BYTES + UUID_BYTES + TIMESTAMP_BYTES + ADDRESS_BYTES ||\n byteLength ===\n HEADER_BYTES +\n UUID_BYTES +\n TIMESTAMP_BYTES +\n UUID_BYTES +\n ADDRESS_BYTES\n );\n}\n","import type {\n AttributionParams,\n FrakContext,\n FrakContextV1,\n FrakContextV2,\n} from \"../types\";\nimport { isV2Context } from \"../types\";\nimport { base64urlDecode, base64urlEncode } from \"../utils/compression/b64\";\nimport {\n deleteQueryParamCaseInsensitive,\n getQueryParamCaseInsensitive,\n} from \"../utils/url/queryParams\";\nimport { addressToBytes, bytesToAddress, isAddress } from \"./address\";\nimport { decodeFrakContextV2, encodeFrakContextV2 } from \"./frakContextV2Codec\";\n\n/**\n * URL parameter key for the Frak referral context\n */\nconst contextKey = \"fCtx\";\n\n/**\n * Compress a Frak context into a URL-safe string.\n *\n * - V2 contexts are encoded using a compact binary layout (see\n * {@link encodeFrakContextV2}) then base64url-encoded.\n * - V1 contexts encode the wallet address as raw bytes (base64url).\n *\n * @param context - The context to compress (V1 or V2)\n * @returns A compressed base64url string, or undefined on failure\n */\nfunction compress(context?: FrakContextV1 | FrakContextV2): string | undefined {\n if (!context) return;\n try {\n if (isV2Context(context)) {\n const encoded = encodeFrakContextV2(context);\n if (!encoded) return undefined;\n return base64urlEncode(encoded);\n }\n\n // V1 legacy: compress wallet address as raw bytes\n const bytes = addressToBytes(context.r);\n return base64urlEncode(bytes);\n } catch (e) {\n console.error(\"Error compressing Frak context\", { e, context });\n }\n return undefined;\n}\n\n/**\n * Decompress a base64url string back into a Frak context.\n *\n * V1 (exactly 20 bytes) and V2 (37, 41, or 57 bytes) are distinguished by\n * their decoded byte length, so there is no ambiguity.\n *\n * @param context - The compressed context string\n * @returns The decompressed FrakContext, or undefined on failure\n */\nfunction decompress(context?: string): FrakContext | undefined {\n if (!context || context.length === 0) return;\n try {\n const bytes = base64urlDecode(context);\n\n // V1 is a raw 20-byte wallet address; V2 binary is always longer\n // and starts with a header byte whose low nibble is the version.\n if (bytes.length !== 20) {\n const v2 = decodeFrakContextV2(bytes);\n if (v2) return v2;\n return undefined;\n }\n\n const hex = bytesToAddress(bytes);\n if (isAddress(hex)) {\n return { r: hex };\n }\n } catch (e) {\n console.error(\"Error decompressing Frak context\", { e, context });\n }\n return undefined;\n}\n\n/**\n * Parse a URL to extract the Frak referral context from the `fCtx` query parameter.\n *\n * The key is matched case-insensitively: some link channels (emails, messaging\n * apps) lowercase query-param keys in transit, so `fCtx` can arrive as `fctx`.\n *\n * @param args\n * @param args.url - The URL to parse\n * @returns The parsed FrakContext, or null if absent\n */\nfunction parse({ url }: { url: string }): FrakContext | null | undefined {\n if (!url) return null;\n\n const urlObj = new URL(url);\n const frakContext = getQueryParamCaseInsensitive(\n urlObj.searchParams,\n contextKey\n );\n if (!frakContext) return null;\n\n return decompress(frakContext);\n}\n\n/**\n * Default utm_source / via value when attribution is requested.\n */\nconst DEFAULT_ATTRIBUTION_SOURCE = \"frak\";\n\n/**\n * Resolve attribution defaults from the provided context.\n *\n * V2 contexts expose the merchantId (`m`) and, when anonymous, the clientId\n * (`c`), which feed `utm_campaign` and `ref` respectively. When V2 only carries\n * a wallet (`w`), `ref` is intentionally left unset — we don't want wallet\n * addresses leaking into UTM params. V1 contexts have no equivalent.\n */\nfunction resolveAttributionValues(\n overrides: AttributionParams\n): Record<string, string | undefined> {\n return {\n utm_source: overrides.utmSource ?? DEFAULT_ATTRIBUTION_SOURCE,\n utm_medium: overrides.utmMedium ?? undefined,\n utm_campaign: overrides.utmCampaign ?? undefined,\n utm_content: overrides.utmContent,\n utm_term: overrides.utmTerm,\n via: overrides.via ?? undefined,\n ref: overrides.ref ?? undefined,\n };\n}\n\n/**\n * Append attribution query params to a URL using gap-fill semantics.\n *\n * Existing params on the URL are preserved untouched (so merchant-provided\n * UTMs take precedence). Only missing keys are populated.\n */\nfunction applyAttributionParams(\n urlObj: URL,\n attribution?: AttributionParams\n): void {\n const values = resolveAttributionValues(attribution ?? {});\n for (const [key, value] of Object.entries(values)) {\n if (value === undefined || value === \"\") continue;\n if (urlObj.searchParams.has(key)) continue;\n urlObj.searchParams.set(key, value);\n }\n}\n\n/**\n * Add or replace the `fCtx` query parameter in a URL with the given context.\n *\n * Standard affiliation params (`utm_source`, `utm_medium`, `utm_campaign`,\n * `ref`, `via`, ...) are always appended using gap-fill semantics: pre-existing\n * params on the URL are preserved, defaults are derived from the context when\n * applicable, and `attribution` overrides take precedence when provided.\n *\n * @param args\n * @param args.url - The URL to update\n * @param args.context - The context to embed (V1 or V2)\n * @param args.attribution - Optional attribution overrides. Defaults are applied even when omitted.\n * @returns The updated URL string, or null on failure\n */\nfunction update({\n url,\n context,\n attribution,\n}: {\n url?: string;\n context: FrakContextV1 | FrakContextV2;\n attribution?: AttributionParams;\n}): string | null {\n if (!url) return null;\n\n const compressedContext = compress(context);\n if (!compressedContext) return null;\n\n const urlObj = new URL(url);\n deleteQueryParamCaseInsensitive(urlObj.searchParams, contextKey);\n urlObj.searchParams.set(contextKey, compressedContext);\n applyAttributionParams(urlObj, attribution);\n return urlObj.toString();\n}\n\n/**\n * Remove the `fCtx` query parameter from a URL.\n *\n * @param url - The URL to strip the context from\n * @returns The cleaned URL string\n */\nfunction remove(url: string): string {\n const urlObj = new URL(url);\n deleteQueryParamCaseInsensitive(urlObj.searchParams, contextKey);\n return urlObj.toString();\n}\n\n/**\n * Replace the current browser URL with an updated Frak context.\n *\n * - If `context` is non-null, embeds it via {@link update}.\n * - If `context` is null, strips the context via {@link remove}.\n *\n * @param args\n * @param args.url - Base URL (defaults to `window.location.href`)\n * @param args.context - Context to set, or null to remove\n */\nfunction replaceUrl({\n url: baseUrl,\n context,\n}: {\n url?: string;\n context: FrakContextV1 | FrakContextV2 | null;\n}) {\n if (!window.location?.href || typeof window === \"undefined\") {\n console.error(\"No window found, can't update context\");\n return;\n }\n\n const url = baseUrl ?? window.location.href;\n\n let newUrl: string | null;\n if (context !== null) {\n newUrl = update({ url, context });\n } else {\n newUrl = remove(url);\n }\n\n if (!newUrl) return;\n\n window.history.replaceState(null, \"\", newUrl.toString());\n}\n\n/**\n * Manager for Frak referral context in URLs.\n *\n * Handles compression, decompression, URL parsing, and browser history updates\n * for both V1 (wallet address) and V2 (anonymous clientId) referral contexts.\n */\nexport const FrakContextManager = {\n compress,\n decompress,\n parse,\n update,\n remove,\n replaceUrl,\n};\n"],"mappings":"4CAKA,MAAM,EAAgB,iBAMtB,SAAS,GAAuB,CAK5B,OAJI,OAAO,OAAW,KAAe,OAAO,WACjC,OAAO,WAAW,EAGtB,uCAAuC,QAAQ,QAAU,GAAM,CAClE,IAAM,EAAK,KAAK,OAAO,EAAI,GAAM,EAEjC,OADU,IAAM,IAAM,EAAK,EAAI,EAAO,EAAA,CAC7B,SAAS,EAAE,CACxB,CAAC,CACL,CAMA,SAAgB,GAAsB,CAClC,GAAI,OAAO,OAAW,KAAe,CAAC,OAAO,aAKzC,OAHA,QAAQ,KACJ,oEACJ,EACO,EAAa,EAGxB,IAAI,EAAW,aAAa,QAAQ,CAAa,EAKjD,OAJK,IACD,EAAW,EAAa,EACxB,aAAa,QAAQ,EAAe,CAAQ,GAEzC,CACX,CCjCA,IAAa,EAAb,cAA8C,GAAoB,CAC9D,QAEA,YAAY,EAAc,CACtB,MAAM,EACN,KAAK,QAAU,CACnB,CAEA,IAAa,EAAa,CACtB,IAAM,EAAQ,M