diff --git a/README.md b/README.md index dbedd5c..9de0c3d 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ not published yet (MVP.md §0.1). Local proof uses Anvil + `test/fixtures/tiny-m | `services/seed` | `@enspack/seed` | seed node API + qBittorrent + Kubo compose | | `services/registrar` | `@enspack/registrar` | HF-proof → `enspack.eth` publisher subnames | | `services/indexer` | `@enspack/indexer` | Ponder indexer + JSON API | +| `apps/site` | `@enspack/site` | catalog site, read-only view of the indexer API | | `bootstrap/` | `@enspack/bootstrap` | `models.yaml` mirror runner (`enspack-bootstrap plan|run`) | | `infra/` | `@enspack/infra` | one-box Sepolia deploy (Caddy + compose + scripts) | | `test/fixtures` | — | tiny-model folder + torrent for swarm tests | diff --git a/apps/site/CHANGELOG.md b/apps/site/CHANGELOG.md new file mode 100644 index 0000000..8fd7802 --- /dev/null +++ b/apps/site/CHANGELOG.md @@ -0,0 +1,5 @@ +# @enspack/site + +## 0.1.0 (Unreleased) + +- Initial catalog site (WP-19): read-only view of the indexer JSON API. diff --git a/apps/site/README.md b/apps/site/README.md new file mode 100644 index 0000000..c51d395 --- /dev/null +++ b/apps/site/README.md @@ -0,0 +1,50 @@ +# @enspack/site + +Read-only catalog for enspack names. Someone who has never heard of enspack +should see, on one page, what a name is, what it points at, and that the bytes +verify. The site is a window onto the indexer JSON API (`MVP.md` §4.3). No +wallet, no writes, no keys, no RPC URL in the bundle. + +`@enspack/core` is a type-only (dev) dependency. Runtime core is never imported +into the browser bundle (it would pull viem and IPFS). + +## Environment + +| Variable | Default | Meaning | +| --- | --- | --- | +| `VITE_INDEX_URL` | `https://index.enspack.dev` | Indexer base URL | +| `VITE_REGISTRAR_URL` | `https://registrar.enspack.dev` | Registrar base URL (attestations, claim-page link) | +| `VITE_CHAIN` | `sepolia` | `sepolia` \| `mainnet`; drives the testnet banner and explorer / ENS-app links | +| `VITE_DATA_SOURCE` | `http` | `http` \| `demo`; `demo` serves the recorded fixtures in-app | + +## Develop + +From the repo root: + +```sh +pnpm --filter @enspack/site dev +``` + +Without a live indexer, serve the recorded fixtures: + +```sh +VITE_DATA_SOURCE=demo pnpm --filter @enspack/site dev +``` + +```sh +pnpm --filter @enspack/site build +pnpm --filter @enspack/site preview +``` + +## Deploy (Cloudflare Pages) + +Hosting target is `enspack.dev`. DNS and the Pages project are manual. + +| | | +| --- | --- | +| Build command | `pnpm --filter @enspack/site build` | +| Output directory | `apps/site/dist` | +| SPA fallback | `public/_redirects` (`/* /index.html 200`) | + +The built bundle contains no keys and no RPC URLs. Chain state arrives only +through the indexer (or demo fixtures). diff --git a/apps/site/index.html b/apps/site/index.html new file mode 100644 index 0000000..5e1b9b4 --- /dev/null +++ b/apps/site/index.html @@ -0,0 +1,17 @@ + + + + + + + + enspack + + +
+ + + diff --git a/apps/site/package.json b/apps/site/package.json new file mode 100644 index 0000000..d265ad8 --- /dev/null +++ b/apps/site/package.json @@ -0,0 +1,35 @@ +{ + "name": "@enspack/site", + "version": "0.1.0", + "private": true, + "description": "enspack catalog site: read-only window onto the indexer JSON API (MVP.md §4.3)", + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit -p tsconfig.json", + "test": "vitest run", + "check": "pnpm typecheck && pnpm test" + }, + "dependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router": "^7.6.0" + }, + "devDependencies": { + "@enspack/core": "workspace:*", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^4.4.0", + "jsdom": "^26.1.0", + "typescript": "^5.7.0", + "vite": "^6.3.0", + "vitest": "^3.0.0" + } +} diff --git a/apps/site/public/_redirects b/apps/site/public/_redirects new file mode 100644 index 0000000..bbb3e7a --- /dev/null +++ b/apps/site/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 diff --git a/apps/site/src/app.tsx b/apps/site/src/app.tsx new file mode 100644 index 0000000..3377c46 --- /dev/null +++ b/apps/site/src/app.tsx @@ -0,0 +1,23 @@ +import { Route, Routes } from "react-router"; +import { Layout } from "./components/layout.js"; +import { GetStartedPage } from "./pages/get-started.js"; +import { HomePage } from "./pages/home.js"; +import { ModelPage } from "./pages/model.js"; +import { NotFoundPage } from "./pages/not-found.js"; +import { PublisherPage } from "./pages/publisher.js"; +import { ViolationsPage } from "./pages/violations.js"; + +export function App() { + return ( + + + } /> + } /> + } /> + } /> + } /> + } /> + + + ); +} diff --git a/apps/site/src/components/badge.tsx b/apps/site/src/components/badge.tsx new file mode 100644 index 0000000..82f813e --- /dev/null +++ b/apps/site/src/components/badge.tsx @@ -0,0 +1,46 @@ +import type { ReactNode } from "react"; + +export type BadgeTone = "neutral" | "accent" | "ok" | "warn" | "danger"; + +export function Badge({ + children, + tone = "neutral", + title, +}: { + children: ReactNode; + tone?: BadgeTone; + title?: string; +}) { + return ( + + {children} + + ); +} + +const ROLE_TONE: Record = { + weight: "accent", + config: "neutral", + tokenizer: "neutral", + index: "neutral", + doc: "neutral", + license: "warn", + code: "neutral", + other: "neutral", +}; + +export function RoleBadge({ role }: { role: string | undefined }) { + const r = role ?? "other"; + return {r}; +} + +export function LicenseBadge({ license, href }: { license: string; href?: string }) { + const badge = {license}; + return href ? ( + + {badge} + + ) : ( + badge + ); +} diff --git a/apps/site/src/components/command-block.tsx b/apps/site/src/components/command-block.tsx new file mode 100644 index 0000000..f6a2e3d --- /dev/null +++ b/apps/site/src/components/command-block.tsx @@ -0,0 +1,13 @@ +import { CopyButton } from "./copy-button.js"; + +/** Copyable CLI snippet; chrome comes from global `.cmd`. */ +export function CommandBlock({ command }: { command: string }) { + return ( +
+
+        {command}
+      
+ +
+ ); +} diff --git a/apps/site/src/components/copy-button.tsx b/apps/site/src/components/copy-button.tsx new file mode 100644 index 0000000..fde04eb --- /dev/null +++ b/apps/site/src/components/copy-button.tsx @@ -0,0 +1,55 @@ +import { useEffect, useState } from "react"; + +/** The async clipboard API is denied in some embedded/permissioned contexts; fall back to execCommand. */ +async function copyText(value: string): Promise { + try { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(value); + return true; + } + } catch { + // fall through to the legacy path + } + const ta = document.createElement("textarea"); + ta.value = value; + ta.setAttribute("readonly", ""); + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + let ok = false; + try { + ok = document.execCommand("copy"); + } catch { + ok = false; + } + document.body.removeChild(ta); + return ok; +} + +export function CopyButton({ value, label = "Copy" }: { value: string; label?: string }) { + const [copied, setCopied] = useState(false); + + useEffect(() => { + if (!copied) return; + const t = setTimeout(() => setCopied(false), 1500); + return () => clearTimeout(t); + }, [copied]); + + async function onClick() { + if (await copyText(value)) setCopied(true); + } + + return ( + + ); +} diff --git a/apps/site/src/components/external-link.tsx b/apps/site/src/components/external-link.tsx new file mode 100644 index 0000000..83a6996 --- /dev/null +++ b/apps/site/src/components/external-link.tsx @@ -0,0 +1,17 @@ +import type { ReactNode } from "react"; + +export function ExternalLink({ + href, + children, + className, +}: { + href: string; + children: ReactNode; + className?: string; +}) { + return ( + + {children} + + ); +} diff --git a/apps/site/src/components/files-table.tsx b/apps/site/src/components/files-table.tsx new file mode 100644 index 0000000..28e61d4 --- /dev/null +++ b/apps/site/src/components/files-table.tsx @@ -0,0 +1,102 @@ +import { useMemo, useState } from "react"; +import type { ManifestFile } from "../lib/api.js"; +import { formatBytes } from "../lib/format.js"; +import { RoleBadge } from "./badge.js"; +import { Hash } from "./hash.js"; + +type SortKey = "path" | "size"; +type SortDir = "asc" | "desc"; + +function ariaSort( + active: SortKey | null, + key: SortKey, + dir: SortDir, +): "ascending" | "descending" | "none" { + if (active !== key) return "none"; + return dir === "asc" ? "ascending" : "descending"; +} + +function compareFiles(a: ManifestFile, b: ManifestFile, key: SortKey, dir: SortDir): number { + const mul = dir === "asc" ? 1 : -1; + if (key === "size") return (a.size - b.size) * mul; + return a.path.localeCompare(b.path) * mul; +} + +export function FilesTable({ + files, + totalSize, +}: { + files: readonly ManifestFile[]; + totalSize: number; +}) { + const [key, setKey] = useState(null); + const [dir, setDir] = useState("asc"); + + const rows = useMemo(() => { + if (key === null) return [...files]; + return [...files].sort((a, b) => compareFiles(a, b, key, dir)); + }, [files, key, dir]); + + function onSort(next: SortKey) { + if (key === next) { + setDir((d) => (d === "asc" ? "desc" : "asc")); + return; + } + setKey(next); + setDir("asc"); + } + + return ( +
+

Files

+
+ + + + + + + + + + + {rows.map((file) => ( + + + + + + + ))} + + + + + + + +
+ + role + + sha256
+ {file.path} + + + + {formatBytes(file.size)} + + +
+ {files.length} {files.length === 1 ? "file" : "files"} + + {formatBytes(totalSize)} +
+
+
+ ); +} diff --git a/apps/site/src/components/get-panel.tsx b/apps/site/src/components/get-panel.tsx new file mode 100644 index 0000000..2ad4d76 --- /dev/null +++ b/apps/site/src/components/get-panel.tsx @@ -0,0 +1,68 @@ +import type { Manifest } from "../lib/api.js"; +import { CopyButton } from "./copy-button.js"; + +function lockEntryJson(manifest: Manifest, cid: string): string { + const entry: { + resolved: string; + cid: string; + infohash: string; + totalSize: number; + select: string[]; + upstream?: { repo: string; revision: string }; + } = { + resolved: manifest.name, + cid, + infohash: manifest.distribution.infohash, + totalSize: manifest.totalSize, + select: ["*"], + }; + if (manifest.upstream !== undefined) { + entry.upstream = { + repo: manifest.upstream.repo, + revision: manifest.upstream.revision, + }; + } + return JSON.stringify( + { + lockfileVersion: 1, + models: { + [manifest.model]: entry, + }, + }, + null, + 2, + ); +} + +function CopyBlock({ label, text }: { label: string; text: string }) { + return ( +
+
+ {label} + +
+
{text}
+
+ ); +} + +export function GetPanel({ manifest, cid }: { manifest: Manifest; cid: string }) { + const getCmd = `enspack get ${manifest.model}`; + const addCmd = `enspack add ${manifest.model}\nenspack install`; + const lockJson = lockEntryJson(manifest, cid); + + return ( + + ); +} diff --git a/apps/site/src/components/hash.tsx b/apps/site/src/components/hash.tsx new file mode 100644 index 0000000..c3d2734 --- /dev/null +++ b/apps/site/src/components/hash.tsx @@ -0,0 +1,35 @@ +import { shortHex } from "../lib/format.js"; +import { CopyButton } from "./copy-button.js"; + +/** + * A hash, CID, name or magnet in monospace with a copy control. Full value by default; `short` + * shows an abbreviation but keeps the full value in `title` and on the clipboard. + */ +export function Hash({ + value, + short = false, + href, + className, +}: { + value: string; + short?: boolean; + href?: string; + className?: string; +}) { + const text = short ? shortHex(value, 10, 6) : value; + const inner = href ? ( + + {text} + + ) : ( + + {text} + + ); + return ( + + {inner} + + + ); +} diff --git a/apps/site/src/components/hero.tsx b/apps/site/src/components/hero.tsx new file mode 100644 index 0000000..eb512bb --- /dev/null +++ b/apps/site/src/components/hero.tsx @@ -0,0 +1,18 @@ +import { CommandBlock } from "./command-block.js"; + +const QWEN_EXAMPLE = "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth"; + +export function Hero({ firstModel }: { firstModel?: string }) { + const name = firstModel ?? QWEN_EXAMPLE; + const command = `enspack get ${name}`; + return ( +
+

Model weights with a name you can verify.

+

+ An ENS name points at a content-addressed manifest; every file is SHA-256 checked; bytes + come from BitTorrent + HF webseeds, never trusted. +

+ +
+ ); +} diff --git a/apps/site/src/components/how-it-works.tsx b/apps/site/src/components/how-it-works.tsx new file mode 100644 index 0000000..9ff27ce --- /dev/null +++ b/apps/site/src/components/how-it-works.tsx @@ -0,0 +1,41 @@ +const STEPS = [ + { + title: "Name", + artifact: "v1-0-0.qwen--….mirrors.enspack.eth", + trust: "The owner key of this name is the only party that can point it at a manifest.", + }, + { + title: "Manifest", + artifact: "contenthash → bafy…", + trust: "The CID is the hash of the JSON; a gateway cannot change it.", + }, + { + title: "Bytes", + artifact: "magnet:?xt=urn:btih:… + webseeds", + trust: "Peers and HF webseeds serve files; none is a source of truth.", + }, + { + title: "Verify", + artifact: "sha256 per file → .enspack-quarantine/", + trust: "Every file is checked by size then SHA-256; a mismatch is quarantined.", + }, +] as const; + +export function HowItWorks() { + return ( +
+

+ How it works +

+
    + {STEPS.map((step) => ( +
  1. +

    {step.title}

    + {step.artifact} +

    {step.trust}

    +
  2. + ))} +
+
+ ); +} diff --git a/apps/site/src/components/layout.tsx b/apps/site/src/components/layout.tsx new file mode 100644 index 0000000..113f0d5 --- /dev/null +++ b/apps/site/src/components/layout.tsx @@ -0,0 +1,92 @@ +import { type ReactNode, useState } from "react"; +import { Link, NavLink } from "react-router"; +import { config } from "../lib/config.js"; +import { ExternalLink } from "./external-link.js"; + +/** Session-only so a new tab still sees the banner; reload in this tab stays dismissed. */ +const BANNER_STORAGE_KEY = "enspack.banner.dismissed"; + +function bannerDismissed(): boolean { + try { + return sessionStorage.getItem(BANNER_STORAGE_KEY) === "1"; + } catch { + return false; + } +} + +function persistBannerDismissed(): void { + try { + sessionStorage.setItem(BANNER_STORAGE_KEY, "1"); + } catch { + // private mode can throw; still hide for this document + } +} + +function WordmarkMark() { + return ( + + ); +} + +export function Layout({ children }: { children: ReactNode }) { + const [showBanner, setShowBanner] = useState( + () => config.chain !== "mainnet" && !bannerDismissed(), + ); + + function dismissBanner(): void { + persistBannerDismissed(); + setShowBanner(false); + } + + return ( +
+ + Skip to content + + {showBanner && ( + + )} +
+ + + enspack + + +
+
+ {children} +
+
+ + enspack/0.1 · {config.chain} + + + SPEC.md + {" · "} + {config.indexUrl} + {" · "} + registrar + +
+
+ ); +} diff --git a/apps/site/src/components/model-list.tsx b/apps/site/src/components/model-list.tsx new file mode 100644 index 0000000..5c3e385 --- /dev/null +++ b/apps/site/src/components/model-list.tsx @@ -0,0 +1,160 @@ +import { Link } from "react-router"; +import type { NameListItem } from "../lib/api.js"; +import type { Chain } from "../lib/config.js"; +import { firstLabel, formatBytes } from "../lib/format.js"; +import { LicenseBadge } from "./badge.js"; +import { Hash } from "./hash.js"; +import { EmptyState, ErrorState, Loading } from "./states.js"; + +function displayFromModel(model: string): string { + return firstLabel(model).replaceAll("--", "/"); +} + +export function ModelList({ + items, + nextCursor, + loading, + loadingMore, + error, + qInput, + onQChange, + publishers, + selectedPublisher, + onPublisherChange, + onLoadMore, + chain, +}: { + items: NameListItem[]; + nextCursor: string | null; + loading: boolean; + loadingMore: boolean; + error: Error | null; + qInput: string; + onQChange: (value: string) => void; + publishers: { name: string }[]; + selectedPublisher: string | null; + onPublisherChange: (name: string | null) => void; + onLoadMore: () => void; + chain: Chain; +}) { + const filtered = qInput !== "" || selectedPublisher !== null; + + return ( +
+

+ Models +

+
+
+ +
+ onQChange(e.target.value)} + placeholder="Name, org, or upstream" + autoComplete="off" + /> + {qInput !== "" && ( + + )} +
+
+
+ Filter by publisher + + {publishers.map((p) => ( + + ))} +
+
+ {loading ? ( + + ) : error ? ( + + ) : items.length === 0 ? ( + + {filtered ? null : ( +

+ Publish one: enspack publish --from-hf <org/repo>. +

+ )} +
+ ) : ( + <> +
+ + + + + + + + + + + + {items.map((item) => ( + + + + + + + + ))} + +
NamePublisherLicenseSizeLatest
+ + {displayFromModel(item.model)} + + + + + {item.publisher} + + + + {formatBytes(item.totalSize)}{item.latest.version}
+
+ {nextCursor !== null && ( + + )} + + )} +
+ ); +} diff --git a/apps/site/src/components/states.tsx b/apps/site/src/components/states.tsx new file mode 100644 index 0000000..807f493 --- /dev/null +++ b/apps/site/src/components/states.tsx @@ -0,0 +1,34 @@ +import type { ReactNode } from "react"; +import { ApiError } from "../lib/api.js"; + +export function Loading({ label = "Loading" }: { label?: string }) { + return ( + + {label}… + + ); +} + +export function EmptyState({ title, children }: { title: string; children?: ReactNode }) { + return ( +
+

{title}

+ {children} +
+ ); +} + +export function ErrorState({ error, what = "data" }: { error: Error; what?: string }) { + const detail = + error instanceof ApiError + ? error.status === 0 + ? "The indexer could not be reached." + : `The indexer answered ${error.status} (${error.code}).` + : error.message; + return ( +
+

Could not load {what}

+

{detail}

+
+ ); +} diff --git a/apps/site/src/components/stats.tsx b/apps/site/src/components/stats.tsx new file mode 100644 index 0000000..2534cae --- /dev/null +++ b/apps/site/src/components/stats.tsx @@ -0,0 +1,28 @@ +import { formatBytes } from "../lib/format.js"; + +export function Stats({ + models, + publishers, + bytes, +}: { + models: number; + publishers: number; + bytes: number; +}) { + return ( +
+
+
{models}
+
indexed models
+
+
+
{publishers}
+
publishers
+
+
+
{formatBytes(bytes)}
+
verified bytes
+
+
+ ); +} diff --git a/apps/site/src/components/trust-chain.tsx b/apps/site/src/components/trust-chain.tsx new file mode 100644 index 0000000..bf5159f --- /dev/null +++ b/apps/site/src/components/trust-chain.tsx @@ -0,0 +1,175 @@ +import type { ReactNode } from "react"; +import type { Manifest } from "../lib/api.js"; +import type { Chain } from "../lib/config.js"; +import { ensAppUrl, formatBytes, formatDate, ipfsUrl } from "../lib/format.js"; +import { ExternalLink } from "./external-link.js"; +import { Hash } from "./hash.js"; + +function BytesField({ label, children }: { label: string; children: ReactNode }) { + return ( +
+
{label}
+
{children}
+
+ ); +} + +export function TrustChain({ + manifest, + cid, + chain, +}: { + manifest: Manifest; + cid: string; + chain: Chain; +}) { + const dist = manifest.distribution; + const torrent = dist.torrent; + const torrentCid = torrent?.cid; + const torrentUrl = torrent?.url; + const seeds = dist.seeds; + const ipfs = dist.ipfs; + const oci = dist.oci; + const hb = dist.hb; + + return ( +
+

Trust chain

+
    +
  1. +
  2. + +
  3. +
  4. + +
  5. +
  6. + +
  7. +
  8. + +
  9. +
  10. +
+
+ ); +} diff --git a/apps/site/src/components/versions-list.tsx b/apps/site/src/components/versions-list.tsx new file mode 100644 index 0000000..a807c95 --- /dev/null +++ b/apps/site/src/components/versions-list.tsx @@ -0,0 +1,72 @@ +import { Link } from "react-router"; +import type { NameVersion } from "../lib/api.js"; +import { formatDate } from "../lib/format.js"; +import { Badge } from "./badge.js"; +import { CopyButton } from "./copy-button.js"; +import { Hash } from "./hash.js"; + +/** Hash + in-app link without nesting a copy button inside the ``. */ +function VersionName({ name }: { name: string }) { + return ( + + + {name} + + + + ); +} + +export function VersionsList({ + versions, + latestName, +}: { + versions: readonly NameVersion[]; + latestName: string; +}) { + return ( +
+

Versions

+
+ + + + + + + + + + + {versions.map((v) => { + const latest = v.name === latestName; + return ( + + + + + + + ); + })} + +
versionnamecidcreatedAt
+ {v.version} + {latest ? latest : null} + + + + + {formatDate(v.createdAt)}
+
+

+ Version names are immutable by convention; a changed contenthash on one appears under{" "} + Violations. +

+
+ ); +} diff --git a/apps/site/src/demo/fixtures.ts b/apps/site/src/demo/fixtures.ts new file mode 100644 index 0000000..e998dce --- /dev/null +++ b/apps/site/src/demo/fixtures.ts @@ -0,0 +1,26 @@ +import attestationJeff from "../../test/fixtures/attestation-jeff.json"; +import nameJeff from "../../test/fixtures/name-jeff.json"; +import nameLlama from "../../test/fixtures/name-llama.json"; +import nameQwen from "../../test/fixtures/name-qwen.json"; +import names from "../../test/fixtures/names.json"; +import publishers from "../../test/fixtures/publishers.json"; +import violations from "../../test/fixtures/violations.json"; +import type { + Attestation, + NameDetail, + NamesPage, + PublishersPage, + ViolationsPage, +} from "../lib/api.js"; +import type { FixtureData } from "../lib/client.js"; + +const details = [nameQwen, nameLlama, nameJeff] as unknown as NameDetail[]; + +/** Recorded indexer responses. Shared by unit tests and `VITE_DATA_SOURCE=demo`. */ +export const demoFixtures: FixtureData = { + names: names as NamesPage, + details: Object.fromEntries(details.map((d) => [d.model, d])), + publishers: publishers as PublishersPage, + violations: violations as ViolationsPage, + attestations: { jeff: attestationJeff as Attestation }, +}; diff --git a/apps/site/src/lib/api.ts b/apps/site/src/lib/api.ts new file mode 100644 index 0000000..a7eedb4 --- /dev/null +++ b/apps/site/src/lib/api.ts @@ -0,0 +1,75 @@ +import type { Manifest, ManifestFile } from "@enspack/core"; + +export type { Manifest, ManifestFile }; + +/** Mirrors `services/indexer/src/api/app.ts` (MVP.md §4.3). Do not widen without changing the indexer. */ +export interface NameListItem { + model: string; + publisher: string; + latest: { name: string; version: string; cid: string }; + upstream: string; + license: string; + totalSize: number; +} + +export interface NamesPage { + items: NameListItem[]; + nextCursor: string | null; +} + +export interface NameVersion { + name: string; + version: string; + cid: string; + createdAt: string; +} + +export interface NameDetail { + model: string; + versions: NameVersion[]; + manifest: Manifest; +} + +export interface PublisherItem { + name: string; + hf: string | null; + models: number; +} + +export interface PublishersPage { + items: PublisherItem[]; +} + +export interface Violation { + name: string; + node: string; + previousCid: string; + newCid: string; + block: number; +} + +export interface ViolationsPage { + items: Violation[]; +} + +/** Registrar `GET /v1/publishers/:label`. Shape is the registrar's; render as key/value. */ +export type Attestation = Record; + +export interface ListNamesParams { + q?: string; + publisher?: string; + cursor?: string; + limit?: number; +} + +export class ApiError extends Error { + readonly status: number; + readonly code: string; + + constructor(status: number, code: string, message: string) { + super(message); + this.name = "ApiError"; + this.status = status; + this.code = code; + } +} diff --git a/apps/site/src/lib/client-context.tsx b/apps/site/src/lib/client-context.tsx new file mode 100644 index 0000000..65daa49 --- /dev/null +++ b/apps/site/src/lib/client-context.tsx @@ -0,0 +1,29 @@ +import { type ReactNode, createContext, useContext } from "react"; +import { demoFixtures } from "../demo/fixtures.js"; +import { FixtureIndexClient, HttpIndexClient, type IndexClient } from "./client.js"; +import { type SiteConfig, config } from "./config.js"; + +export function createClient(cfg: SiteConfig): IndexClient { + return cfg.dataSource === "demo" + ? new FixtureIndexClient(demoFixtures) + : new HttpIndexClient(cfg.indexUrl, cfg.registrarUrl); +} + +const ClientContext = createContext(null); + +export function ClientProvider({ + client, + children, +}: { + client?: IndexClient; + children: ReactNode; +}) { + const value = client ?? createClient(config); + return {children}; +} + +export function useClient(): IndexClient { + const c = useContext(ClientContext); + if (c === null) throw new Error("useClient outside ClientProvider"); + return c; +} diff --git a/apps/site/src/lib/client.ts b/apps/site/src/lib/client.ts new file mode 100644 index 0000000..f842816 --- /dev/null +++ b/apps/site/src/lib/client.ts @@ -0,0 +1,150 @@ +import { + ApiError, + type Attestation, + type ListNamesParams, + type NameDetail, + type NamesPage, + type PublishersPage, + type ViolationsPage, +} from "./api.js"; + +export interface IndexClient { + listNames(params?: ListNamesParams): Promise; + getName(name: string): Promise; + listPublishers(): Promise; + listViolations(): Promise; + /** Resolves to null on 404 (name not issued by the registrar). */ + getAttestation(label: string): Promise; +} + +async function getJson(url: string, fetchImpl: typeof fetch): Promise { + let res: Response; + try { + res = await fetchImpl(url, { headers: { accept: "application/json" } }); + } catch (err) { + throw new ApiError(0, "NETWORK", err instanceof Error ? err.message : "network error"); + } + if (!res.ok) { + let code = "HTTP"; + let message = `${res.status} ${res.statusText}`; + try { + const body = (await res.json()) as { error?: string; code?: string }; + if (typeof body.code === "string") code = body.code; + if (typeof body.error === "string") message = body.error; + } catch { + // non-JSON error body: keep the status text + } + throw new ApiError(res.status, code, message); + } + return (await res.json()) as T; +} + +export class HttpIndexClient implements IndexClient { + private readonly indexUrl: string; + private readonly registrarUrl: string; + private readonly fetchImpl: typeof fetch; + + constructor(indexUrl: string, registrarUrl: string, fetchImpl: typeof fetch = fetch) { + this.indexUrl = indexUrl; + this.registrarUrl = registrarUrl; + this.fetchImpl = fetchImpl; + } + + listNames(params: ListNamesParams = {}): Promise { + const qs = new URLSearchParams(); + if (params.q) qs.set("q", params.q); + if (params.publisher) qs.set("publisher", params.publisher); + if (params.cursor) qs.set("cursor", params.cursor); + if (params.limit !== undefined) qs.set("limit", String(params.limit)); + const suffix = qs.size > 0 ? `?${qs.toString()}` : ""; + return getJson(`${this.indexUrl}/v1/names${suffix}`, this.fetchImpl); + } + + getName(name: string): Promise { + return getJson( + `${this.indexUrl}/v1/names/${encodeURIComponent(name)}`, + this.fetchImpl, + ); + } + + listPublishers(): Promise { + return getJson(`${this.indexUrl}/v1/publishers`, this.fetchImpl); + } + + listViolations(): Promise { + return getJson(`${this.indexUrl}/v1/violations`, this.fetchImpl); + } + + async getAttestation(label: string): Promise { + try { + return await getJson( + `${this.registrarUrl}/v1/publishers/${encodeURIComponent(label)}`, + this.fetchImpl, + ); + } catch (err) { + if (err instanceof ApiError && err.status === 404) return null; + throw err; + } + } +} + +export interface FixtureData { + names: NamesPage; + details: Record; + publishers: PublishersPage; + violations: ViolationsPage; + attestations: Record; +} + +/** Serves recorded fixtures. Used by tests and by `VITE_DATA_SOURCE=demo`. */ +export class FixtureIndexClient implements IndexClient { + private readonly data: FixtureData; + + constructor(data: FixtureData) { + this.data = data; + } + + async listNames(params: ListNamesParams = {}): Promise { + let items = this.data.names.items; + if (params.publisher) items = items.filter((i) => i.publisher === params.publisher); + if (params.q) { + const needle = params.q.toLowerCase(); + items = items.filter((i) => { + const display = this.data.details[i.model]?.manifest.displayName ?? ""; + return ( + i.model.toLowerCase().includes(needle) || + i.upstream.toLowerCase().includes(needle) || + display.toLowerCase().includes(needle) + ); + }); + } + if (params.cursor) items = items.filter((i) => i.model > (params.cursor as string)); + const limit = params.limit ?? 50; + const page = items.slice(0, limit); + const last = page[page.length - 1]; + const nextCursor = + page.length === limit && items.length > page.length && last ? last.model : null; + return { items: page, nextCursor }; + } + + async getName(name: string): Promise { + const direct = this.data.details[name]; + if (direct) return direct; + for (const d of Object.values(this.data.details)) { + if (d.versions.some((v) => v.name === name)) return d; + } + throw new ApiError(404, "NOT_FOUND", "unknown name"); + } + + async listPublishers(): Promise { + return this.data.publishers; + } + + async listViolations(): Promise { + return this.data.violations; + } + + async getAttestation(label: string): Promise { + return this.data.attestations[label] ?? null; + } +} diff --git a/apps/site/src/lib/config.ts b/apps/site/src/lib/config.ts new file mode 100644 index 0000000..78b0a42 --- /dev/null +++ b/apps/site/src/lib/config.ts @@ -0,0 +1,29 @@ +export type Chain = "mainnet" | "sepolia"; +export type DataSource = "http" | "demo"; + +export interface SiteConfig { + indexUrl: string; + registrarUrl: string; + chain: Chain; + dataSource: DataSource; + repoUrl: string; +} + +function stripSlash(u: string): string { + return u.endsWith("/") ? u.slice(0, -1) : u; +} + +/** Reads Vite env once. Only public URLs and the chain name; never keys or RPC URLs. */ +export function readConfig(env: ImportMetaEnv = import.meta.env): SiteConfig { + const chain: Chain = env.VITE_CHAIN === "mainnet" ? "mainnet" : "sepolia"; + const dataSource: DataSource = env.VITE_DATA_SOURCE === "demo" ? "demo" : "http"; + return { + indexUrl: stripSlash(env.VITE_INDEX_URL ?? "https://index.enspack.dev"), + registrarUrl: stripSlash(env.VITE_REGISTRAR_URL ?? "https://registrar.enspack.dev"), + chain, + dataSource, + repoUrl: "https://github.com/jefflau/enspack", + }; +} + +export const config: SiteConfig = readConfig(); diff --git a/apps/site/src/lib/format.ts b/apps/site/src/lib/format.ts new file mode 100644 index 0000000..fc32444 --- /dev/null +++ b/apps/site/src/lib/format.ts @@ -0,0 +1,63 @@ +import type { Chain } from "./config.js"; + +const UNITS = ["B", "KB", "MB", "GB", "TB"] as const; + +export function formatBytes(n: number): string { + if (!Number.isFinite(n) || n < 0) return "–"; + let v = n; + let i = 0; + while (v >= 1000 && i < UNITS.length - 1) { + v /= 1000; + i += 1; + } + const digits = i === 0 ? 0 : v >= 100 ? 0 : v >= 10 ? 1 : 2; + return `${v.toFixed(digits)} ${UNITS[i]}`; +} + +export function formatDate(iso: string): string { + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return iso; + return d.toISOString().slice(0, 10); +} + +/** `0xabcd…1234` style; keeps the full value available via `title` at the call site. */ +export function shortHex(hex: string, head = 6, tail = 4): string { + if (hex.length <= head + tail + 1) return hex; + return `${hex.slice(0, head)}…${hex.slice(-tail)}`; +} + +/** First label of an ENS name: `jeff.enspack.eth` → `jeff`. */ +export function firstLabel(name: string): string { + return name.split(".")[0] ?? name; +} + +export function explorerTx(chain: Chain, hash: string): string { + const host = chain === "mainnet" ? "etherscan.io" : "sepolia.etherscan.io"; + return `https://${host}/tx/${hash}`; +} + +export function explorerBlock(chain: Chain, block: number): string { + const host = chain === "mainnet" ? "etherscan.io" : "sepolia.etherscan.io"; + return `https://${host}/block/${block}`; +} + +export function ensAppUrl(chain: Chain, name: string): string { + const host = chain === "mainnet" ? "app.ens.domains" : "sepolia.app.ens.domains"; + return `https://${host}/${name}`; +} + +export function ipfsUrl(cid: string): string { + return `https://${cid}.ipfs.dweb.link/`; +} + +export function hfUrl(namespaceOrRepo: string): string { + return `https://huggingface.co/${namespaceOrRepo}`; +} + +export function isTxHash(v: unknown): v is string { + return typeof v === "string" && /^0x[0-9a-fA-F]{64}$/.test(v); +} + +export function isAddress(v: unknown): v is string { + return typeof v === "string" && /^0x[0-9a-fA-F]{40}$/.test(v); +} diff --git a/apps/site/src/lib/use-document-title.ts b/apps/site/src/lib/use-document-title.ts new file mode 100644 index 0000000..587f649 --- /dev/null +++ b/apps/site/src/lib/use-document-title.ts @@ -0,0 +1,15 @@ +import { useEffect } from "react"; + +const SUFFIX = "enspack"; + +/** Sets `document.title` for the current page; pass `null` while data is still loading. */ +export function useDocumentTitle(title: string | null): void { + useEffect(() => { + if (title === null) return; + const previous = document.title; + document.title = title === "" ? SUFFIX : `${title} · ${SUFFIX}`; + return () => { + document.title = previous; + }; + }, [title]); +} diff --git a/apps/site/src/lib/use-query.ts b/apps/site/src/lib/use-query.ts new file mode 100644 index 0000000..c50d42c --- /dev/null +++ b/apps/site/src/lib/use-query.ts @@ -0,0 +1,40 @@ +import { useEffect, useState } from "react"; + +export interface QueryState { + data: T | null; + error: Error | null; + loading: boolean; +} + +/** + * Minimal async hook. `key` must change when the inputs change; the effect re-runs on it and + * ignores results from superseded runs. + */ +export function useQuery(key: string, run: () => Promise): QueryState { + const [state, setState] = useState>({ data: null, error: null, loading: true }); + + // biome-ignore lint/correctness/useExhaustiveDependencies: `run` is keyed by `key` on purpose + useEffect(() => { + let cancelled = false; + setState({ data: null, error: null, loading: true }); + run().then( + (data) => { + if (!cancelled) setState({ data, error: null, loading: false }); + }, + (err: unknown) => { + if (!cancelled) { + setState({ + data: null, + error: err instanceof Error ? err : new Error(String(err)), + loading: false, + }); + } + }, + ); + return () => { + cancelled = true; + }; + }, [key]); + + return state; +} diff --git a/apps/site/src/main.tsx b/apps/site/src/main.tsx new file mode 100644 index 0000000..4d2469f --- /dev/null +++ b/apps/site/src/main.tsx @@ -0,0 +1,20 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router"; +import { App } from "./app.js"; +import { ClientProvider } from "./lib/client-context.js"; +import "./styles/tokens.css"; +import "./styles/global.css"; + +const root = document.getElementById("root"); +if (root === null) throw new Error("missing #root"); + +createRoot(root).render( + + + + + + + , +); diff --git a/apps/site/src/pages/get-started.tsx b/apps/site/src/pages/get-started.tsx new file mode 100644 index 0000000..ffa7415 --- /dev/null +++ b/apps/site/src/pages/get-started.tsx @@ -0,0 +1,124 @@ +import { CommandBlock } from "../components/command-block.js"; +import { ExternalLink } from "../components/external-link.js"; +import { config } from "../lib/config.js"; +import { useDocumentTitle } from "../lib/use-document-title.js"; +import "../styles/pages/get-started.css"; + +const EXAMPLE = "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth"; +const GET_CMD = `enspack get ${EXAMPLE}`; +const ADD_CMD = `enspack add ${EXAMPLE}`; +const PUBLISH_CMD = "enspack publish --from-hf --publisher --version "; + +export function GetStartedPage() { + useDocumentTitle("Get started"); + const claimHref = `${config.registrarUrl}/`; + const specHref = `${config.repoUrl}/blob/master/SPEC.md`; + + return ( +
+

Get started

+

+ An ENS name points at a content-addressed manifest. Hosts are never trusted. +

+
    +
  • The CLI checks every file by SHA-256.
  • +
  • Bytes come from BitTorrent plus HF webseeds.
  • +
+ +
+

Install

+

+ Requires Node 22+ and aria2c on PATH (the downloader the CLI + shells out to). +

+ +
+ +
+

Fetch a model

+

Resolves the name and verifies every file before writing the HF cache.

+
    +
  • Download the manifest CID.
  • +
  • Fetch the torrent and webseeds.
  • +
  • Check each file by SHA-256.
  • +
+ + {config.chain !== "mainnet" && ( + <> +

+ This catalog is on the {config.chain} testnet. Pass{" "} + --chain {config.chain} so the CLI reads the same names. +

+ + + )} +
+ +
+

Pin in a project

+

Lock a name into a project so later installs stay content-addressed.

+
    +
  • + enspack add resolves a name and appends it to enspack.lock. +
  • +
  • + enspack install reads the lockfile, requires each CID to match, then + downloads and verifies. +
  • +
  • + A CID mismatch exits 3 (unless you pass --update). +
  • +
+ + +
+ +
+

Publish

+

+ Build a manifest from a Hugging Face repo and write the version + model names on-chain. +

+
    +
  • ENSv1 (SPEC §8): 3 transactions for a new model, 2 for a new version.
  • +
  • + ENSv2 (Sepolia's default): 4 txs for a new model, 2 for a new version, plus + first-time publisher setup. +
  • +
+ +
+ +
+

Claim a publisher name

+

+ Proof of Hugging Face org or user control binds a label under enspack.eth to + your wallet (SPEC §7). +

+
    +
  • + Request a claim: POST /v1/claims with your HF namespace and address; you + get a claimId and a challenge. +
  • +
  • + Commit enspack-verify.txt to a public repo under that namespace, containing + exactly the challenge and address. +
  • +
  • + Sign the challenge with the same address (EIP-191 personal_sign). +
  • +
  • + Submit POST /v1/claims/{"{claimId}"}/verify with the repo and signature. + The registrar issues the name and stores the attestation permanently. +
  • +
+ + Claim <you>.enspack.eth + +

+ SPEC.md + GitHub +

+
+
+ ); +} diff --git a/apps/site/src/pages/home.tsx b/apps/site/src/pages/home.tsx new file mode 100644 index 0000000..e535deb --- /dev/null +++ b/apps/site/src/pages/home.tsx @@ -0,0 +1,114 @@ +import { useEffect, useState } from "react"; +import { Hero } from "../components/hero.js"; +import { HowItWorks } from "../components/how-it-works.js"; +import { ModelList } from "../components/model-list.js"; +import { Stats } from "../components/stats.js"; +import type { ListNamesParams, NameListItem } from "../lib/api.js"; +import { useClient } from "../lib/client-context.js"; +import { config } from "../lib/config.js"; +import { useDocumentTitle } from "../lib/use-document-title.js"; +import { useQuery } from "../lib/use-query.js"; +import "../styles/pages/home.css"; + +const SEARCH_DEBOUNCE_MS = 250; + +function namesParams(q: string, publisher: string | null, cursor?: string): ListNamesParams { + const params: ListNamesParams = {}; + if (q) params.q = q; + if (publisher) params.publisher = publisher; + if (cursor) params.cursor = cursor; + return params; +} + +export function HomePage() { + useDocumentTitle(""); + const client = useClient(); + const [qInput, setQInput] = useState(""); + const [q, setQ] = useState(""); + const [publisher, setPublisher] = useState(null); + const [items, setItems] = useState([]); + const [nextCursor, setNextCursor] = useState(null); + const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [namesError, setNamesError] = useState(null); + + // Stable key: hero/stats stay on the first unfiltered page and do not refetch on search. + const catalog = useQuery("names-unfiltered", () => client.listNames()); + const publishers = useQuery("publishers", () => client.listPublishers()); + + useEffect(() => { + if (qInput === q) return; + const t = setTimeout(() => setQ(qInput), SEARCH_DEBOUNCE_MS); + return () => clearTimeout(t); + }, [qInput, q]); + + // useQuery resets on key change, which would drop already-loaded pages. + useEffect(() => { + let cancelled = false; + setLoading(true); + setNamesError(null); + client.listNames(namesParams(q, publisher)).then( + (page) => { + if (cancelled) return; + setItems(page.items); + setNextCursor(page.nextCursor); + setLoading(false); + }, + (err: unknown) => { + if (cancelled) return; + setItems([]); + setNextCursor(null); + setNamesError(err instanceof Error ? err : new Error(String(err))); + setLoading(false); + }, + ); + return () => { + cancelled = true; + }; + }, [client, q, publisher]); + + function loadMore() { + if (nextCursor === null || loadingMore) return; + setLoadingMore(true); + client.listNames(namesParams(q, publisher, nextCursor)).then( + (page) => { + setItems((prev) => [...prev, ...page.items]); + setNextCursor(page.nextCursor); + setLoadingMore(false); + }, + (err: unknown) => { + setNamesError(err instanceof Error ? err : new Error(String(err))); + setLoadingMore(false); + }, + ); + } + + const catalogItems = catalog.data?.items ?? []; + const first = catalogItems[0]; + const bytes = catalogItems.reduce((sum, item) => sum + item.totalSize, 0); + const publisherCount = publishers.data?.items.length ?? 0; + + return ( +
+ {first ? : } + {!catalog.loading && catalog.error === null && ( + + )} + + +
+ ); +} diff --git a/apps/site/src/pages/model.tsx b/apps/site/src/pages/model.tsx new file mode 100644 index 0000000..66fd6e9 --- /dev/null +++ b/apps/site/src/pages/model.tsx @@ -0,0 +1,117 @@ +import { Link, useParams } from "react-router"; +import { LicenseBadge } from "../components/badge.js"; +import { ExternalLink } from "../components/external-link.js"; +import { FilesTable } from "../components/files-table.js"; +import { GetPanel } from "../components/get-panel.js"; +import { Hash } from "../components/hash.js"; +import { EmptyState, ErrorState, Loading } from "../components/states.js"; +import { TrustChain } from "../components/trust-chain.js"; +import { VersionsList } from "../components/versions-list.js"; +import { ApiError, type NameDetail, type NameVersion } from "../lib/api.js"; +import { useClient } from "../lib/client-context.js"; +import { config } from "../lib/config.js"; +import { ensAppUrl, formatBytes, shortHex } from "../lib/format.js"; +import { useDocumentTitle } from "../lib/use-document-title.js"; +import { useQuery } from "../lib/use-query.js"; +import "../styles/pages/model.css"; + +function decodeNameParam(raw: string | undefined): string | null { + if (raw === undefined || raw === "") return null; + try { + return decodeURIComponent(raw); + } catch { + return null; + } +} + +/** Latest CID: the version whose name matches the manifest, else the last listed. */ +function latestVersion(detail: NameDetail): NameVersion { + const match = detail.versions.find((v) => v.name === detail.manifest.name); + if (match !== undefined) return match; + const last = detail.versions[detail.versions.length - 1]; + if (last !== undefined) return last; + return detail.manifest.versions[0]; +} + +function ModelHeader({ detail }: { detail: NameDetail }) { + const { manifest } = detail; + const title = manifest.displayName ?? manifest.model; + const upstream = manifest.upstream; + const canonical = manifest.canonical; + + return ( +
+

{title}

+ +

+ + {manifest.publisher} + + + {formatBytes(manifest.totalSize)} + {upstream !== undefined && ( + + {upstream.url} @{shortHex(upstream.revision)} + + )} +

+ {canonical !== undefined && ( +

+ Publisher-verified name:{" "} + +

+ )} +
+ ); +} + +export function ModelPage() { + const { name: raw } = useParams(); + const name = decodeNameParam(raw); + const client = useClient(); + const { data, error, loading } = useQuery(`name:${name ?? ""}`, () => { + if (name === null) { + return Promise.reject(new ApiError(404, "NOT_FOUND", "unknown name")); + } + return client.getName(name); + }); + + useDocumentTitle(data?.manifest.displayName ?? null); + + if (loading) return ; + if (error instanceof ApiError && error.status === 404) { + return ( + + Home + + ); + } + if (error) return ; + if (data === null) return ; + + const latest = latestVersion(data); + + return ( +
+

+ Indexed from {config.chain}. Verification shown here is the indexer's; the CLI + re-verifies every file locally. +

+ +
+
+ + + +
+ +
+
+ ); +} diff --git a/apps/site/src/pages/not-found.tsx b/apps/site/src/pages/not-found.tsx new file mode 100644 index 0000000..5a95434 --- /dev/null +++ b/apps/site/src/pages/not-found.tsx @@ -0,0 +1,12 @@ +import { Link } from "react-router"; +import { EmptyState } from "../components/states.js"; +import { useDocumentTitle } from "../lib/use-document-title.js"; + +export function NotFoundPage() { + useDocumentTitle("Not found"); + return ( + + Back to models + + ); +} diff --git a/apps/site/src/pages/publisher.tsx b/apps/site/src/pages/publisher.tsx new file mode 100644 index 0000000..c263801 --- /dev/null +++ b/apps/site/src/pages/publisher.tsx @@ -0,0 +1,215 @@ +import { Link, useParams } from "react-router"; +import { Badge, LicenseBadge } from "../components/badge.js"; +import { CopyButton } from "../components/copy-button.js"; +import { ExternalLink } from "../components/external-link.js"; +import { Hash } from "../components/hash.js"; +import { EmptyState, ErrorState, Loading } from "../components/states.js"; +import type { Attestation, NameListItem } from "../lib/api.js"; +import { useClient } from "../lib/client-context.js"; +import type { IndexClient } from "../lib/client.js"; +import { config } from "../lib/config.js"; +import { explorerTx, firstLabel, formatBytes, hfUrl, isTxHash } from "../lib/format.js"; +import { useDocumentTitle } from "../lib/use-document-title.js"; +import { useQuery } from "../lib/use-query.js"; +import "../styles/pages/publisher.css"; + +function decodePublisherParam(raw: string | undefined): string | null { + if (raw === undefined || raw === "") return null; + try { + return decodeURIComponent(raw); + } catch { + return null; + } +} + +async function loadPublisherPage(client: IndexClient, name: string) { + const [pubs, namesPage, attestation] = await Promise.all([ + client.listPublishers(), + client.listNames({ publisher: name }), + // Spec §4.3: missing or unreachable attestation is absence, not a page error. + client + .getAttestation(firstLabel(name)) + .catch(() => null), + ]); + return { + publisher: pubs.items.find((p) => p.name === name) ?? null, + models: namesPage.items, + attestation, + }; +} + +function attestationItemKey(item: unknown, i: number): string { + if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") { + return String(item); + } + return String(i); +} + +/** Addresses, commits, signatures: full hex plus a copy control. Tx hashes stay explorer links. */ +function isCopyableHex(value: unknown): value is string { + return typeof value === "string" && /^(?:0x)?[0-9a-fA-F]{40,}$/.test(value); +} + +function AttestationValue({ value }: { value: unknown }) { + if (value === null || value === undefined) { + return ; + } + if (isTxHash(value)) { + return ( + + {value} + + ); + } + if (isCopyableHex(value)) { + return ; + } + if (Array.isArray(value)) { + if (value.length === 0) return []; + return ( +
+ {value.map((item, i) => ( +
+ +
+ ))} +
+ ); + } + if (typeof value === "object") { + return
{JSON.stringify(value, null, 2)}
; + } + return {String(value)}; +} + +function AttestationTable({ data }: { data: Attestation }) { + return ( +
+ + + {Object.entries(data).map(([key, value]) => ( + + + + + ))} + +
{key} + +
+
+ ); +} + +/** Hash + in-app link without nesting a button inside the `
`. */ +function ModelNameLink({ name }: { name: string }) { + return ( + + + {name} + + + + ); +} + +function PublisherModelTable({ items }: { items: NameListItem[] }) { + if (items.length === 0) { + return

No models indexed for this publisher.

; + } + return ( +
+ + + + + + + + + + + {items.map((item) => ( + + + + + + + ))} + +
ModelLicenseSizeLatest
+ + + + {formatBytes(item.totalSize)}{item.latest.version}
+
+ ); +} + +export function PublisherPage() { + const { label: rawLabel } = useParams(); + const name = decodePublisherParam(rawLabel); + const client = useClient(); + const { data, error, loading } = useQuery(`publisher:${name ?? ""}`, () => + name === null + ? Promise.resolve({ publisher: null, models: [] as NameListItem[], attestation: null }) + : loadPublisherPage(client, name), + ); + + useDocumentTitle(name); + + if (name === null) { + return ; + } + if (loading) return ; + if (error) return ; + if (data === null || data.publisher === null) { + return ; + } + + const { publisher, models, attestation } = data; + const modelLabel = publisher.models === 1 ? "model" : "models"; + + return ( +
+
+

+ +

+

+ {publisher.hf !== null && ( + + {publisher.hf} + HF verified + + )} + + {publisher.models} {modelLabel} + +

+
+ +
+

Attestation

+ {attestation === null ? ( +

+ No registrar attestation. This name is not issued through the enspack.eth registrar + (e.g. mirrors.enspack.eth, or a self-owned .eth). +

+ ) : ( + + )} +
+ +
+

Models

+ +
+
+ ); +} diff --git a/apps/site/src/pages/violations.tsx b/apps/site/src/pages/violations.tsx new file mode 100644 index 0000000..3825eb3 --- /dev/null +++ b/apps/site/src/pages/violations.tsx @@ -0,0 +1,93 @@ +import { Link } from "react-router"; +import { CopyButton } from "../components/copy-button.js"; +import { ExternalLink } from "../components/external-link.js"; +import { Hash } from "../components/hash.js"; +import { EmptyState, ErrorState, Loading } from "../components/states.js"; +import { useClient } from "../lib/client-context.js"; +import { config } from "../lib/config.js"; +import { explorerBlock } from "../lib/format.js"; +import { useDocumentTitle } from "../lib/use-document-title.js"; +import { useQuery } from "../lib/use-query.js"; +import "../styles/pages/violations.css"; + +/** Hash + in-app link without nesting a button inside the `
`. */ +function ViolationNameLink({ name }: { name: string }) { + return ( + + + {name} + + + + ); +} + +export function ViolationsPage() { + useDocumentTitle("Violations"); + const client = useClient(); + const { data, error, loading } = useQuery("violations", () => client.listViolations()); + + if (loading) return ; + if (error) return ; + if (data === null || data.items.length === 0) { + return ( + +

+ A row would show the version name, the previous CID → new CID, and the block of the + change. +

+
+ ); + } + + return ( +
+

Violations

+

+ Version names are immutable by convention: publishers must not change a version's + contenthash after it is first set. The indexer flags any later contenthash change on a + version name as a policy violation (SPEC §6.2). +

+
+ + + + + + + + + + + {data.items.map((v) => ( + + + + + + + ))} + +
NameNodeCIDBlock
+ + + + +
+ + + +
+
+ {v.block} +
+
+
+ ); +} diff --git a/apps/site/src/styles/global.css b/apps/site/src/styles/global.css new file mode 100644 index 0000000..62f11d2 --- /dev/null +++ b/apps/site/src/styles/global.css @@ -0,0 +1,485 @@ +/* Base reset and shared primitives. Page-specific rules live in src/styles/pages/*.css. */ + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: var(--font-sans); + font-size: var(--fs-md); + line-height: 1.5; +} + +code, +pre, +.mono { + font-family: var(--font-mono); + font-size: 0.92em; +} + +pre, +.cmd { + background: var(--surface-2); + border: 1px solid var(--border); + border-radius: var(--radius-lg); +} + +a { + color: var(--accent); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +button { + font: inherit; + color: inherit; +} + +.muted { + color: var(--muted); +} + +/* Layout */ +.site { + min-height: 100dvh; + display: flex; + flex-direction: column; +} + +.skip-link { + position: absolute; + left: var(--sp-4); + top: var(--sp-2); + z-index: 100; + transform: translateY(-200%); + background: var(--surface); + color: var(--text); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: var(--sp-2) var(--sp-3); + font-size: var(--fs-sm); +} + +.skip-link:focus { + transform: none; + text-decoration: none; +} + +.banner { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: var(--sp-3); + background: var(--warn-soft); + color: var(--text); + border-bottom: 1px solid var(--border); + text-align: center; + font-size: var(--fs-sm); + padding: var(--sp-2) var(--sp-4); +} + +.banner-text { + margin: 0; +} + +.banner-dismiss { + border: 1px solid var(--border); + background: var(--surface); + border-radius: var(--radius); + padding: 2px var(--sp-2); + font-size: var(--fs-xs); + line-height: 1.4; + cursor: pointer; + color: var(--muted); +} + +.banner-dismiss:hover { + color: var(--text); + border-color: var(--muted); +} + +.site-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: var(--sp-4) var(--sp-6); + max-width: var(--content-max); + width: 100%; + margin: 0 auto; + padding: var(--sp-4) var(--sp-6); +} + +.wordmark { + display: inline-flex; + align-items: center; + gap: var(--sp-2); + font-family: var(--font-mono); + font-weight: 600; + font-size: var(--fs-lg); + color: var(--text); + letter-spacing: -0.02em; +} + +.wordmark:hover { + color: var(--text); + text-decoration: none; +} + +.wordmark-mark { + display: block; + flex-shrink: 0; + color: var(--accent); +} + +.site-nav { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--sp-5); + font-size: var(--fs-sm); +} + +.site-nav a { + color: var(--muted); + padding-block: var(--sp-1); + border-bottom: 2px solid transparent; +} + +.site-nav a.active, +.site-nav a:hover { + color: var(--text); + text-decoration: none; +} + +.site-nav a.active { + border-bottom-color: var(--accent); +} + +@media (max-width: 640px) { + .site-header { + align-items: flex-start; + } + + .site-nav { + flex-basis: 100%; + gap: var(--sp-3) var(--sp-4); + } +} + +.site-main { + flex: 1; + width: 100%; + max-width: var(--content-max); + margin: 0 auto; + padding: var(--sp-6) var(--sp-6) var(--sp-16); +} + +.site-footer { + border-top: 1px solid var(--border); + color: var(--muted); + font-size: var(--fs-xs); + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: var(--sp-4); + max-width: var(--content-max); + width: 100%; + margin: 0 auto; + padding: var(--sp-4) var(--sp-6); +} + +.site-footer a { + color: var(--muted); +} + +.site-footer a:hover { + color: var(--text); +} + +.site-footer-links { + overflow-wrap: anywhere; +} + +/* Pages */ +.page { + display: flex; + flex-direction: column; + gap: var(--sp-8); +} + +.page-title { + font-size: var(--fs-2xl); + font-weight: 600; + letter-spacing: -0.03em; + line-height: 1.15; + margin: 0 0 var(--sp-3); +} + +.section { + display: flex; + flex-direction: column; + gap: var(--sp-4); +} + +.section-title { + font-size: var(--fs-lg); + font-weight: 600; + margin: 0; +} + +.grid-2 { + display: grid; + grid-template-columns: minmax(0, 1fr) var(--aside); + gap: var(--sp-8); + align-items: start; +} + +@media (max-width: 960px) { + .grid-2 { + grid-template-columns: minmax(0, 1fr); + } +} + +/* Tables */ +.table { + width: 100%; + border-collapse: collapse; + font-size: var(--fs-sm); +} + +.table th, +.table td { + text-align: left; + padding: var(--sp-3) var(--sp-3); + border-bottom: 1px solid var(--border); + vertical-align: top; +} + +.table th { + color: var(--muted); + font-weight: 600; + font-size: var(--fs-xs); + text-transform: uppercase; + letter-spacing: 0.02em; +} + +.table td { + overflow-wrap: anywhere; +} + +.table-sticky { + overflow: auto; +} + +.table-sticky thead th { + position: sticky; + top: 0; + background: var(--bg); + z-index: 1; + box-shadow: 0 1px 0 var(--border); +} + +/* Key / value */ +.kv { + display: grid; + grid-template-columns: minmax(7rem, 12rem) minmax(0, 1fr); + gap: var(--sp-2) var(--sp-4); + font-size: var(--fs-sm); + margin: 0; +} + +.kv dt { + color: var(--muted); + font-weight: 500; +} + +.kv dd { + margin: 0; + overflow-wrap: anywhere; +} + +/* Command block: room for a copy button in the top-right */ +.cmd { + position: relative; + padding: var(--sp-4); + padding-right: 5.5rem; + overflow-x: auto; + overflow-wrap: anywhere; + font-family: var(--font-mono); + font-size: var(--fs-sm); + margin: 0; +} + +.cmd pre { + margin: 0; + padding: 0; + background: transparent; + border: none; + border-radius: 0; + font-family: inherit; + font-size: inherit; + overflow-x: auto; +} + +.cmd .copy-btn { + position: absolute; + top: var(--sp-2); + right: var(--sp-2); +} + +/* Primitives */ +.copy-btn { + border: 1px solid var(--border); + background: var(--surface); + border-radius: var(--radius); + padding: 2px var(--sp-2); + font-size: var(--fs-xs); + line-height: 1.4; + cursor: pointer; + color: var(--muted); + white-space: nowrap; + flex-shrink: 0; +} + +.copy-btn:hover { + color: var(--text); + border-color: var(--muted); +} + +@media (pointer: coarse) { + .copy-btn { + min-height: 32px; + } +} + +.btn-primary { + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--accent); + color: #fff; + border: none; + border-radius: var(--radius); + padding: var(--sp-3) var(--sp-5); + font-weight: 600; + cursor: pointer; + text-decoration: none; +} + +.btn-primary:hover { + color: #fff; + text-decoration: none; +} + +.copy-btn[data-copied] { + color: var(--ok); + border-color: var(--ok); +} + +.hash { + display: inline-flex; + align-items: baseline; + flex-wrap: wrap; + gap: var(--sp-2); + max-width: 100%; +} + +.hash-value { + font-family: var(--font-mono); + overflow-wrap: anywhere; + word-break: break-all; +} + +.badge { + display: inline-block; + border-radius: 999px; + padding: 1px var(--sp-2); + font-size: var(--fs-xs); + line-height: 1.5; + border: 1px solid var(--border); + background: var(--surface); + color: var(--muted); + white-space: nowrap; +} + +.badge-accent { + color: var(--accent); + background: var(--accent-soft); + border-color: transparent; +} + +.badge-ok { + color: var(--ok); + background: var(--ok-soft); + border-color: transparent; +} + +.badge-warn { + color: var(--warn); + background: var(--warn-soft); + border-color: transparent; +} + +.badge-danger { + color: var(--danger); + background: var(--danger-soft); + border-color: transparent; +} + +.badge-link:hover { + text-decoration: none; +} + +.state { + border: 1px dashed var(--border); + border-radius: var(--radius-lg); + padding: var(--sp-8); + text-align: center; + color: var(--muted); +} + +.state-title { + color: var(--text); + font-weight: 600; + margin: 0 0 var(--sp-2); +} + +.state-error { + border-color: var(--danger); + border-style: solid; +} + +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: var(--sp-5); +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/apps/site/src/styles/pages/get-started.css b/apps/site/src/styles/pages/get-started.css new file mode 100644 index 0000000..c185ea7 --- /dev/null +++ b/apps/site/src/styles/pages/get-started.css @@ -0,0 +1,51 @@ +.get-started-page h1 { + margin: 0 0 var(--sp-4); + font-size: var(--fs-xl); + font-weight: 600; +} + +.get-started-lead { + margin: 0 0 var(--sp-3); + color: var(--muted); + max-width: 62ch; +} + +.get-started-page section { + margin-bottom: var(--sp-10); +} + +.get-started-page h2 { + margin: 0 0 var(--sp-3); + font-size: var(--fs-lg); + font-weight: 600; +} + +.get-started-page p { + margin: 0 0 var(--sp-3); + max-width: 62ch; +} + +.get-started-page ul { + margin: 0 0 var(--sp-4); + padding-left: 1.25rem; + max-width: 68ch; +} + +.get-started-page li + li { + margin-top: var(--sp-2); +} + +.get-started-page .cmd { + margin: var(--sp-3) 0 var(--sp-4); +} + +.get-started-page .btn-primary { + margin: var(--sp-2) 0 var(--sp-5); +} + +.get-started-links { + display: flex; + flex-wrap: wrap; + gap: var(--sp-5); + font-size: var(--fs-sm); +} diff --git a/apps/site/src/styles/pages/home.css b/apps/site/src/styles/pages/home.css new file mode 100644 index 0000000..c390f51 --- /dev/null +++ b/apps/site/src/styles/pages/home.css @@ -0,0 +1,306 @@ +.home { + display: flex; + flex-direction: column; + gap: var(--sp-12); +} + +.home-hero { + display: flex; + flex-direction: column; + gap: var(--sp-4); + max-width: 44rem; +} + +.home-hero-title { + font-size: var(--fs-2xl); + font-weight: 650; + letter-spacing: -0.03em; + line-height: 1.15; + margin: 0; +} + +.home-hero-sub { + margin: 0; + color: var(--muted); + font-size: var(--fs-md); +} + +.home-stats { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--sp-4); +} + +.stat { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: var(--sp-5); +} + +.stat-value { + font-family: var(--font-mono); + font-size: var(--fs-xl); + font-variant-numeric: tabular-nums; + letter-spacing: -0.02em; + line-height: 1.2; +} + +.stat-label { + color: var(--text); + font-size: var(--fs-sm); + margin-top: var(--sp-1); +} + +.home-section-title { + font-size: var(--fs-lg); + font-weight: 600; + margin: 0 0 var(--sp-4); +} + +.how-cards { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: var(--sp-4); +} + +.how-card { + display: flex; + flex-direction: column; + gap: var(--sp-3); + min-width: 0; +} + +.how-card-title { + margin: 0; + font-size: var(--fs-sm); + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--muted); +} + +.how-artifact { + display: block; + overflow-wrap: anywhere; + word-break: break-all; + color: var(--text); + background: var(--surface-2); + border-radius: var(--radius); + padding: var(--sp-2) var(--sp-3); +} + +.how-trust { + margin: 0; + color: var(--muted); + font-size: var(--fs-sm); + line-height: 1.45; +} + +.model-list-controls { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: var(--sp-4); + margin-bottom: var(--sp-5); +} + +.model-search { + display: flex; + flex-direction: column; + gap: var(--sp-1); + font-size: var(--fs-sm); + color: var(--muted); + flex: 1 1 16rem; +} + +.model-search-field { + position: relative; + display: flex; + align-items: center; +} + +.model-search input { + width: 100%; + font: inherit; + font-family: var(--font-mono); + color: var(--text); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: var(--sp-2) var(--sp-3); + padding-right: calc(24px + var(--sp-3) + var(--sp-2)); +} + +.model-search input[type="search"]::-webkit-search-cancel-button, +.model-search input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; + appearance: none; +} + +.model-search-clear { + position: absolute; + right: var(--sp-2); + top: 0; + bottom: 0; + margin-block: auto; + box-sizing: border-box; + width: 24px; + height: 24px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface-2); + color: var(--text); + cursor: pointer; + font-size: var(--fs-sm); + line-height: 1; +} + +.model-search-clear:hover { + border-color: var(--muted); +} + +.publisher-chips { + display: flex; + flex-wrap: wrap; + gap: var(--sp-2); + margin: 0; + padding: 0; + border: 0; + min-inline-size: 0; +} + +.publisher-chips legend { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.chip { + display: inline-flex; + align-items: center; + border: 1px solid var(--border); + background: var(--surface); + border-radius: 999px; + padding: var(--sp-1) var(--sp-3); + font-size: var(--fs-xs); + font-family: var(--font-mono); + color: var(--muted); + cursor: pointer; + text-decoration: none; + line-height: 1.5; +} + +a.chip:hover { + text-decoration: none; + color: var(--text); +} + +.chip[aria-pressed="true"] { + color: var(--accent); + background: var(--accent-soft); + border-color: transparent; +} + +.chip-link { + color: var(--text); +} + +.model-table-wrap { + overflow-x: auto; +} + +.model-table { + width: 100%; + border-collapse: collapse; + font-size: var(--fs-sm); +} + +.model-table th { + text-align: left; + font-weight: 500; + color: var(--muted); + font-size: var(--fs-xs); + letter-spacing: 0.02em; + text-transform: uppercase; + border-bottom: 1px solid var(--border); + padding: var(--sp-3) var(--sp-3); +} + +.model-table td { + border-bottom: 1px solid var(--border); + padding: var(--sp-4) var(--sp-3); + vertical-align: top; +} + +.model-table td .hash { + display: flex; + margin-top: var(--sp-1); +} + +.model-display { + display: block; + font-weight: 600; + color: var(--text); +} + +.model-display:hover { + color: var(--accent); + text-decoration: none; +} + +.model-table .num { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.load-more { + display: block; + margin: var(--sp-5) auto 0; + border: 1px solid var(--border); + background: var(--surface); + border-radius: var(--radius); + padding: var(--sp-2) var(--sp-5); + cursor: pointer; + color: var(--text); +} + +.load-more:hover:not(:disabled) { + border-color: var(--muted); +} + +.load-more:disabled { + opacity: 0.6; + cursor: default; +} + +@media (max-width: 960px) { + .how-cards { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 640px) { + .home-hero-title { + font-size: var(--fs-xl); + } + + .home-stats, + .how-cards { + grid-template-columns: 1fr; + } +} diff --git a/apps/site/src/styles/pages/model.css b/apps/site/src/styles/pages/model.css new file mode 100644 index 0000000..5e5c8d7 --- /dev/null +++ b/apps/site/src/styles/pages/model.css @@ -0,0 +1,606 @@ +.model-page { + display: flex; + flex-direction: column; + gap: var(--sp-6); +} + +.model-status { + margin: 0; + padding: var(--sp-3) var(--sp-4); + font-size: var(--fs-sm); + color: var(--muted); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); +} + +.model-header { + display: flex; + flex-direction: column; + gap: var(--sp-3); +} + +.model-title { + margin: 0; + font-size: var(--fs-2xl); + font-weight: 650; + letter-spacing: -0.03em; + line-height: 1.15; +} + +.model-name-hash { + font-size: var(--fs-sm); +} + +.model-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--sp-3); + margin: 0; + font-size: var(--fs-sm); + color: var(--muted); +} + +.model-publisher { + display: inline-flex; + align-items: center; + border: 1px solid var(--border); + background: var(--surface); + border-radius: 999px; + padding: var(--sp-1) var(--sp-3); + font-size: var(--fs-xs); + font-family: var(--font-mono); + color: var(--text); + line-height: 1.5; +} + +.model-publisher:hover { + text-decoration: none; + color: var(--accent); +} + +.model-canonical { + margin: 0; + font-size: var(--fs-sm); + color: var(--muted); +} + +.model-canonical a.hash-value, +.model-page a.hash-value:not(.versions-name-link) { + color: var(--accent); +} + +.model-page .hash { + min-width: 0; + max-width: 100%; +} + +.model-page .hash-value { + overflow-wrap: anywhere; + word-break: break-all; + min-width: 0; +} + +.model-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 340px; + gap: var(--sp-8); + align-items: start; +} + +.model-main { + display: flex; + flex-direction: column; + gap: var(--sp-10); + min-width: 0; +} + +@media (max-width: 960px) { + .model-layout { + grid-template-columns: minmax(0, 1fr); + } + + .get-panel { + position: static; + } +} + +.model-page h2 { + margin: 0 0 var(--sp-4); + font-size: var(--fs-lg); + font-weight: 600; +} + +.model-page h3 { + margin: 0 0 var(--sp-3); + font-size: var(--fs-md); + font-weight: 600; +} + +/* Trust chain: 2px rail, accent dots; verify uses --ok. */ +.trust-chain { + list-style: none; + margin: 0; + padding: 0; + position: relative; +} + +.trust-chain::before { + content: ""; + position: absolute; + left: 7px; + top: 6px; + bottom: 6px; + width: 2px; + background: var(--border); +} + +.trust-node { + display: grid; + grid-template-columns: 16px minmax(0, 1fr); + gap: var(--sp-4); + position: relative; + padding-bottom: var(--sp-6); +} + +.trust-node:last-child { + padding-bottom: 0; +} + +.trust-dot { + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--accent); + border: 2px solid var(--bg); + margin-top: 4px; + position: relative; + z-index: 1; +} + +.trust-node-verify .trust-dot { + background: var(--ok); +} + +.trust-body { + min-width: 0; +} + +.trust-caption { + margin: 0 0 var(--sp-1); + font-size: var(--fs-xs); +} + +.trust-names { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: var(--sp-4); +} + +.trust-names > div { + min-width: 0; +} + +@media (max-width: 719px) { + .trust-names { + grid-template-columns: minmax(0, 1fr); + } + + .trust-bytes-field { + grid-template-columns: minmax(0, 1fr); + } +} + +.trust-proves, +.trust-verify { + margin: var(--sp-3) 0 0; + color: var(--muted); + font-size: var(--fs-sm); +} + +.trust-verify { + color: var(--text); +} + +.trust-verify code, +.trust-bytes-field dd, +.trust-bytes-field a, +.trust-bytes-field .hash-value { + overflow-wrap: anywhere; + word-break: break-all; +} + +.trust-manifest-kv { + margin: 0; +} + +.trust-json { + margin-top: var(--sp-3); + font-size: var(--fs-sm); +} + +.trust-json summary { + cursor: pointer; + color: var(--accent); +} + +.trust-json pre { + margin: var(--sp-3) 0 0; + padding: var(--sp-3); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow-x: auto; + font-size: var(--fs-xs); + max-height: 24rem; +} + +.trust-bytes { + display: flex; + flex-direction: column; + gap: var(--sp-3); + margin: 0; +} + +.trust-bytes-field { + display: grid; + grid-template-columns: minmax(6rem, 8rem) minmax(0, 1fr); + gap: var(--sp-2) var(--sp-4); + font-size: var(--fs-sm); +} + +.trust-bytes-field dt { + color: var(--muted); + font-weight: 500; +} + +.trust-bytes-field dd { + margin: 0; + min-width: 0; +} + +.trust-link-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: var(--sp-1); +} + +.trust-link-list a, +.trust-link-list code { + overflow-wrap: anywhere; + word-break: break-all; + font-size: var(--fs-xs); +} + +/* Files */ +.files-table-wrap { + max-height: min(70vh, 36rem); + overflow-x: hidden; + overflow-y: auto; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--surface); +} + +.files-table { + width: 100%; + table-layout: fixed; + border-collapse: collapse; + font-size: var(--fs-sm); +} + +.files-table th:nth-child(1) { + width: 28%; +} + +.files-table th:nth-child(2) { + width: 12%; +} + +.files-table th:nth-child(3) { + width: 12%; +} + +.files-table th:nth-child(4) { + width: 48%; +} + +.files-table th, +.files-table td { + text-align: left; + padding: var(--sp-2) var(--sp-3); + border-bottom: 1px solid var(--border); + vertical-align: top; +} + +.files-table thead th { + position: sticky; + top: 0; + z-index: 1; + background: var(--surface-2); + color: var(--muted); + font-weight: 500; + font-size: var(--fs-xs); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.files-table tfoot th, +.files-table tfoot td { + border-bottom: none; + color: var(--muted); + font-weight: 500; +} + +.files-sort { + background: none; + border: 0; + padding: 0; + cursor: pointer; + color: inherit; + font: inherit; + text-transform: inherit; + letter-spacing: inherit; +} + +.files-sort:hover { + color: var(--text); +} + +.files-path { + font-family: var(--font-mono); + font-size: 0.92em; + font-weight: 500; + overflow-wrap: anywhere; + word-break: break-all; +} + +.files-size { + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.files-sha { + overflow-wrap: anywhere; + word-break: break-all; +} + +.files-sha .hash { + display: flex; + flex-wrap: wrap; + min-width: 0; + max-width: 100%; +} + +@media (max-width: 719px) { + .files-table-wrap { + overflow-x: hidden; + } + + .files-table, + .files-table tbody, + .files-table tfoot { + display: block; + width: 100%; + } + + .files-table th:nth-child(1), + .files-table th:nth-child(2), + .files-table th:nth-child(3), + .files-table th:nth-child(4) { + width: auto; + } + + .files-table thead { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + clip-path: inset(50%); + white-space: nowrap; + border: 0; + } + + .files-table tbody tr { + display: grid; + grid-template-columns: 1fr auto; + grid-template-areas: + "path path" + "role size" + "sha sha"; + gap: var(--sp-2) var(--sp-3); + padding: var(--sp-3); + border-bottom: 1px solid var(--border); + } + + .files-table th, + .files-table td { + display: block; + width: auto; + max-width: 100%; + min-width: 0; + border: 0; + padding: 0; + } + + .files-table tbody td:nth-child(1) { + grid-area: path; + } + + .files-table tbody td:nth-child(2) { + grid-area: role; + } + + .files-table tbody td:nth-child(3) { + grid-area: size; + justify-self: end; + align-self: center; + } + + .files-table tbody td:nth-child(4) { + grid-area: sha; + } + + .files-sha { + min-width: 0; + } + + .files-table tfoot tr { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--sp-3); + padding: var(--sp-3); + } + + .files-table tfoot td:empty { + display: none; + } +} + +/* Versions */ +.versions-table-wrap { + overflow-x: auto; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--surface); +} + +.versions-table { + width: 100%; + border-collapse: collapse; + font-size: var(--fs-sm); +} + +.versions-table th, +.versions-table td { + text-align: left; + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border); + vertical-align: top; + overflow-wrap: anywhere; +} + +.versions-table thead th { + color: var(--muted); + font-weight: 500; + background: var(--surface-2); + font-size: var(--fs-xs); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.versions-table tbody tr:last-child td { + border-bottom: none; +} + +.versions-table td:first-child { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--sp-2); +} + +.versions-name-link { + color: var(--text); +} + +.versions-name-link:hover { + color: var(--accent); + text-decoration: none; +} + +.versions-note { + margin: var(--sp-3) 0 0; + font-size: var(--fs-sm); +} + +/* Get it */ +.get-panel { + position: sticky; + top: var(--sp-4); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: var(--sp-5); +} + +.get-panel h2 { + margin: 0 0 var(--sp-4); +} + +.get-block { + position: relative; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: var(--sp-3); + padding-right: 5.5rem; + margin-bottom: var(--sp-3); +} + +.get-block-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--sp-2); + margin-bottom: var(--sp-2); +} + +.get-block-head .copy-btn { + position: absolute; + top: var(--sp-2); + right: var(--sp-2); +} + +.get-block-label { + font-size: var(--fs-xs); + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.get-block pre { + margin: 0; + font-family: var(--font-mono); + font-size: var(--fs-xs); + overflow-x: auto; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.get-lock { + margin-bottom: var(--sp-3); +} + +.get-lock summary { + cursor: pointer; + color: var(--accent); + font-size: var(--fs-xs); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.get-lock .get-block { + margin-top: var(--sp-3); + margin-bottom: 0; +} + +.get-footnote { + margin: var(--sp-2) 0 0; + font-size: var(--fs-xs); +} + +@media (pointer: coarse) { + .model-page .copy-btn { + min-height: 32px; + padding-block: 6px; + } +} diff --git a/apps/site/src/styles/pages/publisher.css b/apps/site/src/styles/pages/publisher.css new file mode 100644 index 0000000..8f76ed1 --- /dev/null +++ b/apps/site/src/styles/pages/publisher.css @@ -0,0 +1,141 @@ +.publisher-header { + display: flex; + flex-direction: column; + gap: var(--sp-3); + margin-bottom: var(--sp-8); +} + +.publisher-header h1 { + margin: 0; + font-size: var(--fs-xl); + font-weight: 600; + line-height: 1.3; + overflow-wrap: anywhere; +} + +.publisher-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--sp-3); + margin: 0; + color: var(--muted); + font-size: var(--fs-sm); +} + +.publisher-hf { + display: inline-flex; + align-items: center; + gap: var(--sp-2); +} + +.publisher-hf a, +.publisher-page a.hash-value:not(.publisher-name-link) { + color: var(--accent); +} + +.publisher-section { + margin-bottom: var(--sp-8); +} + +.publisher-section h2 { + margin: 0 0 var(--sp-4); + font-size: var(--fs-lg); + font-weight: 600; +} + +.publisher-attestation-note { + margin: 0; + color: var(--muted); + max-width: 62ch; +} + +.publisher-table-wrap { + overflow-x: hidden; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--surface); +} + +.publisher-table { + width: 100%; + table-layout: fixed; + border-collapse: collapse; + font-size: var(--fs-sm); +} + +.publisher-table th, +.publisher-table td { + text-align: left; + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border); + vertical-align: top; +} + +.publisher-table thead th { + color: var(--muted); + font-weight: 500; + background: var(--surface-2); +} + +.publisher-table tbody tr:last-child th, +.publisher-table tbody tr:last-child td { + border-bottom: none; +} + +.publisher-table th[scope="row"] { + font-family: var(--font-mono); + font-size: var(--fs-xs); + font-weight: 500; + color: var(--muted); + width: 160px; + overflow-wrap: anywhere; +} + +.publisher-table td { + overflow-wrap: anywhere; + word-break: break-all; +} + +.publisher-page .hash { + min-width: 0; + max-width: 100%; +} + +.attestation-list { + display: flex; + flex-direction: column; + gap: var(--sp-2); +} + +.attestation-pre { + margin: 0; + padding: var(--sp-3); + background: var(--surface-2); + border-radius: var(--radius); + overflow-wrap: anywhere; + word-break: break-all; + white-space: pre-wrap; + font-size: var(--fs-xs); +} + +.publisher-name-link { + color: var(--text); +} + +.publisher-name-link:hover { + color: var(--accent); + text-decoration: none; +} + +.publisher-empty-models { + margin: 0; + color: var(--muted); +} + +@media (pointer: coarse) { + .publisher-page .copy-btn { + min-height: 32px; + padding-block: 6px; + } +} diff --git a/apps/site/src/styles/pages/violations.css b/apps/site/src/styles/pages/violations.css new file mode 100644 index 0000000..f3a616d --- /dev/null +++ b/apps/site/src/styles/pages/violations.css @@ -0,0 +1,62 @@ +.violations-page h1 { + margin: 0 0 var(--sp-4); + font-size: var(--fs-xl); + font-weight: 600; +} + +.violations-intro { + margin: 0 0 var(--sp-6); + color: var(--muted); + max-width: 68ch; +} + +.violations-table-wrap { + overflow-x: auto; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--surface); +} + +.violations-table { + width: 100%; + border-collapse: collapse; + font-size: var(--fs-sm); +} + +.violations-table th, +.violations-table td { + text-align: left; + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border); + vertical-align: top; +} + +.violations-table thead th { + color: var(--muted); + font-weight: 500; + background: var(--surface-2); +} + +.violations-table tbody tr:last-child td { + border-bottom: none; +} + +.violations-cids { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--sp-2); +} + +.violations-arrow { + color: var(--muted); +} + +.violations-name-link { + color: var(--text); +} + +.violations-name-link:hover { + color: var(--accent); + text-decoration: none; +} diff --git a/apps/site/src/styles/tokens.css b/apps/site/src/styles/tokens.css new file mode 100644 index 0000000..ff1c41f --- /dev/null +++ b/apps/site/src/styles/tokens.css @@ -0,0 +1,64 @@ +:root { + color-scheme: dark light; + + --bg: #0b0d10; + --surface: #12151a; + --surface-2: #1a1f26; + --border: #262c35; + --text: #e6e8eb; + --muted: #8b949e; + --accent: #5298ff; + --accent-soft: rgba(82, 152, 255, 0.14); + --ok: #3fb950; + --ok-soft: rgba(63, 185, 80, 0.14); + --warn: #d29922; + --warn-soft: rgba(210, 153, 34, 0.14); + --danger: #f85149; + --danger-soft: rgba(248, 81, 73, 0.14); + + --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; + --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + + --fs-xs: 13px; + --fs-sm: 14px; + --fs-md: 16px; + --fs-lg: 20px; + --fs-xl: 28px; + --fs-2xl: 40px; + + --radius: 6px; + --radius-lg: 10px; + + --sp-1: 4px; + --sp-2: 8px; + --sp-3: 12px; + --sp-4: 16px; + --sp-5: 20px; + --sp-6: 24px; + --sp-8: 32px; + --sp-10: 40px; + --sp-12: 48px; + --sp-16: 64px; + + --content-max: 1120px; + --aside: 340px; +} + +@media (prefers-color-scheme: light) { + :root { + --bg: #ffffff; + --surface: #f6f8fa; + --surface-2: #eaeef2; + --border: #d0d7de; + --text: #1f2328; + --muted: #59636e; + --accent: #0969da; + --accent-soft: rgba(9, 105, 218, 0.1); + --ok: #1a7f37; + --ok-soft: rgba(26, 127, 55, 0.1); + --warn: #9a6700; + --warn-soft: rgba(154, 103, 0, 0.1); + --danger: #cf222e; + --danger-soft: rgba(207, 34, 46, 0.1); + } +} diff --git a/apps/site/src/vite-env.d.ts b/apps/site/src/vite-env.d.ts new file mode 100644 index 0000000..52ea523 --- /dev/null +++ b/apps/site/src/vite-env.d.ts @@ -0,0 +1,8 @@ +/// + +interface ImportMetaEnv { + readonly VITE_INDEX_URL?: string; + readonly VITE_REGISTRAR_URL?: string; + readonly VITE_CHAIN?: string; + readonly VITE_DATA_SOURCE?: string; +} diff --git a/apps/site/test/copy-button.test.tsx b/apps/site/test/copy-button.test.tsx new file mode 100644 index 0000000..fd70672 --- /dev/null +++ b/apps/site/test/copy-button.test.tsx @@ -0,0 +1,32 @@ +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { CopyButton } from "../src/components/copy-button.js"; + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +describe("CopyButton", () => { + it("shows Copied after the async clipboard succeeds", async () => { + const writeText = vi.fn(async () => undefined); + Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true }); + render(); + fireEvent.click(screen.getByRole("button")); + await waitFor(() => expect(screen.getByRole("button")).toHaveTextContent("Copied")); + expect(writeText).toHaveBeenCalledWith("abc"); + }); + + it("falls back to execCommand when the clipboard API is denied", async () => { + Object.defineProperty(navigator, "clipboard", { + value: { writeText: vi.fn(async () => Promise.reject(new Error("denied"))) }, + configurable: true, + }); + const exec = vi.fn(() => true); + Object.defineProperty(document, "execCommand", { value: exec, configurable: true }); + render(); + fireEvent.click(screen.getByRole("button")); + await waitFor(() => expect(screen.getByRole("button")).toHaveTextContent("Copied")); + expect(exec).toHaveBeenCalledWith("copy"); + }); +}); diff --git a/apps/site/test/fixtures/attestation-jeff.json b/apps/site/test/fixtures/attestation-jeff.json new file mode 100644 index 0000000..aad442a --- /dev/null +++ b/apps/site/test/fixtures/attestation-jeff.json @@ -0,0 +1,17 @@ +{ + "label": "jeff", + "name": "jeff.enspack.eth", + "hfNamespace": "jefflau", + "address": "0x14ac0369ddef2ce89577f19207f84a1fb764d543", + "claimId": "clm_eae6dce4d473c56a", + "challenge": "enspack-verify:6b284480a6dda9b7867f94e86a035fe5b93d7130d2044d4ca7aea5d846981469", + "repo": "jefflau/enspack-verify", + "commit": "a56625b5541b33f6b1842637965888e8a15b470b", + "signature": "0xd368b0e8bbbd4a474a846a5fbfab4d8cfc1b3f8f413d25dbce5605f3fd38bbc260af54755aae952ac69e62de639b238540c593d6d3e6e9cc992768ff150fdc987f", + "txs": [ + "0x4f804f611d05209bb3c24ed4275733d27ed5f5198c5bfe023dabe3da835d82ff", + "0x4d97664a0b4d68b03261abd7a5683ac97bdc093430699f676c514d3bc96ba014" + ], + "issuedAt": "2026-09-16T09:41:00Z", + "chain": "sepolia" +} diff --git a/apps/site/test/fixtures/name-jeff.json b/apps/site/test/fixtures/name-jeff.json new file mode 100644 index 0000000..5f26a48 --- /dev/null +++ b/apps/site/test/fixtures/name-jeff.json @@ -0,0 +1,86 @@ +{ + "model": "tiny-random.jeff.enspack.eth", + "versions": [ + { + "name": "v0-1-0.tiny-random.jeff.enspack.eth", + "version": "0.1.0", + "cid": "bafkreiq2jzse7l6h3bz37newkvc5qkjog6b6slmbt7smopt333icaq4ipl", + "createdAt": "2026-09-16T10:00:00Z" + }, + { + "name": "v0-2-0.tiny-random.jeff.enspack.eth", + "version": "0.2.0", + "cid": "bafkreiqlqo4tavkcgykxj22n246t6z462d45spaqezclkcxnqw3dryt4sx", + "createdAt": "2026-09-18T14:12:00Z" + } + ], + "manifest": { + "spec": "enspack/0.1", + "name": "v0-2-0.tiny-random.jeff.enspack.eth", + "model": "tiny-random.jeff.enspack.eth", + "publisher": "jeff.enspack.eth", + "version": "0.2.0", + "createdAt": "2026-09-18T14:12:00Z", + "displayName": "tiny-random", + "license": "mit", + "licenseUrl": "https://huggingface.co/jefflau/tiny-random/blob/3f0c2a9d1e8b7c6a5d4e3f2a1b0c9d8e7f6a5b4c/LICENSE", + "upstream": { + "provider": "huggingface", + "repo": "jefflau/tiny-random", + "url": "https://huggingface.co/jefflau/tiny-random", + "revision": "3f0c2a9d1e8b7c6a5d4e3f2a1b0c9d8e7f6a5b4c" + }, + "distribution": { + "infohash": "cc14c60026a418e23e1a2ad0c1aaee22ed4439f2", + "webseeds": [ + "https://huggingface.co/jefflau/tiny-random/resolve/3f0c2a9d1e8b7c6a5d4e3f2a1b0c9d8e7f6a5b4c/" + ], + "torrent": { + "cid": "bafkrei5j7377yemorjn7fik7ktjkd76htfwpqjftzj5hnk7m45qcmhsi6z" + }, + "magnet": "magnet:?xt=urn:btih:cc14c60026a418e23e1a2ad0c1aaee22ed4439f2&dn=tiny-random" + }, + "files": [ + { + "path": "config.json", + "size": 612, + "sha256": "d34ddab30f9487b9ede3e550786d30eeec2285e2e2a9e7a5e66084b47fe99d65", + "role": "config" + }, + { + "path": "model.safetensors", + "size": 4194512, + "sha256": "ecad3aaece44f0d2180b09308aefb49eea7aa4f3a970b0df190deb54c78a410f", + "role": "weight" + }, + { + "path": "tokenizer.json", + "size": 2113, + "sha256": "f0e21a1b1c7d365985647c38e75d7ff8e4578955da446cac9ca7ebcd0d3234b4", + "role": "tokenizer" + }, + { + "path": "README.md", + "size": 1420, + "sha256": "2ea30fe5d1f68bb891d692993de79d89dc513064a2e3f641b844bd333e8a7acd", + "role": "doc" + } + ], + "totalSize": 4198657, + "versions": [ + { + "version": "0.1.0", + "name": "v0-1-0.tiny-random.jeff.enspack.eth", + "cid": "bafkreiq2jzse7l6h3bz37newkvc5qkjog6b6slmbt7smopt333icaq4ipl", + "createdAt": "2026-09-16T10:00:00Z" + }, + { + "version": "0.2.0", + "name": "v0-2-0.tiny-random.jeff.enspack.eth", + "cid": "bafkreiqlqo4tavkcgykxj22n246t6z462d45spaqezclkcxnqw3dryt4sx", + "createdAt": "2026-09-18T14:12:00Z" + } + ], + "previous": "bafkreiq2jzse7l6h3bz37newkvc5qkjog6b6slmbt7smopt333icaq4ipl" + } +} diff --git a/apps/site/test/fixtures/name-llama.json b/apps/site/test/fixtures/name-llama.json new file mode 100644 index 0000000..e34132b --- /dev/null +++ b/apps/site/test/fixtures/name-llama.json @@ -0,0 +1,97 @@ +{ + "model": "meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth", + "versions": [ + { + "name": "v1-0-0.meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth", + "version": "1.0.0", + "cid": "bafkreioovluxxz2mgsituvkognp33nuccuxtsn7kiwa5pvisywiy2rarrh", + "createdAt": "2026-09-15T08:30:00Z" + } + ], + "manifest": { + "spec": "enspack/0.1", + "name": "v1-0-0.meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth", + "model": "meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth", + "publisher": "mirrors.enspack.eth", + "version": "1.0.0", + "createdAt": "2026-09-15T08:30:00Z", + "displayName": "Llama-3.2-1B-Instruct", + "license": "llama3.2", + "licenseUrl": "https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/blob/9213176726f574b556790deb65791e0c5aa438b6/LICENSE", + "upstream": { + "provider": "huggingface", + "repo": "meta-llama/Llama-3.2-1B-Instruct", + "url": "https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct", + "revision": "9213176726f574b556790deb65791e0c5aa438b6" + }, + "distribution": { + "infohash": "2261eac56b458d3a2720ff6f5e5e5cb6de75649e", + "webseeds": [ + "https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/resolve/9213176726f574b556790deb65791e0c5aa438b6/" + ], + "torrent": { + "cid": "bafkreiari6rfjs3w7ozmozlpllnsirjwtfv54lkqnf3nmpzaaf7lsztciq" + }, + "magnet": "magnet:?xt=urn:btih:2261eac56b458d3a2720ff6f5e5e5cb6de75649e&dn=Llama-3.2-1B-Instruct" + }, + "files": [ + { + "path": "config.json", + "size": 877, + "sha256": "b2adbeb198827c155f321d14a71864793e42e9c7b3784be7e91e362f4c8fbe1d", + "role": "config" + }, + { + "path": "generation_config.json", + "size": 189, + "sha256": "07973cd474e5f28ce207287f5c8ed0435d08fa7952903212e212cf9ba84311b5", + "role": "config" + }, + { + "path": "model.safetensors", + "size": 2471645608, + "sha256": "3afb8e36158ca06f20f7555c526835fd62db6a460189de52c0e88eca3f4c41c6", + "role": "weight" + }, + { + "path": "special_tokens_map.json", + "size": 296, + "sha256": "732b4fcb3f140f5afe4849a9fc9cc076b5604192bafbcdef22bb4694b8cfa5ab", + "role": "tokenizer" + }, + { + "path": "tokenizer.json", + "size": 9085657, + "sha256": "1de82e2af146b4ce8a57f05e6a890bd4a453e18b898842bb0d99e5fe7af917b3", + "role": "tokenizer" + }, + { + "path": "tokenizer_config.json", + "size": 54528, + "sha256": "be27ee438dcd533a36a44cf9a74eff034dd1226aab5d765ce5fe2efe7e30f74b", + "role": "tokenizer" + }, + { + "path": "LICENSE.txt", + "size": 7712, + "sha256": "bb0e95fae6937efdbba43f7fb0562ccd0b521b7fcb396a20e8b33a94286933f9", + "role": "license" + }, + { + "path": "README.md", + "size": 41742, + "sha256": "2f0f009369df31606499eb9b862ced426d6ef3b0dc4953a49e4446cfaf89b77c", + "role": "doc" + } + ], + "totalSize": 2480836609, + "versions": [ + { + "version": "1.0.0", + "name": "v1-0-0.meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth", + "cid": "bafkreioovluxxz2mgsituvkognp33nuccuxtsn7kiwa5pvisywiy2rarrh", + "createdAt": "2026-09-15T08:30:00Z" + } + ] + } +} diff --git a/apps/site/test/fixtures/name-qwen.json b/apps/site/test/fixtures/name-qwen.json new file mode 100644 index 0000000..ccfec22 --- /dev/null +++ b/apps/site/test/fixtures/name-qwen.json @@ -0,0 +1,140 @@ +{ + "model": "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth", + "versions": [ + { + "version": "1.0.0", + "name": "v1-0-0.qwen--qwen2-5-7b-instruct.mirrors.enspack.eth", + "cid": "bafkreis5hm6e3jayscwqnqudbrnems5iuqts36dojc5hgq5yuvxl4vg42c", + "createdAt": "2026-09-14T00:00:00Z" + } + ], + "manifest": { + "spec": "enspack/0.1", + "name": "v1-0-0.qwen--qwen2-5-7b-instruct.mirrors.enspack.eth", + "model": "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth", + "publisher": "mirrors.enspack.eth", + "version": "1.0.0", + "createdAt": "2026-09-14T00:00:00Z", + "displayName": "Qwen2.5-7B-Instruct", + "license": "apache-2.0", + "licenseUrl": "https://huggingface.co/Qwen/Qwen2.5-7B-Instruct/blob/a09a35458c702b33eeacc393d103063234e8bc28/LICENSE", + "upstream": { + "provider": "huggingface", + "repo": "Qwen/Qwen2.5-7B-Instruct", + "url": "https://huggingface.co/Qwen/Qwen2.5-7B-Instruct", + "revision": "a09a35458c702b33eeacc393d103063234e8bc28" + }, + "canonical": "qwen2-5-7b-instruct.qwen.enspack.eth", + "distribution": { + "infohash": "136cb94838da83a906263ec23d03dce95d736c64", + "magnet": "magnet:?xt=urn:btih:136cb94838da83a906263ec23d03dce95d736c64&dn=Qwen2.5-7B-Instruct", + "torrent": { + "cid": "bafkreii7g5a56h67x3lapfb5y4hqi6s4zsk54ttcent2g5e54bl3refik3", + "url": "https://seed1.enspack.dev/torrents/qwen--qwen2-5-7b-instruct-v1-0-0.torrent" + }, + "webseeds": [ + "https://huggingface.co/Qwen/Qwen2.5-7B-Instruct/resolve/a09a35458c702b33eeacc393d103063234e8bc28/", + "https://huggingbay.xyz/api/downloads/hf-model-qwen-qwen2-5-7b-instruct/" + ], + "hb": "hb://Qwen/Qwen2.5-7B-Instruct@sha256:d82247b7101aea36fd25bc61273bc1d2627d2cc0cb0fd29cb9586044f4e999fd", + "seeds": [ + "seed1.enspack.eth:6881" + ] + }, + "files": [ + { + "path": ".gitattributes", + "size": 1519, + "sha256": "11ad7efa24975ee4b0c3c3a38ed18737f0658a5f75a0a96787b576a78a023361", + "role": "other" + }, + { + "path": "LICENSE", + "size": 11343, + "sha256": "832dd9e00a68dd83b3c3fb9f5588dad7dcf337a0db50f7d9483f310cd292e92e", + "role": "license" + }, + { + "path": "README.md", + "size": 6240, + "sha256": "f366f33bbf6bcadbb7d87f0a21a7b65584a56b8d58b0743c77c88bee625b93a6", + "role": "doc" + }, + { + "path": "config.json", + "size": 663, + "sha256": "7463bb0ea78315365e6c6b74de4e73bbcc8359dfb0c5a737584e077d42c0b03c", + "role": "config" + }, + { + "path": "generation_config.json", + "size": 243, + "sha256": "3a8f9087e486054c8a4a08dae2e5a3ba62e23da212b5b8c08bc42cb983c3459f", + "role": "config" + }, + { + "path": "merges.txt", + "size": 1671839, + "sha256": "599bab54075088774b1733fde865d5bd747cbcc7a547c5bc12610e874e26f5e3", + "role": "tokenizer" + }, + { + "path": "model-00001-of-00004.safetensors", + "size": 3945441440, + "sha256": "a1333e6293854747c481288ea83b348226af178dd565c49b6f9495ba1966aba7", + "role": "weight" + }, + { + "path": "model-00002-of-00004.safetensors", + "size": 3864726352, + "sha256": "f5d25a2772cb825164a2a2c0fb6d51a87e282abf21e4dd75bc5cfb3cd0ea6185", + "role": "weight" + }, + { + "path": "model-00003-of-00004.safetensors", + "size": 3864726424, + "sha256": "8efdec4c1bc12317ae1a38dc42b595ce777738a64deea3fcb8a0a91381bcdfd5", + "role": "weight" + }, + { + "path": "model-00004-of-00004.safetensors", + "size": 3556377672, + "sha256": "1a72d403cdf0c1ec3cb7f289f17b394a01e64394c2e9b3c0f94dbce3faf879bd", + "role": "weight" + }, + { + "path": "model.safetensors.index.json", + "size": 27752, + "sha256": "624bf7c47cd12468fdc16e38a47cf4f19e0415b859a223ba3c027eed2f0e1028", + "role": "index" + }, + { + "path": "tokenizer.json", + "size": 7031645, + "sha256": "c0382117ea329cdf097041132f6d735924b697924d6f6fc3945713e96ce87539", + "role": "tokenizer" + }, + { + "path": "tokenizer_config.json", + "size": 7305, + "sha256": "5b5d4f65d0acd3b2d56a35b56d374a36cbc1c8fa5cf3b3febbbfabf22f359583", + "role": "tokenizer" + }, + { + "path": "vocab.json", + "size": 2776833, + "sha256": "ca10d7e9fb3ed18575dd1e277a2579c16d108e32f27439684afa0e10b1440910", + "role": "tokenizer" + } + ], + "totalSize": 15242807270, + "versions": [ + { + "version": "1.0.0", + "name": "v1-0-0.qwen--qwen2-5-7b-instruct.mirrors.enspack.eth", + "cid": "bafkreis5hm6e3jayscwqnqudbrnems5iuqts36dojc5hgq5yuvxl4vg42c", + "createdAt": "2026-09-14T00:00:00Z" + } + ] + } +} diff --git a/apps/site/test/fixtures/names.json b/apps/site/test/fixtures/names.json new file mode 100644 index 0000000..787850f --- /dev/null +++ b/apps/site/test/fixtures/names.json @@ -0,0 +1,41 @@ +{ + "items": [ + { + "model": "meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth", + "publisher": "mirrors.enspack.eth", + "latest": { + "name": "v1-0-0.meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth", + "version": "1.0.0", + "cid": "bafkreioovluxxz2mgsituvkognp33nuccuxtsn7kiwa5pvisywiy2rarrh" + }, + "upstream": "meta-llama/Llama-3.2-1B-Instruct", + "license": "llama3.2", + "totalSize": 2480836609 + }, + { + "model": "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth", + "publisher": "mirrors.enspack.eth", + "latest": { + "name": "v1-0-0.qwen--qwen2-5-7b-instruct.mirrors.enspack.eth", + "version": "1.0.0", + "cid": "bafkreis5hm6e3jayscwqnqudbrnems5iuqts36dojc5hgq5yuvxl4vg42c" + }, + "upstream": "Qwen/Qwen2.5-7B-Instruct", + "license": "apache-2.0", + "totalSize": 15242807270 + }, + { + "model": "tiny-random.jeff.enspack.eth", + "publisher": "jeff.enspack.eth", + "latest": { + "name": "v0-2-0.tiny-random.jeff.enspack.eth", + "version": "0.2.0", + "cid": "bafkreiqlqo4tavkcgykxj22n246t6z462d45spaqezclkcxnqw3dryt4sx" + }, + "upstream": "jefflau/tiny-random", + "license": "mit", + "totalSize": 4198657 + } + ], + "nextCursor": null +} diff --git a/apps/site/test/fixtures/publishers.json b/apps/site/test/fixtures/publishers.json new file mode 100644 index 0000000..9e40072 --- /dev/null +++ b/apps/site/test/fixtures/publishers.json @@ -0,0 +1,14 @@ +{ + "items": [ + { + "name": "jeff.enspack.eth", + "hf": "jefflau", + "models": 1 + }, + { + "name": "mirrors.enspack.eth", + "hf": null, + "models": 2 + } + ] +} diff --git a/apps/site/test/fixtures/violations-one.json b/apps/site/test/fixtures/violations-one.json new file mode 100644 index 0000000..9f9dfe2 --- /dev/null +++ b/apps/site/test/fixtures/violations-one.json @@ -0,0 +1,11 @@ +{ + "items": [ + { + "name": "v0-1-0.tiny-random.jeff.enspack.eth", + "node": "0xe06ee3b0143f362de535730222149d6448db717552464f2cb79cf5e145bd9162", + "previousCid": "bafkreiq2jzse7l6h3bz37newkvc5qkjog6b6slmbt7smopt333icaq4ipl", + "newCid": "bafkreiz7kc52q2kk5jepgmkk3grbtroft46lfgvwqelbrfpkp4vgqm4kit", + "block": 9123456 + } + ] +} diff --git a/apps/site/test/fixtures/violations.json b/apps/site/test/fixtures/violations.json new file mode 100644 index 0000000..2feb210 --- /dev/null +++ b/apps/site/test/fixtures/violations.json @@ -0,0 +1,3 @@ +{ + "items": [] +} diff --git a/apps/site/test/get-started.test.tsx b/apps/site/test/get-started.test.tsx new file mode 100644 index 0000000..ac081d9 --- /dev/null +++ b/apps/site/test/get-started.test.tsx @@ -0,0 +1,21 @@ +import { screen, waitFor } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { config } from "../src/lib/config.js"; +import { GetStartedPage } from "../src/pages/get-started.js"; +import { renderAt } from "./helpers.js"; + +describe("GetStartedPage", () => { + it("renders the registrar CTA and the enspack get command", async () => { + renderAt("/get-started", ); + + const cta = screen.getByRole("link", { name: /Claim \.enspack\.eth/ }); + expect(cta.getAttribute("href")?.startsWith(config.registrarUrl)).toBe(true); + expect(cta).toHaveClass("btn-primary"); + expect( + screen.getByText("enspack get qwen--qwen2-5-7b-instruct.mirrors.enspack.eth"), + ).toBeInTheDocument(); + await waitFor(() => { + expect(document.title).toBe("Get started · enspack"); + }); + }); +}); diff --git a/apps/site/test/helpers.tsx b/apps/site/test/helpers.tsx new file mode 100644 index 0000000..0555cc0 --- /dev/null +++ b/apps/site/test/helpers.tsx @@ -0,0 +1,30 @@ +import { type RenderResult, render } from "@testing-library/react"; +import type { ReactNode } from "react"; +import { MemoryRouter, Route, Routes } from "react-router"; +import { demoFixtures } from "../src/demo/fixtures.js"; +import { ClientProvider } from "../src/lib/client-context.js"; +import { FixtureIndexClient, type IndexClient } from "../src/lib/client.js"; + +export { demoFixtures }; + +export function fixtureClient(): IndexClient { + return new FixtureIndexClient(demoFixtures); +} + +/** Renders `ui` under a router at `path`, with the fixture client (or a custom one). */ +export function renderAt( + path: string, + ui: ReactNode, + opts: { client?: IndexClient; routePattern?: string } = {}, +): RenderResult { + const client = opts.client ?? fixtureClient(); + return render( + + + + + + + , + ); +} diff --git a/apps/site/test/home.test.tsx b/apps/site/test/home.test.tsx new file mode 100644 index 0000000..e8a456f --- /dev/null +++ b/apps/site/test/home.test.tsx @@ -0,0 +1,97 @@ +import { cleanup, fireEvent, waitFor } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; +import { FixtureIndexClient, type IndexClient } from "../src/lib/client.js"; +import { HomePage } from "../src/pages/home.js"; +import { renderAt } from "./helpers.js"; + +afterEach(() => { + cleanup(); +}); + +const QWEN = "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth"; +const LLAMA = "meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth"; +const JEFF = "tiny-random.jeff.enspack.eth"; + +function rejectingClient(): IndexClient { + const boom = () => Promise.reject(new Error("indexer down")); + return { + listNames: boom, + getName: boom, + listPublishers: boom, + listViolations: boom, + getAttestation: boom, + }; +} + +describe("HomePage", () => { + it("renders three models from fixtures", async () => { + const { findByText, getByText, getByLabelText } = renderAt("/", ); + expect(await findByText(QWEN)).toBeInTheDocument(); + expect(getByText(LLAMA)).toBeInTheDocument(); + expect(getByText(JEFF)).toBeInTheDocument(); + const stats = getByLabelText("Catalog stats"); + expect(stats).toHaveTextContent("3"); + expect(stats).toHaveTextContent("indexed models"); + await waitFor(() => { + expect(document.title).toBe("enspack"); + }); + }); + + it("narrows the list to one model when searching Qwen", async () => { + const { findByText, getByLabelText, getByRole, getByText, queryByText } = renderAt( + "/", + , + ); + await findByText(QWEN); + fireEvent.change(getByLabelText("Search"), { target: { value: "Qwen" } }); + await waitFor(() => { + expect(queryByText(JEFF)).not.toBeInTheDocument(); + expect(queryByText("meta-llama/llama-3-2-1b-instruct")).not.toBeInTheDocument(); + }); + expect(queryByText(QWEN)).toBeInTheDocument(); + expect(getByText(`enspack get ${LLAMA}`)).toBeInTheDocument(); + const stats = getByLabelText("Catalog stats"); + expect(stats).toHaveTextContent("3"); + expect(stats).toHaveTextContent("indexed models"); + expect(getByRole("button", { name: "Clear search" })).toBeInTheDocument(); + fireEvent.click(getByRole("button", { name: "Clear search" })); + await waitFor(() => { + expect(getByText("meta-llama/llama-3-2-1b-instruct")).toBeInTheDocument(); + }); + expect(queryByText(JEFF)).toBeInTheDocument(); + }); + + it("filters by publisher chip", async () => { + const { findByRole, getByText, getByLabelText, queryByText } = renderAt("/", ); + const chip = await findByRole("button", { name: "mirrors.enspack.eth" }); + fireEvent.click(chip); + await waitFor(() => { + expect(queryByText(JEFF)).not.toBeInTheDocument(); + }); + expect(getByText(QWEN)).toBeInTheDocument(); + expect(getByText(LLAMA)).toBeInTheDocument(); + const stats = getByLabelText("Catalog stats"); + expect(stats).toHaveTextContent("3"); + expect(stats).toHaveTextContent("indexed models"); + }); + + it("renders the empty state when the index has no names", async () => { + const client = new FixtureIndexClient({ + names: { items: [], nextCursor: null }, + details: {}, + publishers: { items: [] }, + violations: { items: [] }, + attestations: {}, + }); + const { findByText } = renderAt("/", , { client }); + expect(await findByText("No names indexed yet on sepolia.")).toBeInTheDocument(); + expect(await findByText(/enspack publish --from-hf/)).toBeInTheDocument(); + expect(await findByText(`enspack get ${QWEN}`)).toBeInTheDocument(); + }); + + it("renders the error state when the client rejects", async () => { + const { findByRole } = renderAt("/", , { client: rejectingClient() }); + const alert = await findByRole("alert"); + expect(alert).toHaveTextContent("Could not load models"); + }); +}); diff --git a/apps/site/test/layout.test.tsx b/apps/site/test/layout.test.tsx new file mode 100644 index 0000000..c009ff4 --- /dev/null +++ b/apps/site/test/layout.test.tsx @@ -0,0 +1,69 @@ +import { cleanup, fireEvent, render, screen, within } from "@testing-library/react"; +import type { ReactNode } from "react"; +import { MemoryRouter } from "react-router"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { Layout } from "../src/components/layout.js"; +import { ClientProvider } from "../src/lib/client-context.js"; +import { fixtureClient } from "./helpers.js"; + +function renderLayout(ui: ReactNode =

page

) { + return render( + + + {ui} + + , + ); +} + +describe("Layout", () => { + beforeEach(() => { + sessionStorage.clear(); + }); + + afterEach(() => { + cleanup(); + sessionStorage.clear(); + }); + + it("renders the Sepolia banner and hides it after dismiss", () => { + const { unmount } = renderLayout(); + expect( + screen.getByText("Showing Sepolia testnet data. Mainnet follows once the v1 path is live."), + ).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: "Dismiss" })); + expect( + screen.queryByText("Showing Sepolia testnet data. Mainnet follows once the v1 path is live."), + ).not.toBeInTheDocument(); + + unmount(); + renderLayout(); + expect( + screen.queryByText("Showing Sepolia testnet data. Mainnet follows once the v1 path is live."), + ).not.toBeInTheDocument(); + }); + + it("nav links have the catalog hrefs", () => { + renderLayout(); + const nav = screen.getByRole("navigation", { name: "Main" }); + expect(within(nav).getByRole("link", { name: "Models" })).toHaveAttribute("href", "/"); + expect(within(nav).getByRole("link", { name: "Violations" })).toHaveAttribute( + "href", + "/violations", + ); + expect(within(nav).getByRole("link", { name: "Get started" })).toHaveAttribute( + "href", + "/get-started", + ); + expect(within(nav).getByRole("link", { name: "GitHub" })).toHaveAttribute( + "href", + "https://github.com/jefflau/enspack", + ); + }); + + it("footer shows enspack/0.1", () => { + renderLayout(); + expect(screen.getByText("enspack/0.1")).toBeInTheDocument(); + }); +}); diff --git a/apps/site/test/lib.test.ts b/apps/site/test/lib.test.ts new file mode 100644 index 0000000..7124bb6 --- /dev/null +++ b/apps/site/test/lib.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it, vi } from "vitest"; +import { ApiError } from "../src/lib/api.js"; +import { FixtureIndexClient, HttpIndexClient } from "../src/lib/client.js"; +import { readConfig } from "../src/lib/config.js"; +import { ensAppUrl, firstLabel, formatBytes, shortHex } from "../src/lib/format.js"; +import { demoFixtures } from "./helpers.js"; + +describe("config", () => { + it("defaults to sepolia + http and strips trailing slashes", () => { + const c = readConfig({ VITE_INDEX_URL: "https://idx.example/" } as ImportMetaEnv); + expect(c.chain).toBe("sepolia"); + expect(c.dataSource).toBe("http"); + expect(c.indexUrl).toBe("https://idx.example"); + }); + + it("honours mainnet + demo", () => { + const c = readConfig({ VITE_CHAIN: "mainnet", VITE_DATA_SOURCE: "demo" } as ImportMetaEnv); + expect(c.chain).toBe("mainnet"); + expect(c.dataSource).toBe("demo"); + }); +}); + +describe("format", () => { + it("formats bytes in decimal units", () => { + expect(formatBytes(663)).toBe("663 B"); + expect(formatBytes(15242807270)).toBe("15.2 GB"); + expect(formatBytes(2471645608)).toBe("2.47 GB"); + }); + it("shortens hex and keeps short values intact", () => { + expect(shortHex("0x1234567890abcdef")).toBe("0x1234…cdef"); + expect(shortHex("abc")).toBe("abc"); + }); + it("derives labels and ENS app urls per chain", () => { + expect(firstLabel("jeff.enspack.eth")).toBe("jeff"); + expect(ensAppUrl("sepolia", "x.eth")).toBe("https://sepolia.app.ens.domains/x.eth"); + expect(ensAppUrl("mainnet", "x.eth")).toBe("https://app.ens.domains/x.eth"); + }); +}); + +describe("FixtureIndexClient", () => { + const client = new FixtureIndexClient(demoFixtures); + + it("lists, filters by publisher and searches by display name", async () => { + expect((await client.listNames()).items).toHaveLength(3); + const mirrors = await client.listNames({ publisher: "mirrors.enspack.eth" }); + expect(mirrors.items.map((i) => i.publisher)).toEqual([ + "mirrors.enspack.eth", + "mirrors.enspack.eth", + ]); + const qwen = await client.listNames({ q: "Qwen2.5" }); + expect(qwen.items).toHaveLength(1); + }); + + it("resolves a version name to its model detail and 404s unknown names", async () => { + const d = await client.getName("v0-1-0.tiny-random.jeff.enspack.eth"); + expect(d.model).toBe("tiny-random.jeff.enspack.eth"); + await expect(client.getName("nope.eth")).rejects.toMatchObject({ status: 404 }); + }); + + it("returns null attestation for non-registrar publishers", async () => { + expect(await client.getAttestation("mirrors")).toBeNull(); + expect(await client.getAttestation("jeff")).not.toBeNull(); + }); +}); + +describe("HttpIndexClient", () => { + it("builds query strings and surfaces indexer error codes", async () => { + const calls: string[] = []; + const fetchImpl = vi.fn(async (input: RequestInfo | URL) => { + calls.push(String(input)); + return new Response(JSON.stringify({ error: "invalid cursor", code: "INVALID" }), { + status: 400, + headers: { "content-type": "application/json" }, + }); + }) as unknown as typeof fetch; + const client = new HttpIndexClient("https://idx", "https://reg", fetchImpl); + const err = await client.listNames({ q: "a b", limit: 5 }).catch((e: unknown) => e); + expect(err).toBeInstanceOf(ApiError); + expect((err as ApiError).code).toBe("INVALID"); + expect(calls[0]).toBe("https://idx/v1/names?q=a+b&limit=5"); + }); + + it("maps registrar 404 to null and network failure to NETWORK", async () => { + const notFound = (async () => new Response("{}", { status: 404 })) as unknown as typeof fetch; + const client = new HttpIndexClient("https://idx", "https://reg", notFound); + expect(await client.getAttestation("x")).toBeNull(); + const down = (async () => { + throw new TypeError("Failed to fetch"); + }) as unknown as typeof fetch; + const c2 = new HttpIndexClient("https://idx", "https://reg", down); + await expect(c2.listPublishers()).rejects.toMatchObject({ status: 0, code: "NETWORK" }); + }); +}); diff --git a/apps/site/test/model.test.tsx b/apps/site/test/model.test.tsx new file mode 100644 index 0000000..3f95ff8 --- /dev/null +++ b/apps/site/test/model.test.tsx @@ -0,0 +1,139 @@ +import { cleanup, fireEvent, screen, waitFor, within } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; +import { ensAppUrl } from "../src/lib/format.js"; +import { ModelPage } from "../src/pages/model.js"; +import { renderAt } from "./helpers.js"; + +afterEach(() => { + cleanup(); +}); + +const QWEN = "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth"; +const QWEN_VERSION = "v1-0-0.qwen--qwen2-5-7b-instruct.mirrors.enspack.eth"; +const JEFF = "tiny-random.jeff.enspack.eth"; +const JEFF_OLD = "v0-1-0.tiny-random.jeff.enspack.eth"; + +const QWEN_PATHS = [ + ".gitattributes", + "LICENSE", + "README.md", + "config.json", + "generation_config.json", + "merges.txt", + "model-00001-of-00004.safetensors", + "model-00002-of-00004.safetensors", + "model-00003-of-00004.safetensors", + "model-00004-of-00004.safetensors", + "model.safetensors.index.json", + "tokenizer.json", + "tokenizer_config.json", + "vocab.json", +] as const; + +const QWEN_SHA = "11ad7efa24975ee4b0c3c3a38ed18737f0658a5f75a0a96787b576a78a023361"; +const QWEN_INFOHASH = "136cb94838da83a906263ec23d03dce95d736c64"; +const QWEN_MAGNET = + "magnet:?xt=urn:btih:136cb94838da83a906263ec23d03dce95d736c64&dn=Qwen2.5-7B-Instruct"; +const QWEN_WEBSEEDS = [ + "https://huggingface.co/Qwen/Qwen2.5-7B-Instruct/resolve/a09a35458c702b33eeacc393d103063234e8bc28/", + "https://huggingbay.xyz/api/downloads/hf-model-qwen-qwen2-5-7b-instruct/", +] as const; + +function renderName(name: string) { + return renderAt(`/name/${encodeURIComponent(name)}`, , { + routePattern: "/name/:name", + }); +} + +function filePaths(): string[] { + const table = screen.getByRole("table", { name: "Files" }); + const tbody = table.querySelector("tbody"); + if (tbody === null) throw new Error("expected files tbody"); + return [...tbody.querySelectorAll("tr")].map((row) => { + const path = within(row).getAllByRole("cell")[0]?.textContent?.trim() ?? ""; + return path; + }); +} + +describe("ModelPage", () => { + it("renders the Qwen trust chain, files, and lock entry", async () => { + renderName(QWEN); + + expect(await screen.findByRole("heading", { name: "Qwen2.5-7B-Instruct" })).toBeInTheDocument(); + expect( + screen.getByText( + "Indexed from sepolia. Verification shown here is the indexer's; the CLI re-verifies every file locally.", + ), + ).toBeInTheDocument(); + + for (const path of QWEN_PATHS) { + expect(screen.getByText(path)).toBeInTheDocument(); + } + expect(QWEN_PATHS).toHaveLength(14); + expect(screen.getByText(QWEN_SHA)).toBeInTheDocument(); + expect(screen.getByText(QWEN_INFOHASH)).toBeInTheDocument(); + + const magnet = document.querySelector(`a[href="${QWEN_MAGNET}"]`); + expect(magnet).not.toBeNull(); + + for (const url of QWEN_WEBSEEDS) { + expect(screen.getByRole("link", { name: url })).toHaveAttribute("href", url); + } + + const ensHref = ensAppUrl("sepolia", QWEN_VERSION); + const ensLink = document.querySelector(`a[href="${ensHref}"]`); + expect(ensLink).not.toBeNull(); + + const lockPre = screen.getByText(/"lockfileVersion": 1/); + expect(lockPre).toBeInTheDocument(); + const lockDetails = lockPre.closest("details"); + expect(lockDetails).toBeInstanceOf(HTMLDetailsElement); + expect(lockDetails?.open).toBe(false); + expect(screen.getByText("enspack.lock entry")).toBeInTheDocument(); + await waitFor(() => { + expect(document.title).toBe("Qwen2.5-7B-Instruct · enspack"); + }); + }); + + it("marks Jeff's latest version and links the older version name", async () => { + renderName(JEFF); + expect(await screen.findByRole("heading", { name: "tiny-random" })).toBeInTheDocument(); + + const versions = screen.getByRole("table", { name: "Versions" }); + const latestRow = versions.querySelector("tr[data-latest='true']"); + expect(latestRow).toBeInstanceOf(HTMLTableRowElement); + if (!(latestRow instanceof HTMLTableRowElement)) throw new Error("expected latest row"); + expect(within(latestRow).getByText("latest")).toBeInTheDocument(); + expect(within(latestRow).getByText("0.2.0")).toBeInTheDocument(); + + expect(screen.getByRole("link", { name: JEFF_OLD })).toHaveAttribute( + "href", + `/name/${encodeURIComponent(JEFF_OLD)}`, + ); + }); + + it("resolves a version-name URL to the model", async () => { + renderName(JEFF_OLD); + expect(await screen.findByRole("heading", { name: "tiny-random" })).toBeInTheDocument(); + expect(screen.getAllByText(JEFF).length).toBeGreaterThan(0); + }); + + it("shows the unknown-name state", async () => { + renderName("nope.eth"); + expect(await screen.findByText("Unknown name")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "Home" })).toHaveAttribute("href", "/"); + }); + + it("reorders file rows when sorting by size", async () => { + renderName(QWEN); + await screen.findByRole("heading", { name: "Qwen2.5-7B-Instruct" }); + + const before = filePaths(); + expect(before[0]).toBe(".gitattributes"); + + fireEvent.click(screen.getByRole("button", { name: "size" })); + const after = filePaths(); + expect(after).not.toEqual(before); + expect(after[0]).toBe("generation_config.json"); + }); +}); diff --git a/apps/site/test/publisher.test.tsx b/apps/site/test/publisher.test.tsx new file mode 100644 index 0000000..68e1b38 --- /dev/null +++ b/apps/site/test/publisher.test.tsx @@ -0,0 +1,55 @@ +import { cleanup, screen, waitFor } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; +import { PublisherPage } from "../src/pages/publisher.js"; +import { renderAt } from "./helpers.js"; + +const route = { routePattern: "/publisher/:label" }; + +afterEach(() => { + cleanup(); +}); + +describe("PublisherPage", () => { + it("shows the no-attestation sentence and two models for mirrors", async () => { + renderAt("/publisher/mirrors.enspack.eth", , route); + + expect( + await screen.findByText(/No registrar attestation\. This name is not issued through/), + ).toBeInTheDocument(); + expect( + screen.getByRole("link", { name: "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth" }), + ).toHaveAttribute("href", "/name/qwen--qwen2-5-7b-instruct.mirrors.enspack.eth"); + expect( + screen.getByRole("link", { + name: "meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth", + }), + ).toHaveAttribute("href", "/name/meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth"); + }); + + it("shows the HF namespace, verified badge, and attestation table for jeff", async () => { + renderAt("/publisher/jeff.enspack.eth", , route); + + const hf = await screen.findByRole("link", { name: "jefflau" }); + expect(hf).toHaveAttribute("href", "https://huggingface.co/jefflau"); + expect(screen.getByText("HF verified")).toBeInTheDocument(); + expect(screen.getByText("jefflau/enspack-verify")).toBeInTheDocument(); + + const txLinks = screen + .getAllByRole("link") + .filter((el) => el.getAttribute("href")?.includes("etherscan.io/tx/")); + expect(txLinks.length).toBeGreaterThan(0); + expect( + screen.getByText( + "0xd368b0e8bbbd4a474a846a5fbfab4d8cfc1b3f8f413d25dbce5605f3fd38bbc260af54755aae952ac69e62de639b238540c593d6d3e6e9cc992768ff150fdc987f", + ), + ).toBeInTheDocument(); + await waitFor(() => { + expect(document.title).toBe("jeff.enspack.eth · enspack"); + }); + }); + + it("shows the unknown-publisher empty state", async () => { + renderAt("/publisher/nope.eth", , route); + expect(await screen.findByText("Unknown publisher")).toBeInTheDocument(); + }); +}); diff --git a/apps/site/test/setup.ts b/apps/site/test/setup.ts new file mode 100644 index 0000000..f149f27 --- /dev/null +++ b/apps/site/test/setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/apps/site/test/violations.test.tsx b/apps/site/test/violations.test.tsx new file mode 100644 index 0000000..8fb0c83 --- /dev/null +++ b/apps/site/test/violations.test.tsx @@ -0,0 +1,43 @@ +import { screen, waitFor } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import type { ViolationsPage } from "../src/lib/api.js"; +import { FixtureIndexClient } from "../src/lib/client.js"; +import { ViolationsPage as ViolationsPageView } from "../src/pages/violations.js"; +import violationsOne from "./fixtures/violations-one.json"; +import { demoFixtures, renderAt } from "./helpers.js"; + +describe("ViolationsPage", () => { + it("shows the empty-state sentence when there are no violations", async () => { + renderAt("/violations", ); + expect( + await screen.findByText( + "No violations. No version name has changed its contenthash after it was first set.", + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + "A row would show the version name, the previous CID → new CID, and the block of the change.", + ), + ).toBeInTheDocument(); + await waitFor(() => { + expect(document.title).toBe("Violations · enspack"); + }); + }); + + it("renders a violation row with a name link and a sepolia block explorer link", async () => { + const client = new FixtureIndexClient({ + ...demoFixtures, + violations: violationsOne as ViolationsPage, + }); + renderAt("/violations", , { client }); + + const nameLink = await screen.findByRole("link", { + name: "v0-1-0.tiny-random.jeff.enspack.eth", + }); + expect(nameLink).toHaveAttribute("href", "/name/v0-1-0.tiny-random.jeff.enspack.eth"); + expect(screen.getByRole("link", { name: "9123456" })).toHaveAttribute( + "href", + "https://sepolia.etherscan.io/block/9123456", + ); + }); +}); diff --git a/apps/site/tsconfig.json b/apps/site/tsconfig.json new file mode 100644 index 0000000..6b1e48c --- /dev/null +++ b/apps/site/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "noEmit": true, + "declaration": false, + "types": ["vite/client", "@testing-library/jest-dom"] + }, + "include": ["src", "test", "vite.config.ts", "vitest.config.ts"] +} diff --git a/apps/site/vite.config.ts b/apps/site/vite.config.ts new file mode 100644 index 0000000..2911915 --- /dev/null +++ b/apps/site/vite.config.ts @@ -0,0 +1,7 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], + build: { target: "es2022", sourcemap: true }, +}); diff --git a/apps/site/vitest.config.ts b/apps/site/vitest.config.ts new file mode 100644 index 0000000..4452601 --- /dev/null +++ b/apps/site/vitest.config.ts @@ -0,0 +1,11 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [react()], + test: { + environment: "jsdom", + setupFiles: ["./test/setup.ts"], + include: ["test/**/*.test.{ts,tsx}"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 131428d..2415964 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,50 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) + + apps/site: + dependencies: + react: + specifier: ^19.1.0 + version: 19.3.0 + react-dom: + specifier: ^19.1.0 + version: 19.3.0(react@19.3.0) + react-router: + specifier: ^7.6.0 + version: 7.18.3(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + devDependencies: + '@enspack/core': + specifier: workspace:* + version: link:../../packages/core + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.10.0(@testing-library/dom@10.4.2) + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@types/react': + specifier: ^19.1.0 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.1.0 + version: 19.3.0(@types/react@19.3.0) + '@vitejs/plugin-react': + specifier: ^4.4.0 + version: 4.7.0(vite@6.4.3(@types/node@22.20.2)(yaml@2.9.1)) + jsdom: + specifier: ^26.1.0 + version: 26.1.0 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vite: + specifier: ^6.3.0 + version: 6.4.3(@types/node@22.20.2)(yaml@2.9.1) + vitest: + specifier: ^3.0.0 + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) bootstrap: dependencies: @@ -56,7 +99,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) infra: devDependencies: @@ -68,7 +111,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) yaml: specifier: ^2.7.0 version: 2.9.1 @@ -102,7 +145,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) packages/core: dependencies: @@ -145,7 +188,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) packages/hf: dependencies: @@ -164,7 +207,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) packages/torrent: dependencies: @@ -198,7 +241,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) services/indexer: dependencies: @@ -223,7 +266,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) services/registrar: dependencies: @@ -263,7 +306,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) services/seed: dependencies: @@ -291,7 +334,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) yaml: specifier: ^2.7.0 version: 2.9.1 @@ -319,10 +362,13 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) packages: + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + '@adraffy/ens-normalize@1.11.1': resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} @@ -330,14 +376,96 @@ packages: resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} engines: {node: '>= 16'} + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + '@biomejs/biome@1.9.4': resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} engines: {node: '>=14.21.3'} @@ -396,6 +524,34 @@ packages: peerDependencies: commander: ~12.1.0 + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@electric-sql/pglite@0.2.13': resolution: {integrity: sha512-YRY806NnScVqa21/1L1vaysSQ+0/cAva50z7vlwzaGiBOTS9JhdzIRHN0KfgMhobFAphbznZJ7urMso4RtMBIQ==} @@ -424,14 +580,14 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.28.2': - resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -442,14 +598,14 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.28.2': - resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -460,14 +616,14 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-arm@0.28.2': - resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -478,14 +634,14 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/android-x64@0.28.2': - resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -496,14 +652,14 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.28.2': - resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -514,14 +670,14 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.28.2': - resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -532,14 +688,14 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.28.2': - resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -550,14 +706,14 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.28.2': - resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -568,14 +724,14 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.28.2': - resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -586,14 +742,14 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.28.2': - resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -604,14 +760,14 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.28.2': - resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -622,14 +778,14 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.28.2': - resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -640,14 +796,14 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.28.2': - resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -658,14 +814,14 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.28.2': - resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -676,14 +832,14 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.28.2': - resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -694,14 +850,14 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.28.2': - resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -712,26 +868,26 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.28.2': - resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.28.2': - resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -742,26 +898,26 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.28.2': - resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.28.2': - resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -772,26 +928,26 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.28.2': - resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.28.2': - resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -802,14 +958,14 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.28.2': - resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -820,14 +976,14 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.28.2': - resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -838,14 +994,14 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.28.2': - resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -856,14 +1012,14 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.28.2': - resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -962,6 +1118,9 @@ packages: '@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'} @@ -1016,6 +1175,9 @@ packages: '@repeaterjs/repeater@3.1.0': resolution: {integrity: sha512-TaoVksZRSx2KWYYpyLQtMQXXeS98VsgZImzW65xmiVgbYhXLk+aEsmzPLirqVuE4/XuUapH2iMtxUzaBNDzdSQ==} + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rollup/rollup-android-arm-eabi@4.63.2': resolution: {integrity: sha512-Xa6RDoWa+hNiX6PgsljlH6W75RaONx3y6PVlbLhkEWW+GaPQ3dP5gwbL/erAzQHWwkvW5UxdD5l87Qx2FAQ/4A==} cpu: [arm] @@ -1150,10 +1312,51 @@ packages: '@scure/bip39@1.6.0': resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + '@testing-library/dom@10.4.2': + resolution: {integrity: sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.10.0': + resolution: {integrity: sha512-HQwu0KaB2zyT0iLzBL+8CLyZDL3KlZlZJ+2iyc9uCUnlJVskJU/UlPuVCyIPhtukjPQdT2QNoR5nCP5FqTmmDQ==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + deprecated: Incorrect minor release with breaking changes (Node >=22 and required @testing-library/dom peer). Use 6.9.1 for the 6.x line, or upgrade to 7.0.0. + peerDependencies: + '@testing-library/dom': '>=10 <11' + + '@testing-library/react@16.3.3': + resolution: {integrity: sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@thaunknown/thirty-two@1.0.6': resolution: {integrity: sha512-Uz2kFb0cNeHg7hds8BtI/umcwzaSMUdbkeQgZrIZQbp3gWRFjrvrvBHNEB6SQYChVxC3rWHovKx1Ya/vSRRMYA==} engines: {node: '>=0.2.6'} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1178,6 +1381,20 @@ packages: '@types/picomatch@3.0.2': resolution: {integrity: sha512-n0i8TD3UDB7paoMMxA3Y65vUncFJXjcUf7lQY7YyKGl6031FNjfsLs6pdLFCy2GNFxItPJG8GvvpbZc2skH7WA==} + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/expect@3.2.7': resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} @@ -1262,6 +1479,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1297,6 +1518,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -1307,6 +1532,13 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1324,6 +1556,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.11.23: + resolution: {integrity: sha512-le521dGVfxM7yRX0EikCoSz+rOK+hHzdDt/E7mG1jOJB/6WAAUuwVroLwaB7ApaUsz5Q0kFlDXLSA9MheUIfRQ==} + engines: {node: '>=6.0.0'} + hasBin: true + bencode@4.0.1: resolution: {integrity: sha512-/bCR3FvgfB8AZk4LlowShU+nfaTNRM2vruJ9aejDa5LpgkCoRjFyEauFiZFxbpxVxQbBFapYCN42Er2dkE/Qgw==} engines: {node: '>=12.20.0'} @@ -1341,6 +1578,11 @@ packages: brace-expansion@2.1.4: resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + browserslist@4.28.9: + resolution: {integrity: sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -1354,6 +1596,9 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1392,6 +1637,13 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + copy-anything@4.1.0: resolution: {integrity: sha512-ufbM3smX/Jbnpk5wcQjzd1MgBpzmqfNETUAyZNrGwU9foRlyHoGzMMBBCRzEhQLBjZfFDE1W2ufPXX2vdWkV8Q==} engines: {node: '>=18'} @@ -1412,10 +1664,24 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + dataloader@2.2.3: resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==} @@ -1432,6 +1698,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -1440,10 +1709,20 @@ packages: resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} engines: {node: '>=10'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-package-manager@3.0.2: resolution: {integrity: sha512-8JFjJHutStYrfWwzfretQoyNGoZVW1Fsrp4JO9spa7h/fBfwgTMEIy4/LBzRDGsxwVPHU0q+T9YvwLDJoOApLQ==} engines: {node: '>=12'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dot-prop@8.0.2: resolution: {integrity: sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ==} engines: {node: '>=16'} @@ -1544,12 +1823,19 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + electron-to-chromium@1.5.427: + resolution: {integrity: sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + env-paths@3.0.0: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1566,16 +1852,20 @@ packages: engines: {node: '>=12'} hasBin: true - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true - esbuild@0.28.2: - resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1648,6 +1938,10 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-stdin@10.0.0: resolution: {integrity: sha512-eWSePJ4zXFdqz+/Lyfopob4rIcoF/U2XfE8nJc7iZV6lnebWc9k7DoQQpX+2a9jc0AOvBsXvbe5YkjXl/MHbpg==} engines: {node: '>=20'} @@ -1682,17 +1976,37 @@ packages: resolution: {integrity: sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==} engines: {node: '>=16.9.0'} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + http-terminator@3.2.0: resolution: {integrity: sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g==} engines: {node: '>=14'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + ipfs-unixfs@13.1.0: resolution: {integrity: sha512-gT6c9ii/OiPDs1l+7VZDFe/sTNWLbGCUSd9FaZGCHNnO48xkyTTOMl3+K83+qhc/os1DCYKNkf36db5XeG4VFQ==} @@ -1711,6 +2025,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -1746,6 +2063,20 @@ packages: resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-schema-to-typescript@15.0.4: resolution: {integrity: sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==} engines: {node: '>=16.0.0'} @@ -1757,6 +2088,11 @@ packages: json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + junk@4.0.1: resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==} engines: {node: '>=12.20'} @@ -1785,6 +2121,13 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1803,6 +2146,10 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimatch@9.0.9: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} @@ -1843,10 +2190,17 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-releases@2.0.55: + resolution: {integrity: sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==} + engines: {node: '>=18'} + npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + nwsapi@2.2.27: + resolution: {integrity: sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1893,6 +2247,9 @@ packages: engines: {node: '>=12.20.0'} hasBin: true + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -2033,6 +2390,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + process-warning@3.0.0: resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} @@ -2048,6 +2409,10 @@ packages: protons-runtime@7.1.0: resolution: {integrity: sha512-TAL8CpKEUK+k0KKZIQrDTBgyH86zZAjz3ZULcCjxrb9zvkTVEGJyVbATmTV4KpZmFFEH7ERWDNWxfiE+N3bOUQ==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} @@ -2057,6 +2422,32 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + react-dom@19.3.0: + resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==} + peerDependencies: + react: ^19.3.0 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-router@7.18.3: + resolution: {integrity: sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.3.0: + resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} + engines: {node: '>=0.10.0'} + readable-stream@4.7.0: resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2069,6 +2460,10 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2086,6 +2481,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2096,14 +2494,31 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + semver-compare@1.0.0: resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2170,6 +2585,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} @@ -2188,6 +2607,9 @@ packages: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tdigest@0.1.3: resolution: {integrity: sha512-zbRt+lT+/H4fRItHshczHErVCQnitJk8MfMT24MqFJf3YL7SJJPqGIGeuOdvxXxM/AHFzKBl7WoyaYwqO9s3Kw==} @@ -2227,6 +2649,21 @@ packages: resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} engines: {node: '>=14.0.0'} + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -2306,6 +2743,12 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + update-browserslist-db@1.3.3: + resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + urlpattern-polyfill@10.1.0: resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} @@ -2366,19 +2809,19 @@ packages: terser: optional: true - vite@7.3.6: - resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} - engines: {node: ^20.19.0 || >=22.12.0} + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 jiti: '>=1.21.0' - less: ^4.0.0 + less: '*' lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 @@ -2434,10 +2877,31 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + when-exit@2.1.5: resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} @@ -2474,10 +2938,20 @@ packages: utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.9.1: resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} engines: {node: '>= 14.6'} @@ -2485,6 +2959,8 @@ packages: snapshots: + '@adobe/css-tools@4.5.0': {} + '@adraffy/ens-normalize@1.11.1': {} '@apidevtools/json-schema-ref-parser@11.9.3': @@ -2493,14 +2969,128 @@ snapshots: '@types/json-schema': 7.0.15 js-yaml: 4.3.2 + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.9 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@biomejs/biome@1.9.4': optionalDependencies: '@biomejs/cli-darwin-arm64': 1.9.4 @@ -2540,6 +3130,26 @@ snapshots: dependencies: commander: 12.1.0 + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + '@electric-sql/pglite@0.2.13': {} '@electric-sql/pglite@0.3.16': {} @@ -2569,226 +3179,226 @@ snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/aix-ppc64@0.27.7': + '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/aix-ppc64@0.28.2': + '@esbuild/aix-ppc64@0.27.7': optional: true '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm64@0.27.7': + '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm64@0.28.2': + '@esbuild/android-arm64@0.27.7': optional: true '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/android-arm@0.27.7': + '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-arm@0.28.2': + '@esbuild/android-arm@0.27.7': optional: true '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/android-x64@0.27.7': + '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/android-x64@0.28.2': + '@esbuild/android-x64@0.27.7': optional: true '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.27.7': + '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.28.2': + '@esbuild/darwin-arm64@0.27.7': optional: true '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/darwin-x64@0.27.7': + '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/darwin-x64@0.28.2': + '@esbuild/darwin-x64@0.27.7': optional: true '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.27.7': + '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.28.2': + '@esbuild/freebsd-arm64@0.27.7': optional: true '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.27.7': + '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.28.2': + '@esbuild/freebsd-x64@0.27.7': optional: true '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm64@0.27.7': + '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm64@0.28.2': + '@esbuild/linux-arm64@0.27.7': optional: true '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-arm@0.27.7': + '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-arm@0.28.2': + '@esbuild/linux-arm@0.27.7': optional: true '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-ia32@0.27.7': + '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-ia32@0.28.2': + '@esbuild/linux-ia32@0.27.7': optional: true '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-loong64@0.27.7': + '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-loong64@0.28.2': + '@esbuild/linux-loong64@0.27.7': optional: true '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-mips64el@0.27.7': + '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-mips64el@0.28.2': + '@esbuild/linux-mips64el@0.27.7': optional: true '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-ppc64@0.27.7': + '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-ppc64@0.28.2': + '@esbuild/linux-ppc64@0.27.7': optional: true '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.27.7': + '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.28.2': + '@esbuild/linux-riscv64@0.27.7': optional: true '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-s390x@0.27.7': + '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-s390x@0.28.2': + '@esbuild/linux-s390x@0.27.7': optional: true '@esbuild/linux-x64@0.21.5': optional: true + '@esbuild/linux-x64@0.25.12': + optional: true + '@esbuild/linux-x64@0.27.7': optional: true - '@esbuild/linux-x64@0.28.2': + '@esbuild/netbsd-arm64@0.25.12': optional: true '@esbuild/netbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-arm64@0.28.2': + '@esbuild/netbsd-x64@0.21.5': optional: true - '@esbuild/netbsd-x64@0.21.5': + '@esbuild/netbsd-x64@0.25.12': optional: true '@esbuild/netbsd-x64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.28.2': + '@esbuild/openbsd-arm64@0.25.12': optional: true '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/openbsd-arm64@0.28.2': + '@esbuild/openbsd-x64@0.21.5': optional: true - '@esbuild/openbsd-x64@0.21.5': + '@esbuild/openbsd-x64@0.25.12': optional: true '@esbuild/openbsd-x64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.28.2': + '@esbuild/openharmony-arm64@0.25.12': optional: true '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/openharmony-arm64@0.28.2': - optional: true - '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.27.7': + '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/sunos-x64@0.28.2': + '@esbuild/sunos-x64@0.27.7': optional: true '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-arm64@0.27.7': + '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-arm64@0.28.2': + '@esbuild/win32-arm64@0.27.7': optional: true '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-ia32@0.27.7': + '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-ia32@0.28.2': + '@esbuild/win32-ia32@0.27.7': optional: true '@esbuild/win32-x64@0.21.5': optional: true - '@esbuild/win32-x64@0.27.7': + '@esbuild/win32-x64@0.25.12': optional: true - '@esbuild/win32-x64@0.28.2': + '@esbuild/win32-x64@0.27.7': optional: true '@escape.tech/graphql-armor-max-aliases@2.6.2': @@ -2912,6 +3522,11 @@ snapshots: '@jridgewell/sourcemap-codec': 1.6.0 '@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.6.0': {} @@ -2952,6 +3567,8 @@ snapshots: '@repeaterjs/repeater@3.1.0': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/rollup-android-arm-eabi@4.63.2': optional: true @@ -3040,10 +3657,64 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 + '@testing-library/dom@10.4.2': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.10.0(@testing-library/dom@10.4.2)': + dependencies: + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.2 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.2 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + '@thaunknown/thirty-two@1.0.6': dependencies: uint8-util: 2.3.2 + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.8 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -3069,6 +3740,26 @@ snapshots: '@types/picomatch@3.0.2': {} + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@4.7.0(vite@6.4.3(@types/node@22.20.2)(yaml@2.9.1))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.3(@types/node@22.20.2)(yaml@2.9.1) + transitivePeerDependencies: + - supports-color + '@vitest/expect@3.2.7': dependencies: '@types/chai': 5.2.3 @@ -3077,13 +3768,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@22.20.2)(yaml@2.9.1))': + '@vitest/mocker@3.2.7(vite@6.4.3(@types/node@22.20.2)(yaml@2.9.1))': dependencies: '@vitest/spy': 3.2.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.6(@types/node@22.20.2)(yaml@2.9.1) + vite: 6.4.3(@types/node@22.20.2)(yaml@2.9.1) '@vitest/pretty-format@3.2.7': dependencies: @@ -3158,6 +3849,8 @@ snapshots: acorn@8.18.0: {} + agent-base@7.1.4: {} + ajv-formats@2.1.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 @@ -3185,12 +3878,20 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} any-promise@1.3.0: {} argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + assertion-error@2.0.1: {} atomic-sleep@1.0.0: {} @@ -3204,6 +3905,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.11.23: {} + bencode@4.0.1: dependencies: uint8-util: 2.3.2 @@ -3218,6 +3921,14 @@ snapshots: dependencies: balanced-match: 1.0.2 + browserslist@4.28.9: + dependencies: + baseline-browser-mapping: 2.11.23 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.427 + node-releases: 2.0.55 + update-browserslist-db: 1.3.3(browserslist@4.28.9) + buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -3230,6 +3941,8 @@ snapshots: cac@6.7.14: {} + caniuse-lite@1.0.30001810: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -3270,6 +3983,10 @@ snapshots: consola@3.4.2: {} + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + copy-anything@4.1.0: {} create-torrent@6.1.3: @@ -3302,8 +4019,22 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css.escape@1.5.1: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + csstype@3.2.3: {} + data-uri-to-buffer@4.0.1: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + dataloader@2.2.3: {} debounce-fn@5.1.2: @@ -3314,14 +4045,22 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + deep-eql@5.0.2: {} delay@5.0.0: {} + dequal@2.0.3: {} + detect-package-manager@3.0.2: dependencies: execa: 5.1.1 + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + dot-prop@8.0.2: dependencies: type-fest: 3.13.1 @@ -3338,10 +4077,14 @@ snapshots: eastasianwidth@0.2.0: {} + electron-to-chromium@1.5.427: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + entities@6.0.1: {} + env-paths@3.0.0: {} environment@1.1.0: {} @@ -3374,6 +4117,35 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + esbuild@0.27.7: optionalDependencies: '@esbuild/aix-ppc64': 0.27.7 @@ -3403,34 +4175,7 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 - esbuild@0.28.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.28.2 - '@esbuild/android-arm': 0.28.2 - '@esbuild/android-arm64': 0.28.2 - '@esbuild/android-x64': 0.28.2 - '@esbuild/darwin-arm64': 0.28.2 - '@esbuild/darwin-x64': 0.28.2 - '@esbuild/freebsd-arm64': 0.28.2 - '@esbuild/freebsd-x64': 0.28.2 - '@esbuild/linux-arm': 0.28.2 - '@esbuild/linux-arm64': 0.28.2 - '@esbuild/linux-ia32': 0.28.2 - '@esbuild/linux-loong64': 0.28.2 - '@esbuild/linux-mips64el': 0.28.2 - '@esbuild/linux-ppc64': 0.28.2 - '@esbuild/linux-riscv64': 0.28.2 - '@esbuild/linux-s390x': 0.28.2 - '@esbuild/linux-x64': 0.28.2 - '@esbuild/netbsd-arm64': 0.28.2 - '@esbuild/netbsd-x64': 0.28.2 - '@esbuild/openbsd-arm64': 0.28.2 - '@esbuild/openbsd-x64': 0.28.2 - '@esbuild/openharmony-arm64': 0.28.2 - '@esbuild/sunos-x64': 0.28.2 - '@esbuild/win32-arm64': 0.28.2 - '@esbuild/win32-ia32': 0.28.2 - '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} estree-walker@3.0.3: dependencies: @@ -3497,6 +4242,8 @@ snapshots: fsevents@2.3.3: optional: true + gensync@1.0.0-beta.2: {} + get-stdin@10.0.0: {} get-stream@6.0.1: {} @@ -3534,6 +4281,17 @@ snapshots: hono@4.13.7: {} + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + http-terminator@3.2.0: dependencies: delay: 5.0.0 @@ -3541,10 +4299,23 @@ snapshots: roarr: 7.21.7 type-fest: 2.19.0 + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-signals@2.1.0: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} + indent-string@4.0.0: {} + ipfs-unixfs@13.1.0: dependencies: protons-runtime: 7.1.0 @@ -3561,6 +4332,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-potential-custom-element-name@1.0.1: {} + is-stream@2.0.1: {} isexe@2.0.0: {} @@ -3589,6 +4362,35 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.27 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.21.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + json-schema-to-typescript@15.0.4: dependencies: '@apidevtools/json-schema-ref-parser': 11.9.3 @@ -3605,6 +4407,8 @@ snapshots: json-schema-typed@8.0.2: {} + json5@2.2.3: {} + junk@4.0.1: {} kysely@0.26.3: {} @@ -3621,6 +4425,12 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 @@ -3637,6 +4447,8 @@ snapshots: mimic-fn@4.0.0: {} + min-indent@1.0.1: {} + minimatch@9.0.9: dependencies: brace-expansion: 2.1.4 @@ -3674,10 +4486,14 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + node-releases@2.0.55: {} + npm-run-path@4.0.1: dependencies: path-key: 3.1.1 + nwsapi@2.2.27: {} + object-assign@4.1.1: {} obuf@1.1.2: {} @@ -3728,6 +4544,10 @@ snapshots: queue-microtask: 1.2.3 uint8-util: 2.3.2 + parse5@7.3.0: + dependencies: + entities: 6.0.1 + path-key@3.1.1: {} path-scurry@1.11.1: @@ -3924,6 +4744,12 @@ snapshots: prettier@3.9.6: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + process-warning@3.0.0: {} process@0.11.10: {} @@ -3939,12 +4765,33 @@ snapshots: uint8arraylist: 3.0.2 uint8arrays: 6.1.1 + punycode@2.3.1: {} + pure-rand@6.1.0: {} queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} + react-dom@19.3.0(react@19.3.0): + dependencies: + react: 19.3.0 + scheduler: 0.28.0 + + react-is@17.0.2: {} + + react-refresh@0.17.0: {} + + react-router@7.18.3(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + dependencies: + cookie: 1.1.1 + react: 19.3.0 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.3.0(react@19.3.0) + + react@19.3.0: {} + readable-stream@4.7.0: dependencies: abort-controller: 3.0.0 @@ -3957,6 +4804,11 @@ snapshots: real-require@0.2.0: {} + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + require-from-string@2.0.2: {} resolve-from@5.0.0: {} @@ -3999,6 +4851,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.63.2 fsevents: 2.3.3 + rrweb-cssom@0.8.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -4007,10 +4861,22 @@ snapshots: safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.28.0: {} + semver-compare@1.0.0: {} + semver@6.3.1: {} + semver@7.8.5: {} + set-cookie-parser@2.7.2: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4067,6 +4933,10 @@ snapshots: strip-final-newline@2.0.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 @@ -4091,6 +4961,8 @@ snapshots: dependencies: copy-anything: 4.1.0 + symbol-tree@3.2.4: {} + tdigest@0.1.3: dependencies: bintrees: 1.0.2 @@ -4124,6 +4996,20 @@ snapshots: tinyspy@4.0.6: {} + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} ts-interface-checker@0.1.13: {} @@ -4191,6 +5077,12 @@ snapshots: undici-types@6.21.0: {} + update-browserslist-db@1.3.3(browserslist@4.28.9): + dependencies: + browserslist: 4.28.9 + escalade: 3.2.0 + picocolors: 1.1.1 + urlpattern-polyfill@10.1.0: {} viem@2.56.5(typescript@5.9.3): @@ -4234,7 +5126,7 @@ snapshots: debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.6(@types/node@22.20.2)(yaml@2.9.1) + vite: 6.4.3(@types/node@22.20.2)(yaml@2.9.1) transitivePeerDependencies: - '@types/node' - jiti @@ -4269,9 +5161,9 @@ snapshots: '@types/node': 22.20.2 fsevents: 2.3.3 - vite@7.3.6(@types/node@22.20.2)(yaml@2.9.1): + vite@6.4.3(@types/node@22.20.2)(yaml@2.9.1): dependencies: - esbuild: 0.28.2 + esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 postcss: 8.5.28 @@ -4282,11 +5174,11 @@ snapshots: fsevents: 2.3.3 yaml: 2.9.1 - vitest@3.2.7(@types/node@22.20.2)(yaml@2.9.1): + vitest@3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 - '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@22.20.2)(yaml@2.9.1)) + '@vitest/mocker': 3.2.7(vite@6.4.3(@types/node@22.20.2)(yaml@2.9.1)) '@vitest/pretty-format': 3.2.7 '@vitest/runner': 3.2.7 '@vitest/snapshot': 3.2.7 @@ -4304,11 +5196,12 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.6(@types/node@22.20.2)(yaml@2.9.1) + vite: 6.4.3(@types/node@22.20.2)(yaml@2.9.1) vite-node: 3.2.4(@types/node@22.20.2)(yaml@2.9.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.20.2 + jsdom: 26.1.0 transitivePeerDependencies: - jiti - less @@ -4323,8 +5216,25 @@ snapshots: - tsx - yaml + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + web-streams-polyfill@3.3.3: {} + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + when-exit@2.1.5: {} which@2.0.2: @@ -4352,6 +5262,12 @@ snapshots: ws@8.21.0: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + xtend@4.0.2: {} + yallist@3.1.1: {} + yaml@2.9.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a2b26bb..1736b82 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,7 @@ packages: - "packages/*" - "services/*" + - "apps/*" - "bootstrap" - "infra" - "test/e2e" diff --git a/services/indexer/CHANGELOG.md b/services/indexer/CHANGELOG.md index 77916ad..f25dbbd 100644 --- a/services/indexer/CHANGELOG.md +++ b/services/indexer/CHANGELOG.md @@ -1,5 +1,9 @@ # @enspack/indexer +## Unreleased + +- CORS: `GET` / `OPTIONS` on `/v1/*` with `Access-Control-Allow-Origin: *` so the catalog site can call the API from `enspack.dev`. + ## 0.1.0 - WP-21: production Dockerfile copies `infra/package.json` so the workspace diff --git a/services/indexer/src/api/app.ts b/services/indexer/src/api/app.ts index d1f2f60..1ced376 100644 --- a/services/indexer/src/api/app.ts +++ b/services/indexer/src/api/app.ts @@ -1,5 +1,6 @@ import type { Manifest } from "@enspack/core"; import { Hono } from "hono"; +import { cors } from "hono/cors"; import { decodeNamesCursor, encodeNamesCursor } from "../cursor.js"; import type { IndexerReader, NameRow, VersionRow } from "../repo.js"; @@ -61,6 +62,8 @@ function preferName(rows: NameRow[]): NameRow | undefined { export function createIndexerApi(repo: IndexerReader): Hono { const app = new Hono(); + app.use("/v1/*", cors({ origin: "*", allowMethods: ["GET", "OPTIONS"] })); + app.get("/v1/health", (c) => c.json({ ok: true })); app.get("/v1/names", async (c) => { diff --git a/services/indexer/test/api.test.ts b/services/indexer/test/api.test.ts index 033dc85..f8df181 100644 --- a/services/indexer/test/api.test.ts +++ b/services/indexer/test/api.test.ts @@ -175,4 +175,22 @@ describe("API §4.3", () => { expect(res.status).toBe(200); expect(await res.json()).toEqual({ ok: true }); }); + + it("CORS: GET /v1/names allows any origin; OPTIONS preflight succeeds", async () => { + const { app } = await seededRepo(); + const get = await app.request("/v1/names", { + headers: { Origin: "https://enspack.dev" }, + }); + expect(get.status).toBe(200); + expect(get.headers.get("access-control-allow-origin")).toBe("*"); + + const preflight = await app.request("/v1/names", { + method: "OPTIONS", + headers: { + Origin: "https://enspack.dev", + "Access-Control-Request-Method": "GET", + }, + }); + expect([200, 204]).toContain(preflight.status); + }); });