diff --git a/Cargo.lock b/Cargo.lock index 1d32aa3..3a81e53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6295,6 +6295,7 @@ dependencies = [ "rocksdb", "sha3 0.11.0-rc.3", "tokio", + "tower-http", "tracing", "tracing-subscriber", "uts-bmt", diff --git a/apps/web/index.html b/apps/web/index.html index 508eabd..bca08bf 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -1,12 +1,18 @@ - + - web + UTS — Universal Timestamps + + + - +
diff --git a/apps/web/package.json b/apps/web/package.json index 9bfdbc0..1d6f953 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,13 +11,23 @@ "typecheck": "vue-tsc --noEmit" }, "dependencies": { - "vue": "^3.5.24", - "@uts/sdk": "workspace:*" + "@noble/hashes": "^2.0.1", + "@uts/sdk": "workspace:*", + "@vueuse/core": "^14.2.1", + "date-fns": "^4.1.0", + "ethers": "^6.16.0", + "jszip": "^3.10.1", + "lucide-vue-next": "^0.575.0", + "pinia": "^3.0.4", + "vue": "^3.5.24" }, "devDependencies": { + "@tailwindcss/vite": "^4.2.1", + "@types/jszip": "^3.4.1", "@types/node": "^24.10.1", "@vitejs/plugin-vue": "^6.0.1", "@vue/tsconfig": "^0.8.1", + "tailwindcss": "^4.2.1", "typescript": "~5.9.3", "vite": "^7.2.4", "vue-tsc": "^3.1.4" diff --git a/apps/web/src/App.vue b/apps/web/src/App.vue index aa54efc..9239bff 100644 --- a/apps/web/src/App.vue +++ b/apps/web/src/App.vue @@ -1,30 +1,7 @@ - - diff --git a/apps/web/src/assets/Scroll_Logomark.svg b/apps/web/src/assets/Scroll_Logomark.svg new file mode 100644 index 0000000..0e07355 --- /dev/null +++ b/apps/web/src/assets/Scroll_Logomark.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/components/HelloWorld.vue b/apps/web/src/components/HelloWorld.vue deleted file mode 100644 index 390dd42..0000000 --- a/apps/web/src/components/HelloWorld.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - - - diff --git a/apps/web/src/components/base/BaseButton.vue b/apps/web/src/components/base/BaseButton.vue new file mode 100644 index 0000000..66e3a34 --- /dev/null +++ b/apps/web/src/components/base/BaseButton.vue @@ -0,0 +1,49 @@ + + + diff --git a/apps/web/src/components/base/GlassCard.vue b/apps/web/src/components/base/GlassCard.vue new file mode 100644 index 0000000..71d946d --- /dev/null +++ b/apps/web/src/components/base/GlassCard.vue @@ -0,0 +1,17 @@ + + + diff --git a/apps/web/src/components/base/StatusBadge.vue b/apps/web/src/components/base/StatusBadge.vue new file mode 100644 index 0000000..cf5522c --- /dev/null +++ b/apps/web/src/components/base/StatusBadge.vue @@ -0,0 +1,49 @@ + + + diff --git a/apps/web/src/components/feed/LiveFeed.vue b/apps/web/src/components/feed/LiveFeed.vue new file mode 100644 index 0000000..9973c30 --- /dev/null +++ b/apps/web/src/components/feed/LiveFeed.vue @@ -0,0 +1,258 @@ + + + diff --git a/apps/web/src/components/stamp/StampingWorkflow.vue b/apps/web/src/components/stamp/StampingWorkflow.vue new file mode 100644 index 0000000..fe14475 --- /dev/null +++ b/apps/web/src/components/stamp/StampingWorkflow.vue @@ -0,0 +1,227 @@ + + + diff --git a/apps/web/src/components/terminal/HeroTerminal.vue b/apps/web/src/components/terminal/HeroTerminal.vue new file mode 100644 index 0000000..b3e87c7 --- /dev/null +++ b/apps/web/src/components/terminal/HeroTerminal.vue @@ -0,0 +1,281 @@ + + + diff --git a/apps/web/src/components/upgrade/UpgradePanel.vue b/apps/web/src/components/upgrade/UpgradePanel.vue new file mode 100644 index 0000000..815c7c6 --- /dev/null +++ b/apps/web/src/components/upgrade/UpgradePanel.vue @@ -0,0 +1,203 @@ + + + diff --git a/apps/web/src/components/verify/AttestationDetail.vue b/apps/web/src/components/verify/AttestationDetail.vue new file mode 100644 index 0000000..1bbe3ee --- /dev/null +++ b/apps/web/src/components/verify/AttestationDetail.vue @@ -0,0 +1,413 @@ + + + diff --git a/apps/web/src/components/verify/MerkleTreeViz.vue b/apps/web/src/components/verify/MerkleTreeViz.vue new file mode 100644 index 0000000..63e4dc1 --- /dev/null +++ b/apps/web/src/components/verify/MerkleTreeViz.vue @@ -0,0 +1,390 @@ + + + + + diff --git a/apps/web/src/components/verify/VerificationResult.vue b/apps/web/src/components/verify/VerificationResult.vue new file mode 100644 index 0000000..892b1b1 --- /dev/null +++ b/apps/web/src/components/verify/VerificationResult.vue @@ -0,0 +1,277 @@ + + + diff --git a/apps/web/src/composables/useFileDigest.ts b/apps/web/src/composables/useFileDigest.ts new file mode 100644 index 0000000..4bea4fc --- /dev/null +++ b/apps/web/src/composables/useFileDigest.ts @@ -0,0 +1,176 @@ +import { ref } from 'vue' +import { sha256 } from '@noble/hashes/sha2.js' +import { keccak_256 } from '@noble/hashes/sha3.js' +import { hexlify } from 'ethers/utils' +import type { SecureDigestOp, DigestHeader } from '@uts/sdk' + +export interface FileDigestResult { + fileName: string + fileSize: number + algorithm: SecureDigestOp + digest: string + header: DigestHeader +} + +// Estimated browser hashing throughput (~200 MB/s), used for fake progress +const ESTIMATED_THROUGHPUT = 200 * 1024 * 1024 +const MIN_DURATION_MS = 300 +const MAX_DURATION_MS = 30000 +const PROGRESS_CAP = 92 // fake progress caps at this before real completion + +export function useFileDigest() { + const isDigesting = ref(false) + const progress = ref(0) + const error = ref(null) + const result = ref(null) + + let progressTimer: ReturnType | null = null + + function startFakeProgress(totalBytes: number, fileCount: number) { + progress.value = 0 + const estimatedMs = Math.max( + MIN_DURATION_MS, + Math.min( + ((totalBytes * fileCount) / ESTIMATED_THROUGHPUT) * 1000, + MAX_DURATION_MS, + ), + ) + const intervalMs = 50 + const steps = estimatedMs / intervalMs + const increment = PROGRESS_CAP / steps + + progressTimer = setInterval(() => { + progress.value = Math.min(progress.value + increment, PROGRESS_CAP) + }, intervalMs) + } + + function stopFakeProgress() { + if (progressTimer) { + clearInterval(progressTimer) + progressTimer = null + } + progress.value = 100 + } + + async function digestFile( + file: File, + algorithm: SecureDigestOp = 'SHA256', + ): Promise { + isDigesting.value = true + progress.value = 0 + error.value = null + result.value = null + + startFakeProgress(file.size, 1) + + try { + const factory = algorithm === 'KECCAK256' ? keccak_256 : sha256 + const hasher = factory.create() + + const reader = file.stream().getReader() + while (true) { + const { done, value } = await reader.read() + if (done) break + hasher.update(value) + // Yield to main thread periodically for UI updates + await new Promise((resolve) => setTimeout(resolve, 0)) + } + + stopFakeProgress() + + const digestBytes = hasher.digest() + const digestHex = hexlify(digestBytes) + + const digestResult: FileDigestResult = { + fileName: file.webkitRelativePath || file.name, + fileSize: file.size, + algorithm, + digest: digestHex, + header: { + kind: algorithm, + digest: digestBytes, + }, + } + + result.value = digestResult + return digestResult + } catch (e) { + stopFakeProgress() + const msg = e instanceof Error ? e.message : 'Failed to digest file' + error.value = msg + throw new Error(msg) + } finally { + isDigesting.value = false + } + } + + async function digestFiles( + files: File[], + algorithm: SecureDigestOp = 'SHA256', + ): Promise { + isDigesting.value = true + progress.value = 0 + error.value = null + result.value = null + + const totalBytes = files.reduce((sum, f) => sum + f.size, 0) + startFakeProgress(totalBytes, files.length) + + try { + const results: FileDigestResult[] = [] + for (const file of files) { + const factory = algorithm === 'KECCAK256' ? keccak_256 : sha256 + const hasher = factory.create() + + const reader = file.stream().getReader() + while (true) { + const { done, value } = await reader.read() + if (done) break + hasher.update(value) + await new Promise((resolve) => setTimeout(resolve, 0)) + } + + const digestBytes = hasher.digest() + results.push({ + fileName: file.webkitRelativePath || file.name, + fileSize: file.size, + algorithm, + digest: hexlify(digestBytes), + header: { kind: algorithm, digest: digestBytes }, + }) + } + + stopFakeProgress() + if (results.length > 0) result.value = results[results.length - 1]! + return results + } catch (e) { + stopFakeProgress() + const msg = e instanceof Error ? e.message : 'Failed to digest files' + error.value = msg + throw new Error(msg) + } finally { + isDigesting.value = false + } + } + + function reset() { + if (progressTimer) { + clearInterval(progressTimer) + progressTimer = null + } + isDigesting.value = false + progress.value = 0 + error.value = null + result.value = null + } + + return { + isDigesting, + progress, + error, + result, + digestFile, + digestFiles, + reset, + } +} diff --git a/apps/web/src/composables/useTimestampSDK.ts b/apps/web/src/composables/useTimestampSDK.ts new file mode 100644 index 0000000..579e91c --- /dev/null +++ b/apps/web/src/composables/useTimestampSDK.ts @@ -0,0 +1,286 @@ +import { ref, shallowRef } from 'vue' +import { SDK, VerifyStatus, UpgradeStatus, Decoder, Encoder } from '@uts/sdk' +import type { + DetachedTimestamp, + AttestationStatus, + UpgradeResult, + DigestHeader, + StampEventCallback, +} from '@uts/sdk' +import type { Eip1193Provider } from 'ethers' +import JSZip from 'jszip' + +export type StampPhase = + | 'idle' + | 'generating-nonce' + | 'building-merkle-tree' + | 'broadcasting' + | 'waiting-attestation' + | 'building-proof' + | 'complete' + | 'upgrading' + | 'upgraded' + | 'error' + +let _sdkInstance: SDK | null = null + +export function getSDK(): SDK { + if (!_sdkInstance) { + _sdkInstance = new SDK({ timeout: 15000 }) + } + return _sdkInstance +} + +export function resetSDK(options?: { + calendars?: URL[] + web3Provider?: Eip1193Provider | null +}) { + _sdkInstance = new SDK({ + timeout: 15000, + calendars: options?.calendars, + web3Provider: options?.web3Provider, + }) +} + +export function setWeb3Provider(provider: Eip1193Provider | null) { + const sdk = getSDK() + sdk.web3Provider = provider +} + +function triggerDownload(blob: Blob, fileName: string) { + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = fileName + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) +} + +export function downloadOtsFile(stamp: DetachedTimestamp, fileName?: string) { + const encoded = Encoder.encodeDetachedTimestamp(stamp) + const blob = new Blob([encoded as BlobPart], { + type: 'application/octet-stream', + }) + triggerDownload(blob, fileName ? `${fileName}.ots` : 'timestamp.ots') +} + +async function downloadStampsAsZip( + stamps: DetachedTimestamp[], + names: string[], +) { + const zip = new JSZip() + for (let i = 0; i < stamps.length; i++) { + const fileName = names[i] ?? `file-${i}` + const encoded = Encoder.encodeDetachedTimestamp(stamps[i]!) + // Preserve directory structure in zip (fileName may contain path separators) + zip.file(`${fileName}.ots`, encoded) + } + const blob = await zip.generateAsync({ type: 'blob' }) + triggerDownload(blob, 'timestamps.zip') +} + +export function useTimestampSDK() { + const sdk = getSDK() + + const stampPhase = ref('idle') + const stampError = ref(null) + const stampResult = shallowRef(null) + const stampFileNames = ref([]) + const broadcastProgress = ref('') + const upgradeResults = shallowRef(null) + + const verifyStatus = ref(null) + const verifyAttestations = shallowRef([]) + const isVerifying = ref(false) + const verifyError = ref(null) + + let upgradeTimer: ReturnType | null = null + + async function stamp( + digests: DigestHeader[], + fileNames?: string[], + ): Promise { + stampPhase.value = 'generating-nonce' + stampError.value = null + stampResult.value = null + stampFileNames.value = fileNames ?? [] + broadcastProgress.value = '' + upgradeResults.value = null + + const onEvent: StampEventCallback = (event) => { + switch (event.phase) { + case 'generating-nonce': + stampPhase.value = 'generating-nonce' + break + case 'building-merkle-tree': + stampPhase.value = 'building-merkle-tree' + break + case 'broadcasting': + stampPhase.value = 'broadcasting' + broadcastProgress.value = `0/${event.totalCalendars}` + break + case 'calendar-response': + broadcastProgress.value = `${event.responsesReceived}/${event.totalCalendars}${event.success ? '' : ' (failed: ' + event.calendarUrl + ')'}` + break + case 'building-proof': + stampPhase.value = 'building-proof' + break + case 'complete': + stampPhase.value = 'complete' + break + } + } + + try { + const results = await sdk.stamp(digests, onEvent) + + stampPhase.value = 'complete' + stampResult.value = results + + // Start polling for upgrade (no auto-download — user uses the download button) + startUpgradePolling(results) + + return results + } catch (e) { + stampPhase.value = 'error' + stampError.value = e instanceof Error ? e.message : 'Stamping failed' + throw e + } + } + + async function downloadPendingStamp() { + if (!stampResult.value) return + const results = stampResult.value + const names = stampFileNames.value + + if (results.length === 1) { + downloadOtsFile(results[0]!, names[0]) + } else if (results.length > 1) { + await downloadStampsAsZip(results, names) + } + } + + function startUpgradePolling( + stamps: DetachedTimestamp[], + keepPending?: boolean, + ) { + stopUpgradePolling() + stampPhase.value = 'upgrading' + + let attempts = 0 + const maxAttempts = 40 // ~10 minutes at 15s intervals + + upgradeTimer = setInterval(async () => { + attempts++ + try { + const allResults: UpgradeResult[] = [] + for (const s of stamps) { + const results = await sdk.upgrade(s, keepPending ?? false) + allResults.push(...results) + } + upgradeResults.value = allResults + + const hasUpgraded = allResults.some( + (r) => r.status === UpgradeStatus.Upgraded, + ) + if (hasUpgraded) { + stampPhase.value = 'upgraded' + // Auto-download upgraded timestamps with correct per-file names + const names = stampFileNames.value + if (stamps.length === 1) { + downloadOtsFile(stamps[0]!, names[0]) + } else if (stamps.length > 1) { + await downloadStampsAsZip(stamps, names) + } + stopUpgradePolling() + } else if (attempts >= maxAttempts) { + stopUpgradePolling() + } + } catch { + // Silently retry on next interval + } + }, 15000) + } + + function stopUpgradePolling() { + if (upgradeTimer) { + clearInterval(upgradeTimer) + upgradeTimer = null + } + } + + async function verify( + stamp: DetachedTimestamp, + ): Promise<{ status: VerifyStatus; attestations: AttestationStatus[] }> { + isVerifying.value = true + verifyError.value = null + verifyStatus.value = null + verifyAttestations.value = [] + + try { + const attestations = await sdk.verify(stamp) + const status = sdk.transformResult(attestations) + + verifyStatus.value = status + verifyAttestations.value = attestations + return { status, attestations } + } catch (e) { + verifyError.value = e instanceof Error ? e.message : 'Verification failed' + throw e + } finally { + isVerifying.value = false + } + } + + async function upgrade( + detached: DetachedTimestamp, + keepPending?: boolean, + ): Promise { + return sdk.upgrade(detached, keepPending ?? false) + } + + function decodeOtsFile(data: Uint8Array): DetachedTimestamp { + const decoder = new Decoder(data) + return decoder.readDetachedTimestamp() + } + + function resetStamp() { + stampPhase.value = 'idle' + stampError.value = null + stampResult.value = null + stampFileNames.value = [] + broadcastProgress.value = '' + upgradeResults.value = null + stopUpgradePolling() + } + + function resetVerify() { + verifyStatus.value = null + verifyAttestations.value = [] + isVerifying.value = false + verifyError.value = null + } + + return { + stampPhase, + stampError, + stampResult, + stampFileNames, + broadcastProgress, + upgradeResults, + stamp, + downloadPendingStamp, + resetStamp, + + verifyStatus, + verifyAttestations, + isVerifying, + verifyError, + verify, + upgrade, + decodeOtsFile, + resetVerify, + } +} diff --git a/apps/web/src/composables/useWallet.ts b/apps/web/src/composables/useWallet.ts new file mode 100644 index 0000000..6c221c2 --- /dev/null +++ b/apps/web/src/composables/useWallet.ts @@ -0,0 +1,131 @@ +import { ref, computed } from 'vue' +import { BrowserProvider } from 'ethers' +import type { Eip1193Provider } from 'ethers' +import { WELL_KNOWN_CHAINS } from '@uts/sdk' + +const walletAddress = ref(null) +const walletChainId = ref(null) +const isConnecting = ref(false) +const walletError = ref(null) + +const isConnected = computed(() => walletAddress.value !== null) +const hasWallet = computed( + () => typeof window !== 'undefined' && !!(window as any).ethereum, +) + +const CHAIN_NAMES: Record = { + 1: 'Ethereum', + 17000: 'Holesky', + 11155111: 'Sepolia', + 534352: 'Scroll', + 534351: 'Scroll Sepolia', +} + +const walletChainName = computed(() => { + if (!walletChainId.value) return null + return CHAIN_NAMES[walletChainId.value] ?? `Chain ${walletChainId.value}` +}) + +function getEip1193Provider(): Eip1193Provider | null { + if (typeof window === 'undefined') return null + return (window as any).ethereum ?? null +} + +function truncateAddress(address: string): string { + return `${address.slice(0, 6)}...${address.slice(-4)}` +} + +export function useWallet() { + async function connect() { + const ethereum = getEip1193Provider() + if (!ethereum) { + walletError.value = + 'No wallet detected. Install MetaMask or another EIP-1193 wallet.' + return + } + + isConnecting.value = true + walletError.value = null + + try { + const provider = new BrowserProvider(ethereum) + const accounts = await provider.send('eth_requestAccounts', []) + if (accounts.length === 0) { + walletError.value = 'No accounts returned from wallet' + return + } + walletAddress.value = accounts[0] ?? null + + const network = await provider.getNetwork() + walletChainId.value = Number(network.chainId) + + // Listen for account/chain changes + ;(ethereum as any).on?.('accountsChanged', handleAccountsChanged) + ;(ethereum as any).on?.('chainChanged', handleChainChanged) + } catch (e) { + walletError.value = + e instanceof Error ? e.message : 'Failed to connect wallet' + } finally { + isConnecting.value = false + } + } + + function disconnect() { + const ethereum = getEip1193Provider() + if (ethereum) { + ;(ethereum as any).removeListener?.( + 'accountsChanged', + handleAccountsChanged, + ) + ;(ethereum as any).removeListener?.('chainChanged', handleChainChanged) + } + walletAddress.value = null + walletChainId.value = null + walletError.value = null + } + + async function switchChain(chainId: number) { + const ethereum = getEip1193Provider() + if (!ethereum) return + + const knownChain = WELL_KNOWN_CHAINS[chainId] + if (!knownChain) return + + try { + await ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: knownChain.chainId }], + }) + } catch { + // switch failed + } + } + + return { + walletAddress, + walletChainId, + walletChainName, + isConnected, + isConnecting, + hasWallet, + walletError, + connect, + disconnect, + switchChain, + getEip1193Provider, + truncateAddress, + } +} + +function handleAccountsChanged(accounts: string[]) { + if (accounts.length === 0) { + walletAddress.value = null + walletChainId.value = null + } else { + walletAddress.value = accounts[0] ?? null + } +} + +function handleChainChanged(chainIdHex: string) { + walletChainId.value = parseInt(chainIdHex, 16) +} diff --git a/apps/web/src/composables/useWebSocketFeed.ts b/apps/web/src/composables/useWebSocketFeed.ts new file mode 100644 index 0000000..555e8f3 --- /dev/null +++ b/apps/web/src/composables/useWebSocketFeed.ts @@ -0,0 +1,191 @@ +import { ref, onUnmounted } from 'vue' +import { BrowserProvider } from 'ethers' +import type { Eip1193Provider } from 'ethers' +import { SDK } from '@uts/sdk' +import { getSDK } from './useTimestampSDK' + +const CHAIN_NAMES: Record = { + 1: 'Ethereum', + 17000: 'Holesky', + 11155111: 'Sepolia', + 534352: 'Scroll', + 534351: 'Scroll Sepolia', +} + +export interface FeedEntry { + id: string + hash: string + type: 'ethereum' + chain: string + chainId: number + blockHeight: number + sender?: string + txHash?: string + timestamp: number +} + +export function useWebSocketFeed() { + const entries = ref([]) + const isConnected = ref(false) + const seenIds = new Set() + let intervalId: ReturnType | null = null + let lastBlockWeb3: Record = {} + let lastBlockRPC: Record = {} + + function addEntry(entry: FeedEntry) { + if (seenIds.has(entry.id)) return + seenIds.add(entry.id) + entries.value.unshift(entry) + if (entries.value.length > 50) { + const removed = entries.value.splice(50) + for (const r of removed) seenIds.delete(r.id) + } + } + + /** Poll web3Provider. Returns the wallet's chainId on success, or null. */ + async function fetchEventsFromWeb3( + web3Provider: Eip1193Provider, + ): Promise { + try { + const provider = new BrowserProvider(web3Provider) + const network = await provider.getNetwork() + const chainId = Number(network.chainId) + + const currentBlock = await provider.getBlockNumber() + const fromBlock = lastBlockWeb3[chainId] + ? lastBlockWeb3[chainId] + 1 + : currentBlock - 10 + + if (fromBlock > currentBlock) return chainId + + const logs = await provider.getLogs({ + fromBlock, + toBlock: currentBlock, + topics: [SDK.utsLogTopic], + }) + + for (const log of logs) { + const parsed = SDK.utsInterface.parseLog(log) + if (!parsed) continue + + addEntry({ + id: `${chainId}-${log.blockNumber}-${log.index}`, + hash: parsed.args[0], + type: 'ethereum', + chain: CHAIN_NAMES[chainId] ?? `Chain ${chainId}`, + chainId, + blockHeight: log.blockNumber, + sender: parsed.args[1], + txHash: log.transactionHash, + timestamp: Number(parsed.args[2] as bigint) * 1000, + }) + } + + lastBlockWeb3[chainId] = currentBlock + return chainId + } catch (e) { + console.warn('Feed: failed to poll web3Provider:', e) + return null + } + } + + /** Poll ethRPCs, skipping chains already covered by web3Provider. */ + async function fetchEventsFromRPCs( + sdk: SDK, + skipChainIds: Set = new Set(), + ) { + const chainIds = Object.keys(sdk.ethRPCs).map(Number) + + for (const chainId of chainIds) { + if (skipChainIds.has(chainId)) continue + + const provider = sdk.getEthProvider(chainId) + if (!provider) continue + + try { + const currentBlock = await provider.getBlockNumber() + const fromBlock = lastBlockRPC[chainId] + ? lastBlockRPC[chainId] + 1 + : currentBlock - 5 + + if (fromBlock > currentBlock) continue + + const logs = await provider.getLogs({ + fromBlock, + toBlock: currentBlock, + topics: [SDK.utsLogTopic], + }) + + for (const log of logs) { + const parsed = SDK.utsInterface.parseLog(log) + if (!parsed) continue + + addEntry({ + id: `${chainId}-${log.blockNumber}-${log.index}`, + hash: parsed.args[0], + type: 'ethereum', + chain: CHAIN_NAMES[chainId] ?? `Chain ${chainId}`, + chainId, + blockHeight: log.blockNumber, + sender: parsed.args[1], + txHash: log.transactionHash, + timestamp: Number(parsed.args[2] as bigint) * 1000, + }) + } + + lastBlockRPC[chainId] = currentBlock + } catch (e) { + console.warn(`Feed: failed to poll chain ${chainId}:`, e) + } + } + } + + /** Poll both web3Provider and ethRPCs, deduplicating by entry id. */ + async function pollAll() { + const sdk = getSDK() + const skipChainIds = new Set() + + // Poll web3Provider first (if available) + if (sdk.web3Provider) { + const web3ChainId = await fetchEventsFromWeb3(sdk.web3Provider) + if (web3ChainId !== null) { + // Skip this chain in ethRPCs since web3Provider already covers it + skipChainIds.add(web3ChainId) + } + } + + // Also poll ethRPCs for all remaining chains + await fetchEventsFromRPCs(sdk, skipChainIds) + } + + async function connect() { + isConnected.value = true + lastBlockWeb3 = {} + lastBlockRPC = {} + + await pollAll() + + intervalId = setInterval(() => { + pollAll() + }, 15000) + } + + function disconnect() { + isConnected.value = false + if (intervalId) { + clearInterval(intervalId) + intervalId = null + } + } + + onUnmounted(() => { + disconnect() + }) + + return { + entries, + isConnected, + connect, + disconnect, + } +} diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index 2425c0f..a2cedb6 100644 --- a/apps/web/src/main.ts +++ b/apps/web/src/main.ts @@ -1,5 +1,8 @@ import { createApp } from 'vue' +import { createPinia } from 'pinia' import './style.css' import App from './App.vue' -createApp(App).mount('#app') +const app = createApp(App) +app.use(createPinia()) +app.mount('#app') diff --git a/apps/web/src/stores/app.ts b/apps/web/src/stores/app.ts new file mode 100644 index 0000000..d876af3 --- /dev/null +++ b/apps/web/src/stores/app.ts @@ -0,0 +1,268 @@ +import { defineStore } from 'pinia' +import { ref, computed, watch } from 'vue' +import { DEFAULT_CALENDARS } from '@uts/sdk' +import type { DetachedTimestamp, SecureDigestOp } from '@uts/sdk' +import { getSDK, resetSDK } from '@/composables/useTimestampSDK' +import { JsonRpcProvider } from 'ethers' + +const CHAIN_NAMES: Record = { + 1: 'Ethereum', + 17000: 'Holesky', + 11155111: 'Sepolia', + 534352: 'Scroll', + 534351: 'Scroll Sepolia', +} + +export interface EthChainNode { + chainId: number + name: string + status: 'online' | 'offline' | 'checking' + latency?: number + rpcUrl?: string +} + +const STORAGE_KEY = 'uts-calendars' +const SETTINGS_KEY = 'uts-settings' +const CHAINS_KEY = 'uts-custom-chains' +const CHAIN_RPCS_KEY = 'uts-chain-rpcs' + +// Default chain IDs from SDK ethRPCs +function getDefaultChainIds(): number[] { + return Object.keys(getSDK().ethRPCs).map(Number) +} + +function loadCalendars(): string[] { + try { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored) return JSON.parse(stored) + } catch { + /* ignore */ + } + return DEFAULT_CALENDARS.map((u) => u.toString()) +} + +function saveCalendars(urls: string[]) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(urls)) +} + +function loadSettings(): { + keepPending: boolean + internalHashAlgo: SecureDigestOp +} { + try { + const stored = localStorage.getItem(SETTINGS_KEY) + if (stored) { + const parsed = JSON.parse(stored) + return { + keepPending: parsed.keepPending ?? false, + internalHashAlgo: parsed.internalHashAlgo ?? 'KECCAK256', + } + } + } catch { + /* ignore */ + } + return { keepPending: false, internalHashAlgo: 'KECCAK256' } +} + +function saveSettings(settings: { + keepPending: boolean + internalHashAlgo: SecureDigestOp +}) { + localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)) +} + +function loadCustomChains(): number[] | null { + try { + const stored = localStorage.getItem(CHAINS_KEY) + if (stored) return JSON.parse(stored) + } catch { + /* ignore */ + } + return null +} + +function saveCustomChains(chainIds: number[]) { + localStorage.setItem(CHAINS_KEY, JSON.stringify(chainIds)) +} + +function loadChainRpcs(): Record { + try { + const stored = localStorage.getItem(CHAIN_RPCS_KEY) + if (stored) return JSON.parse(stored) + } catch { + /* ignore */ + } + return {} +} + +function saveChainRpcs(rpcs: Record) { + localStorage.setItem(CHAIN_RPCS_KEY, JSON.stringify(rpcs)) +} + +// Public RPC endpoints for common chains (used when user adds a chain not in SDK ethRPCs) +const PUBLIC_RPCS: Record = { + 1: 'https://eth.llamarpc.com', + 17000: 'https://rpc.holesky.ethpandaops.io', + 11155111: 'https://rpc.sepolia.org', + 534352: 'https://rpc.scroll.io', + 534351: 'https://sepolia-rpc.scroll.io', + 42161: 'https://arb1.arbitrum.io/rpc', + 10: 'https://mainnet.optimism.io', + 8453: 'https://mainnet.base.org', + 137: 'https://polygon-rpc.com', +} + +export const useAppStore = defineStore('app', () => { + const calendarUrls = ref(loadCalendars()) + const keepPending = ref(loadSettings().keepPending) + const internalHashAlgo = ref(loadSettings().internalHashAlgo) + const ethChains = ref([]) + const recentStamps = ref([]) + + // Chain IDs to monitor (persisted or default from SDK) + const customChainIds = ref( + loadCustomChains() ?? getDefaultChainIds(), + ) + + // Custom RPC endpoints per chain (persisted) + const customRpcs = ref>(loadChainRpcs()) + + const onlineCount = computed( + () => ethChains.value.filter((c) => c.status === 'online').length, + ) + + watch( + calendarUrls, + (urls) => { + saveCalendars(urls) + resetSDK({ calendars: urls.map((u) => new URL(u)) }) + }, + { deep: true }, + ) + + watch(keepPending, (val) => { + saveSettings({ keepPending: val, internalHashAlgo: internalHashAlgo.value }) + }) + + watch(internalHashAlgo, (val) => { + saveSettings({ keepPending: keepPending.value, internalHashAlgo: val }) + }) + + async function checkChains() { + const sdk = getSDK() + const chainIds = customChainIds.value + + ethChains.value = chainIds.map((chainId) => { + const rpc = customRpcs.value[chainId] ?? PUBLIC_RPCS[chainId] + return { + chainId, + name: CHAIN_NAMES[chainId] ?? `Chain ${chainId}`, + status: 'checking' as const, + rpcUrl: rpc, + } + }) + + for (const chain of ethChains.value) { + // Try custom RPC first, then SDK provider, then public RPC fallback + let provider: JsonRpcProvider | null = null + const rpc = customRpcs.value[chain.chainId] + if (rpc) { + try { + provider = new JsonRpcProvider(rpc) + } catch { + /* ignore */ + } + } + if (!provider) { + provider = sdk.getEthProvider(chain.chainId) + } + if (!provider && PUBLIC_RPCS[chain.chainId]) { + try { + provider = new JsonRpcProvider(PUBLIC_RPCS[chain.chainId]) + } catch { + /* ignore */ + } + } + + if (!provider) { + chain.status = 'offline' + continue + } + const start = performance.now() + try { + await provider.getBlockNumber() + chain.latency = Math.round(performance.now() - start) + chain.status = 'online' + } catch { + chain.status = 'offline' + chain.latency = undefined + } + } + } + + function addChain(chainId: number) { + if (customChainIds.value.includes(chainId)) return + customChainIds.value.push(chainId) + saveCustomChains(customChainIds.value) + } + + function removeChain(chainId: number) { + customChainIds.value = customChainIds.value.filter((id) => id !== chainId) + ethChains.value = ethChains.value.filter((c) => c.chainId !== chainId) + saveCustomChains(customChainIds.value) + } + + function resetChains() { + customChainIds.value = getDefaultChainIds() + customRpcs.value = {} + localStorage.removeItem(CHAINS_KEY) + localStorage.removeItem(CHAIN_RPCS_KEY) + } + + function setChainRpc(chainId: number, rpcUrl: string) { + const trimmed = rpcUrl.trim() + if (trimmed) { + customRpcs.value[chainId] = trimmed + } else { + delete customRpcs.value[chainId] + } + saveChainRpcs(customRpcs.value) + // Update the rpcUrl in the displayed chain list + const chain = ethChains.value.find((c) => c.chainId === chainId) + if (chain) { + chain.rpcUrl = trimmed || PUBLIC_RPCS[chainId] + } + } + + function addStamp(stamp: DetachedTimestamp) { + recentStamps.value.unshift(stamp) + if (recentStamps.value.length > 20) { + recentStamps.value.pop() + } + } + + function setCalendars(urls: string[]) { + calendarUrls.value = urls + } + + function resetCalendars() { + calendarUrls.value = DEFAULT_CALENDARS.map((u) => u.toString()) + } + + return { + calendarUrls, + keepPending, + internalHashAlgo, + ethChains, + recentStamps, + onlineCount, + checkChains, + addChain, + removeChain, + resetChains, + setChainRpc, + addStamp, + setCalendars, + resetCalendars, + } +}) diff --git a/apps/web/src/style.css b/apps/web/src/style.css index f691315..8442c96 100644 --- a/apps/web/src/style.css +++ b/apps/web/src/style.css @@ -1,79 +1,168 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; +@import 'tailwindcss'; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; +@theme { + --color-deep-black: #050505; + --color-midnight: #0a0f14; + --color-neon-cyan: #00f3ff; + --color-neon-purple: #bc13fe; + --color-neon-orange: #ff9e00; + --color-glass: rgba(255, 255, 255, 0.04); + --color-glass-border: rgba(255, 255, 255, 0.08); + --color-surface: #0d1117; + --color-surface-light: #161b22; + --color-valid: #00ff88; + --color-invalid: #ff3366; + --color-pending: #ff9e00; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} + --font-mono: 'JetBrains Mono', ui-monospace, monospace; + --font-heading: 'Space Grotesk', system-ui, sans-serif; -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; + --animate-glow-pulse: glow-pulse 2s ease-in-out infinite; + --animate-scan: scan 4s linear infinite; + --animate-fade-in: fade-in 0.5s ease-out; + --animate-slide-up: slide-up 0.4s ease-out; + --animate-typewriter: typewriter 0.05s steps(1) infinite; + + @keyframes glow-pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + } + + @keyframes scan { + 0% { + transform: translateY(-100%); + } + 100% { + transform: translateY(100vh); + } + } + + @keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes slide-up { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } } +/* Base styles */ body { margin: 0; - display: flex; - place-items: center; - min-width: 320px; min-height: 100vh; + background: var(--color-deep-black); + overflow-x: hidden; } -h1 { - font-size: 3.2em; - line-height: 1.1; +#app { + width: 100%; + min-height: 100vh; } -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; +/* Scanline overlay */ +.scanlines::after { + content: ''; + position: fixed; + inset: 0; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0, 243, 255, 0.015) 2px, + rgba(0, 243, 255, 0.015) 4px + ); + pointer-events: none; + z-index: 9999; } -button:hover { - border-color: #646cff; + +/* Glass card */ +.glass { + background: var(--color-glass); + border: 1px solid var(--color-glass-border); + backdrop-filter: blur(12px); } -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; + +/* Glow effects */ +.glow-cyan { + box-shadow: + 0 0 10px rgba(0, 243, 255, 0.15), + 0 0 40px rgba(0, 243, 255, 0.05); } -.card { - padding: 2em; +.glow-purple { + box-shadow: + 0 0 10px rgba(188, 19, 254, 0.15), + 0 0 40px rgba(188, 19, 254, 0.05); } -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; +.glow-text-cyan { + text-shadow: 0 0 10px rgba(0, 243, 255, 0.5); } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } +.glow-text-valid { + text-shadow: 0 0 12px rgba(0, 255, 136, 0.6); +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(0, 243, 255, 0.2); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(0, 243, 255, 0.4); +} + +/* Transition classes */ +.fade-enter-active, +.fade-leave-active { + transition: + opacity 0.3s ease, + transform 0.3s ease; +} + +.fade-enter-from, +.fade-leave-to { + opacity: 0; + transform: translateY(8px); +} + +.list-enter-active, +.list-leave-active { + transition: all 0.4s ease; +} + +.list-enter-from { + opacity: 0; + transform: translateX(-20px); +} + +.list-leave-to { + opacity: 0; + transform: translateX(20px); } diff --git a/apps/web/src/views/HomeView.vue b/apps/web/src/views/HomeView.vue new file mode 100644 index 0000000..b03d321 --- /dev/null +++ b/apps/web/src/views/HomeView.vue @@ -0,0 +1,591 @@ + + + diff --git a/apps/web/tsconfig.app.json b/apps/web/tsconfig.app.json index 8d16e42..5839f41 100644 --- a/apps/web/tsconfig.app.json +++ b/apps/web/tsconfig.app.json @@ -1,8 +1,11 @@ { - "extends": "@vue/tsconfig/tsconfig.dom.json", + "extends": ["@vue/tsconfig/tsconfig.dom.json", "../../tsconfig.json"], "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "types": ["vite/client"], + "paths": { + "@/*": ["./src/*"] + }, /* Linting */ "strict": true, @@ -12,5 +15,6 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "../../packages/sdk/tsconfig.json" }] } diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 1087934..0ab1bf5 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,4 +1,5 @@ { + "extends": "../../tsconfig.json", "files": [], "references": [ { "path": "./tsconfig.app.json" }, diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index bbcf80c..f1d2885 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -1,7 +1,15 @@ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' +import tailwindcss from '@tailwindcss/vite' +import { fileURLToPath, URL } from 'node:url' // https://vite.dev/config/ export default defineConfig({ - plugins: [vue()], + plugins: [vue(), tailwindcss()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + conditions: ['uts-source', 'module', 'browser', 'development|production'], + }, }) diff --git a/crates/calendar/Cargo.toml b/crates/calendar/Cargo.toml index fdac50f..9e74106 100644 --- a/crates/calendar/Cargo.toml +++ b/crates/calendar/Cargo.toml @@ -28,6 +28,7 @@ itoa = { workspace = true } rocksdb = { workspace = true } sha3 = { workspace = true } tokio = { workspace = true, features = ["full"] } +tower-http = { workspace = true, features = ["cors"] } tracing = { workspace = true } tracing-subscriber = { workspace = true } uts-bmt = { workspace = true } diff --git a/crates/calendar/src/main.rs b/crates/calendar/src/main.rs index 73390ec..21182a2 100644 --- a/crates/calendar/src/main.rs +++ b/crates/calendar/src/main.rs @@ -6,12 +6,14 @@ use alloy_signer_local::{LocalSigner, MnemonicBuilder}; use axum::{ Router, extract::DefaultBodyLimit, + http::Method, routing::{get, post}, }; use digest::{OutputSizeUser, typenum::Unsigned}; use rocksdb::DB; use sha3::Keccak256; use std::{env, sync::Arc}; +use tower_http::{cors, cors::CorsLayer}; use tracing::info; use uts_calendar::{AppState, routes, shutdown_signal, time}; use uts_contracts::uts::UniversalTimestamps; @@ -78,7 +80,12 @@ async fn main() -> eyre::Result<()> { signer, journal, db, - })); + })) + .layer( + CorsLayer::new() + .allow_methods([Method::GET, Method::POST]) + .allow_origin(cors::Any), + ); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 9235168..0c459f6 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -9,6 +9,7 @@ "exports": { ".": { "types": "./dist/index.d.ts", + "uts-source": "./src/index.ts", "import": "./dist/index.js", "default": "./dist/index.js" } diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 27cd9b4..c909047 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -10,6 +10,13 @@ export type { EthereumUTSAttestationExtraMetadata, } from './types.ts' +export { + DIGEST_OPS, + UpgradeStatus, + AttestationStatusKind, + VerifyStatus, +} from './types.ts' + export { default as Encoder } from './codec/encode.ts' export { default as Decoder } from './codec/decode.ts' @@ -21,6 +28,14 @@ export * from './bmt.ts' export { default as BitcoinRPC } from './rpc/btc.ts' +export { + default as SDK, + DEFAULT_CALENDARS, + UTS_ABI, + WELL_KNOWN_CHAINS, +} from './sdk.ts' +export type { SDKOptions, StampEvent, StampEventCallback } from './sdk.ts' + export const hexlify = (obj: any): any => { if (obj instanceof URL) { return obj diff --git a/packages/sdk/src/sdk.ts b/packages/sdk/src/sdk.ts index 679e237..655333a 100644 --- a/packages/sdk/src/sdk.ts +++ b/packages/sdk/src/sdk.ts @@ -1,5 +1,7 @@ import { type AbstractProvider, + type Eip1193Provider, + BrowserProvider, getBytes, hexlify, id, @@ -33,16 +35,47 @@ import { ripemd160, sha1 } from '@noble/hashes/legacy.js' import BitcoinRPC from './rpc/btc.ts' import { FallbackProvider } from 'ethers' +export type StampEvent = + | { phase: 'generating-nonce' } + | { phase: 'building-merkle-tree' } + | { phase: 'broadcasting'; totalCalendars: number } + | { + phase: 'calendar-response' + calendarUrl: string + success: boolean + responsesReceived: number + totalCalendars: number + } + | { phase: 'building-proof' } + | { phase: 'complete' } + +export type StampEventCallback = (event: StampEvent) => void + export interface SDKOptions { calendars?: URL[] btcRPC?: BitcoinRPC ethRPCs?: Record + web3Provider?: Eip1193Provider | null timeout?: number quorum?: number nonceSize?: number hashAlgorithm?: SecureDigestOp } +/** + * Well-known EVM chain IDs to hex for wallet_switchEthereumChain. + */ +export const WELL_KNOWN_CHAINS: Record< + number, + { chainId: string; chainName: string } +> = { + 1: { chainId: '0x1', chainName: 'Ethereum Mainnet' }, + 17000: { chainId: '0x4268', chainName: 'Holesky' }, + 11155111: { chainId: '0xaa36a7', chainName: 'Sepolia' }, + 534352: { chainId: '0x82750', chainName: 'Scroll' }, + 534351: { chainId: '0x8274f', chainName: 'Scroll Sepolia' }, +} + export const DEFAULT_CALENDARS = [ new URL('https://a.pool.opentimestamps.org/'), new URL('https://b.pool.opentimestamps.org/'), @@ -58,8 +91,9 @@ export const UTS_ABI = [ export default class SDK { readonly calendars: URL[] - readonly btcRPC: BitcoinRPC - readonly ethRPCs: Record + btcRPC: BitcoinRPC + ethRPCs: Record + web3Provider: Eip1193Provider | null = null /** * Maximum time to wait for calendar responses in milliseconds. @@ -98,23 +132,10 @@ export default class SDK { calendars = DEFAULT_CALENDARS, btcRPC = new BitcoinRPC(), ethRPCs = { - 1: new FallbackProvider([ - new JsonRpcProvider('https://eth.drpc.org'), - new JsonRpcProvider('https://eth.llamarpc.com'), - new JsonRpcProvider('https://eth.llamarpc.com'), - ]), - 17000: new FallbackProvider([ - new JsonRpcProvider('https://holesky.drpc.org'), - new JsonRpcProvider('https://1rpc.io/holesky'), - ]), - 11155111: new FallbackProvider([ - new JsonRpcProvider('https://sepolia.drpc.org'), - new JsonRpcProvider('https://0xrpc.io/sep'), - new JsonRpcProvider('https://rpc.sepolia.org'), - ]), - 54352: new JsonRpcProvider('https://rpc.scroll.io'), - 54351: new JsonRpcProvider('https://sepolia-rpc.scroll.io'), + 534352: new JsonRpcProvider('https://rpc.scroll.io'), + 534351: new JsonRpcProvider('https://sepolia-rpc.scroll.io'), }, + web3Provider = null, timeout = 10000, nonceSize = 32, hashAlgorithm = 'KECCAK256', @@ -124,6 +145,7 @@ export default class SDK { this.calendars = calendars this.btcRPC = btcRPC this.ethRPCs = ethRPCs + this.web3Provider = web3Provider this.timeout = timeout this.nonceSize = nonceSize @@ -165,15 +187,56 @@ export default class SDK { return null } + /** + * Try to get a provider for the given chain from the web3 wallet. + * If the wallet is on a different chain, attempts to switch to the target chain if it's well-known. + * Returns null if no web3Provider or if switching fails. + */ + async getWeb3ProviderForChain( + chainId: number, + ): Promise { + if (!this.web3Provider) return null + + try { + const browser = new BrowserProvider(this.web3Provider) + const network = await browser.getNetwork() + if (Number(network.chainId) === chainId) { + return browser + } + + // Try switching to the target chain if it's well-known + const knownChain = WELL_KNOWN_CHAINS[chainId] + if (knownChain) { + try { + await this.web3Provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: knownChain.chainId }], + }) + return new BrowserProvider(this.web3Provider) + } catch { + // Switch failed, fall through + } + } + } catch { + // web3Provider not usable + } + return null + } + /** * Stamp the provided digests by submitting them to the configured calendars. * * @param digests The digests to be stamped, each with its associated header information. Input digests can use different hash algorithms, but the internal Merkle tree will be constructed using the SDK's configured hash algorithm (default KECCAK256). */ - async stamp(digests: DigestHeader[]): Promise { + async stamp( + digests: DigestHeader[], + onEvent?: StampEventCallback, + ): Promise { const nonces: Uint8Array[] = [] const nonceDigests: Uint8Array[] = [] + onEvent?.({ phase: 'generating-nonce' }) + for (const digest of digests) { const hasher = this.hasher.create() hasher.update(getBytes(digest.digest)) @@ -184,6 +247,8 @@ export default class SDK { nonceDigests.push(nonceDigest) } + onEvent?.({ phase: 'building-merkle-tree' }) + const internalMerkleTree = UnorderedMerkleTree.new( nonceDigests, this.hasher, @@ -191,8 +256,34 @@ export default class SDK { const root = internalMerkleTree.root() console.debug(`Internal Merkle root: ${hexlify(root)}`) + onEvent?.({ phase: 'broadcasting', totalCalendars: this.calendars.length }) + + let responsesReceived = 0 const calendarResponses = await Promise.allSettled( - this.calendars.map((calendar) => this.requestAttest(calendar, root)), + this.calendars.map(async (calendar) => { + try { + const result = await this.requestAttest(calendar, root) + responsesReceived++ + onEvent?.({ + phase: 'calendar-response', + calendarUrl: calendar.toString(), + success: true, + responsesReceived, + totalCalendars: this.calendars.length, + }) + return result + } catch (error) { + responsesReceived++ + onEvent?.({ + phase: 'calendar-response', + calendarUrl: calendar.toString(), + success: false, + responsesReceived, + totalCalendars: this.calendars.length, + }) + throw error + } + }), ) const successfulResponses = calendarResponses.filter( @@ -214,7 +305,9 @@ export default class SDK { } as ForkStep, ] - return digests.map((digest, i) => { + onEvent?.({ phase: 'building-proof' }) + + const results = digests.map((digest, i) => { const timestamp: Timestamp = [ { op: 'APPEND', data: nonces[i] }, { op: this.hashAlg }, @@ -256,6 +349,10 @@ export default class SDK { timestamp, } }) + + onEvent?.({ phase: 'complete' }) + + return results } /** @@ -299,12 +396,17 @@ export default class SDK { /** * Perform in-place upgrade of the provided detached timestamp by replacing any pending attestations with their upgraded timestamp steps, if they have become available. * @param detached The detached timestamp to be upgraded. + * @param keepPending Whether to keep the original pending attestation alongside the upgraded one. Default is false (purge pending on success). * @returns The result of the upgrade operation, including the original and upgraded timestamps if applicable. */ - async upgrade(detached: DetachedTimestamp): Promise { + async upgrade( + detached: DetachedTimestamp, + keepPending: boolean = false, + ): Promise { return this.upgradeTimestamp( getBytes(detached.header.digest), detached.timestamp, + keepPending, ) } @@ -313,11 +415,13 @@ export default class SDK { * This function will recursively traverse the timestamp steps and perform in-place upgrades of any pending attestations encountered. * @param input The original digest input associated with the timestamp, which is needed to verify and upgrade the pending attestations. * @param timestamp The timestamp steps to be upgraded, which may contain pending attestations that need to be replaced with their upgraded timestamp steps if they have become available. + * @param keepPending Whether to keep the original pending attestation alongside the upgraded one. Default is false (purge pending on success). * @returns The result of the upgrade operation, including the original and upgraded timestamps if applicable. */ async upgradeTimestamp( input: Uint8Array, timestamp: Timestamp, + keepPending: boolean = false, ): Promise { let current = input @@ -341,7 +445,9 @@ export default class SDK { // upgrade sub stamps const results = ( await Promise.all( - step.steps.map((branch) => this.upgradeTimestamp(input, branch)), + step.steps.map((branch) => + this.upgradeTimestamp(input, branch, keepPending), + ), ) ).flat() result.push(...results) @@ -362,10 +468,15 @@ export default class SDK { }) continue } - // preserve the original attestation in the upgraded timestamp for transparency - timestamp[i] = { - op: 'FORK', - steps: [[step], upgraded], + if (keepPending) { + // preserve the original attestation in the upgraded timestamp for transparency + timestamp[i] = { + op: 'FORK', + steps: [[step], upgraded], + } + } else { + // replace the pending attestation with the upgraded one + timestamp.splice(i, 1, ...upgraded) } result.push({ status: UpgradeStatus.Upgraded, @@ -565,17 +676,25 @@ export default class SDK { input: Uint8Array, attestation: EthereumUTSAttestation, ): Promise { - if (!Object.hasOwn(this.ethRPCs, attestation.chain)) { - return { - attestation, - status: AttestationStatusKind.UNKNOWN, - error: new VerifyError( - ErrorCode.UNSUPPORTED_ATTESTATION, - `No RPC provider configured for Ethereum chain ${attestation.chain}`, - ), + // Try web3Provider first (works in browser without CORS issues) + let provider: AbstractProvider | null = await this.getWeb3ProviderForChain( + attestation.chain, + ) + + // Fallback to ethRPCs + if (!provider) { + if (!Object.hasOwn(this.ethRPCs, attestation.chain)) { + return { + attestation, + status: AttestationStatusKind.UNKNOWN, + error: new VerifyError( + ErrorCode.UNSUPPORTED_ATTESTATION, + `No RPC provider configured for Ethereum chain ${attestation.chain}`, + ), + } } + provider = this.ethRPCs[attestation.chain]! } - const provider = this.ethRPCs[attestation.chain]! try { const logs = await provider.getLogs({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c86bffd..1641d6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,19 +10,19 @@ importers: devDependencies: eslint: specifier: ^9.38.0 - version: 9.39.3 + version: 9.39.3(jiti@2.6.1) eslint-import-resolver-typescript: specifier: ^4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3))(eslint@9.39.3) + version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)) eslint-plugin-import-x: specifier: ^4.16.1 - version: 4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3) + version: 4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)) eslint-plugin-unicorn: specifier: ^63.0.0 - version: 63.0.0(eslint@9.39.3) + version: 63.0.0(eslint@9.39.3(jiti@2.6.1)) eslint-plugin-unused-imports: specifier: ^4.4.1 - version: 4.4.1(@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3) + version: 4.4.1(@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)) pnpm: specifier: ^10.26.2 version: 10.26.2 @@ -31,32 +31,62 @@ importers: version: 3.8.1 typescript-eslint: specifier: ^8.56.1 - version: 8.56.1(eslint@9.39.3)(typescript@5.9.3) + version: 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) apps/web: dependencies: + '@noble/hashes': + specifier: ^2.0.1 + version: 2.0.1 '@uts/sdk': specifier: workspace:* version: link:../../packages/sdk + '@vueuse/core': + specifier: ^14.2.1 + version: 14.2.1(vue@3.5.26(typescript@5.9.3)) + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + ethers: + specifier: ^6.16.0 + version: 6.16.0 + jszip: + specifier: ^3.10.1 + version: 3.10.1 + lucide-vue-next: + specifier: ^0.575.0 + version: 0.575.0(vue@3.5.26(typescript@5.9.3)) + pinia: + specifier: ^3.0.4 + version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)) vue: specifier: ^3.5.24 version: 3.5.26(typescript@5.9.3) devDependencies: + '@tailwindcss/vite': + specifier: ^4.2.1 + version: 4.2.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)) + '@types/jszip': + specifier: ^3.4.1 + version: 3.4.1 '@types/node': specifier: ^24.10.1 version: 24.10.4 '@vitejs/plugin-vue': specifier: ^6.0.1 - version: 6.0.3(vite@7.3.0(@types/node@24.10.4))(vue@3.5.26(typescript@5.9.3)) + version: 6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vue@3.5.26(typescript@5.9.3)) '@vue/tsconfig': specifier: ^0.8.1 version: 0.8.1(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)) + tailwindcss: + specifier: ^4.2.1 + version: 4.2.1 typescript: specifier: ~5.9.3 version: 5.9.3 vite: specifier: ^7.2.4 - version: 7.3.0(@types/node@24.10.4) + version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) vue-tsc: specifier: ^3.1.4 version: 3.2.1(typescript@5.9.3) @@ -87,7 +117,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18) + version: 4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.31.1) packages: @@ -330,9 +360,22 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -646,6 +689,100 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tailwindcss/node@4.2.1': + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + + '@tailwindcss/oxide-android-arm64@4.2.1': + resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.1': + resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.1': + resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.1': + resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -661,6 +798,10 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jszip@3.4.1': + resolution: {integrity: sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A==} + deprecated: This is a stub types definition. jszip provides its own type definitions, so you do not need this installed. + '@types/node@22.7.5': resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} @@ -670,6 +811,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@typescript-eslint/eslint-plugin@8.56.1': resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -900,6 +1044,15 @@ packages: '@vue/compiler-ssr@3.5.26': resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==} + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + '@vue/language-core@3.2.1': resolution: {integrity: sha512-g6oSenpnGMtpxHGAwKuu7HJJkNZpemK/zg3vZzZbJ6cnnXq1ssxuNrXSsAHYM3NvH8p4IkTw+NLmuxyeYz4r8A==} @@ -931,6 +1084,19 @@ packages: vue: optional: true + '@vueuse/core@14.2.1': + resolution: {integrity: sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@14.2.1': + resolution: {integrity: sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==} + + '@vueuse/shared@14.2.1': + resolution: {integrity: sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==} + peerDependencies: + vue: ^3.5.0 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -973,6 +1139,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1029,9 +1198,16 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + core-js-compat@3.48.0: resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1039,6 +1215,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1055,9 +1234,17 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + electron-to-chromium@1.5.302: resolution: {integrity: sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==} + enhanced-resolve@5.19.0: + resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} + engines: {node: '>=10.13.0'} + entities@7.0.0: resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} engines: {node: '>=0.12'} @@ -1261,6 +1448,9 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1269,6 +1459,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1277,6 +1470,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1289,6 +1485,9 @@ packages: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + is-builtin-module@5.0.0: resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} engines: {node: '>=18.20'} @@ -1311,9 +1510,20 @@ packages: is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -1332,6 +1542,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1339,6 +1552,83 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1350,6 +1640,11 @@ packages: resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} + lucide-vue-next@0.575.0: + resolution: {integrity: sha512-UHzA3cYMCgBLyGay5R9IQaidwV0NLocx7cIBnFt8vJ9Xhl6IM/oKD0fUhoCUuouFta15SX1rLXVoko9s3TzWMA==} + peerDependencies: + vue: '>=3.0.1' + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1364,6 +1659,9 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -1408,6 +1706,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1433,6 +1734,9 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1440,6 +1744,15 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + peerDependencies: + typescript: '>=4.5.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + pixelmatch@7.1.0: resolution: {integrity: sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==} hasBin: true @@ -1480,10 +1793,16 @@ packages: engines: {node: '>=14'} hasBin: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + regexp-tree@0.1.27: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} hasBin: true @@ -1504,6 +1823,9 @@ packages: engines: {node: '>= 0.4'} hasBin: true + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@6.1.3: resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} engines: {node: 20 || >=22} @@ -1519,11 +1841,17 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} hasBin: true + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1543,6 +1871,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -1553,6 +1885,9 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + strip-indent@4.1.1: resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} engines: {node: '>=12'} @@ -1561,6 +1896,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1569,6 +1908,13 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tailwindcss@4.2.1: + resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1631,6 +1977,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vite@7.3.0: resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1875,9 +2224,9 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.3)': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.3(jiti@2.6.1))': dependencies: - eslint: 9.39.3 + eslint: 9.39.3(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -1932,8 +2281,25 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.8.1 @@ -2124,6 +2490,74 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tailwindcss/node@4.2.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.19.0 + jiti: 2.6.1 + lightningcss: 1.31.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.1 + + '@tailwindcss/oxide-android-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide@4.2.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-x64': 4.2.1 + '@tailwindcss/oxide-freebsd-x64': 4.2.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-x64-musl': 4.2.1 + '@tailwindcss/oxide-wasm32-wasi': 4.2.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 + + '@tailwindcss/vite@4.2.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))': + dependencies: + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + tailwindcss: 4.2.1 + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.7.0 @@ -2140,6 +2574,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/jszip@3.4.1': + dependencies: + jszip: 3.10.1 + '@types/node@22.7.5': dependencies: undici-types: 6.19.8 @@ -2150,15 +2588,17 @@ snapshots: '@types/resolve@1.20.2': {} - '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3)': + '@types/web-bluetooth@0.0.21': {} + + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.56.1(eslint@9.39.3)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.3)(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.1 - eslint: 9.39.3 + eslint: 9.39.3(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -2166,14 +2606,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.56.1(eslint@9.39.3)(typescript@5.9.3)': + '@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.56.1 '@typescript-eslint/types': 8.56.1 '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.1 debug: 4.4.3 - eslint: 9.39.3 + eslint: 9.39.3(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -2196,13 +2636,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.56.1(eslint@9.39.3)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.56.1 '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3)(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.3 + eslint: 9.39.3(jiti@2.6.1) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -2225,13 +2665,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3)': + '@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.56.1 '@typescript-eslint/types': 8.56.1 '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - eslint: 9.39.3 + eslint: 9.39.3(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -2300,19 +2740,19 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-vue@6.0.3(vite@7.3.0(@types/node@24.10.4))(vue@3.5.26(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vue@3.5.26(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.53 - vite: 7.3.0(@types/node@24.10.4) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) vue: 3.5.26(typescript@5.9.3) - '@vitest/browser-playwright@4.0.18(playwright@1.58.2)(vite@7.3.0(@types/node@24.10.4))(vitest@4.0.18)': + '@vitest/browser-playwright@4.0.18(playwright@1.58.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vitest@4.0.18)': dependencies: - '@vitest/browser': 4.0.18(vite@7.3.0(@types/node@24.10.4))(vitest@4.0.18) - '@vitest/mocker': 4.0.18(vite@7.3.0(@types/node@24.10.4)) + '@vitest/browser': 4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vitest@4.0.18) + '@vitest/mocker': 4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)) playwright: 1.58.2 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18) + vitest: 4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.31.1) transitivePeerDependencies: - bufferutil - msw @@ -2320,16 +2760,16 @@ snapshots: - vite optional: true - '@vitest/browser@4.0.18(vite@7.3.0(@types/node@24.10.4))(vitest@4.0.18)': + '@vitest/browser@4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vitest@4.0.18)': dependencies: - '@vitest/mocker': 4.0.18(vite@7.3.0(@types/node@24.10.4)) + '@vitest/mocker': 4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)) '@vitest/utils': 4.0.18 magic-string: 0.30.21 pixelmatch: 7.1.0 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18) + vitest: 4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.31.1) ws: 8.19.0 transitivePeerDependencies: - bufferutil @@ -2347,13 +2787,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.0(@types/node@24.10.4))': + '@vitest/mocker@4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.0(@types/node@24.10.4) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) '@vitest/pretty-format@4.0.18': dependencies: @@ -2419,6 +2859,24 @@ snapshots: '@vue/compiler-dom': 3.5.26 '@vue/shared': 3.5.26 + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-kit@7.7.9': + dependencies: + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@7.7.9': + dependencies: + rfdc: 1.4.1 + '@vue/language-core@3.2.1': dependencies: '@volar/language-core': 2.4.27 @@ -2458,6 +2916,19 @@ snapshots: typescript: 5.9.3 vue: 3.5.26(typescript@5.9.3) + '@vueuse/core@14.2.1(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.2.1 + '@vueuse/shared': 14.2.1(vue@3.5.26(typescript@5.9.3)) + vue: 3.5.26(typescript@5.9.3) + + '@vueuse/metadata@14.2.1': {} + + '@vueuse/shared@14.2.1(vue@3.5.26(typescript@5.9.3))': + dependencies: + vue: 3.5.26(typescript@5.9.3) + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -2489,6 +2960,8 @@ snapshots: baseline-browser-mapping@2.10.0: {} + birpc@2.9.0: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -2537,10 +3010,16 @@ snapshots: concat-map@0.0.1: {} + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + core-js-compat@3.48.0: dependencies: browserslist: 4.28.1 + core-util-is@1.0.3: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2549,6 +3028,8 @@ snapshots: csstype@3.2.3: {} + date-fns@4.1.0: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -2557,8 +3038,15 @@ snapshots: deepmerge@4.3.1: {} + detect-libc@2.1.2: {} + electron-to-chromium@1.5.302: {} + enhanced-resolve@5.19.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + entities@7.0.0: {} es-module-lexer@1.7.0: {} @@ -2605,10 +3093,10 @@ snapshots: optionalDependencies: unrs-resolver: 1.11.1 - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3))(eslint@9.39.3): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)): dependencies: debug: 4.4.3 - eslint: 9.39.3 + eslint: 9.39.3(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) get-tsconfig: 4.13.6 is-bun-module: 2.0.0 @@ -2616,16 +3104,16 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3) + eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3): + eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)): dependencies: '@typescript-eslint/types': 8.56.1 comment-parser: 1.4.5 debug: 4.4.3 - eslint: 9.39.3 + eslint: 9.39.3(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 minimatch: 10.2.2 @@ -2633,19 +3121,19 @@ snapshots: stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3)(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - supports-color - eslint-plugin-unicorn@63.0.0(eslint@9.39.3): + eslint-plugin-unicorn@63.0.0(eslint@9.39.3(jiti@2.6.1)): dependencies: '@babel/helper-validator-identifier': 7.28.5 - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1)) change-case: 5.4.4 ci-info: 4.4.0 clean-regexp: 1.0.0 core-js-compat: 3.48.0 - eslint: 9.39.3 + eslint: 9.39.3(jiti@2.6.1) find-up-simple: 1.0.1 globals: 16.5.0 indent-string: 5.0.0 @@ -2657,11 +3145,11 @@ snapshots: semver: 7.7.4 strip-indent: 4.1.1 - eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3): + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)): dependencies: - eslint: 9.39.3 + eslint: 9.39.3(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) eslint-scope@8.4.0: dependencies: @@ -2674,9 +3162,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.3: + eslint@9.39.3(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.1 '@eslint/config-helpers': 0.4.2 @@ -2710,6 +3198,8 @@ snapshots: minimatch: 3.1.4 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 transitivePeerDependencies: - supports-color @@ -2806,16 +3296,22 @@ snapshots: globals@16.5.0: {} + graceful-fs@4.2.11: {} + has-flag@4.0.0: {} hasown@2.0.2: dependencies: function-bind: 1.1.2 + hookable@5.5.3: {} + ignore@5.3.2: {} ignore@7.0.5: {} + immediate@3.0.6: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -2825,6 +3321,8 @@ snapshots: indent-string@5.0.0: {} + inherits@2.0.4: {} + is-builtin-module@5.0.0: dependencies: builtin-modules: 5.0.0 @@ -2845,8 +3343,14 @@ snapshots: is-module@1.0.0: {} + is-what@5.5.0: {} + + isarray@1.0.0: {} + isexe@2.0.0: {} + jiti@2.6.1: {} + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -2859,6 +3363,13 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -2868,6 +3379,59 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + lightningcss-android-arm64@1.31.1: + optional: true + + lightningcss-darwin-arm64@1.31.1: + optional: true + + lightningcss-darwin-x64@1.31.1: + optional: true + + lightningcss-freebsd-x64@1.31.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.31.1: + optional: true + + lightningcss-linux-arm64-gnu@1.31.1: + optional: true + + lightningcss-linux-arm64-musl@1.31.1: + optional: true + + lightningcss-linux-x64-gnu@1.31.1: + optional: true + + lightningcss-linux-x64-musl@1.31.1: + optional: true + + lightningcss-win32-arm64-msvc@1.31.1: + optional: true + + lightningcss-win32-x64-msvc@1.31.1: + optional: true + + lightningcss@1.31.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -2876,6 +3440,10 @@ snapshots: lru-cache@11.2.6: {} + lucide-vue-next@0.575.0(vue@3.5.26(typescript@5.9.3)): + dependencies: + vue: 3.5.26(typescript@5.9.3) + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2890,6 +3458,8 @@ snapshots: minipass@7.1.3: {} + mitt@3.0.1: {} + mrmime@2.0.1: optional: true @@ -2926,6 +3496,8 @@ snapshots: package-json-from-dist@1.0.1: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -2945,10 +3517,19 @@ snapshots: pathe@2.0.3: {} + perfect-debounce@1.0.0: {} + picocolors@1.1.1: {} picomatch@4.0.3: {} + pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 7.7.9 + vue: 3.5.26(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + pixelmatch@7.1.0: dependencies: pngjs: 7.0.0 @@ -2981,8 +3562,20 @@ snapshots: prettier@3.8.1: {} + process-nextick-args@2.0.1: {} + punycode@2.3.1: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + regexp-tree@0.1.27: {} regjsparser@0.13.0: @@ -2999,6 +3592,8 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + rfdc@1.4.1: {} + rimraf@6.1.3: dependencies: glob: 13.0.6 @@ -3063,8 +3658,12 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + safe-buffer@5.1.2: {} + semver@7.7.4: {} + setimmediate@1.0.5: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -3082,22 +3681,36 @@ snapshots: source-map-js@1.2.1: {} + speakingurl@14.0.1: {} + stable-hash-x@0.2.0: {} stackback@0.0.2: {} std-env@3.10.0: {} + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + strip-indent@4.1.1: {} strip-json-comments@3.1.1: {} + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 supports-preserve-symlinks-flag@1.0.0: {} + tailwindcss@4.2.1: {} + + tapable@2.3.0: {} + tinybench@2.9.0: {} tinyexec@1.0.2: {} @@ -3122,13 +3735,13 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.56.1(eslint@9.39.3)(typescript@5.9.3): + typescript-eslint@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3) - '@typescript-eslint/parser': 8.56.1(eslint@9.39.3)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3)(typescript@5.9.3) - eslint: 9.39.3 + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.3(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3173,7 +3786,9 @@ snapshots: dependencies: punycode: 2.3.1 - vite@7.3.0(@types/node@24.10.4): + util-deprecate@1.0.2: {} + + vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -3184,11 +3799,13 @@ snapshots: optionalDependencies: '@types/node': 24.10.4 fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.31.1 - vitest@4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18): + vitest@4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.31.1): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.0(@types/node@24.10.4)) + '@vitest/mocker': 4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -3205,11 +3822,11 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.0(@types/node@24.10.4) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.10.4 - '@vitest/browser-playwright': 4.0.18(playwright@1.58.2)(vite@7.3.0(@types/node@24.10.4))(vitest@4.0.18) + '@vitest/browser-playwright': 4.0.18(playwright@1.58.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vitest@4.0.18) transitivePeerDependencies: - jiti - less diff --git a/tsconfig.json b/tsconfig.json index 9b516e0..9e7bf2a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -48,7 +48,16 @@ "incremental": true, // Completeness - "skipLibCheck": true // skip all type checks for .d.ts files + "skipLibCheck": true, // skip all type checks for .d.ts files + + "paths": { + "@uts/sdk": ["./packages/sdk/src/index.ts"] + }, + "customConditions": ["uts-source"] }, - "references": [{ "path": "./packages/sdk" }, { "path": "./packages/sdk/test/" }, { "path": "./apps/web" }] + "references": [ + { "path": "./packages/sdk" }, + { "path": "./packages/sdk/test/" }, + { "path": "./apps/web" } + ] }