diff --git a/AGENTS.md b/AGENTS.md index 2586540..1920a1b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ Guidance for `pkgs`, the canonical monorepo for Ewan's TypeScript/Svelte librari - OAuth scopes, client IDs, redirect URIs, metadata JSON, and deployment origins must change together. App passwords, OAuth sessions, API keys, Vercel Blob tokens, Git/SSH credentials, uploaded histories, `.docx` contents, and local caches/state must not enter logs, bundles, fixtures, or commits. - `svelte-standard-site` renders remote rich text, Markdown, embeds, iframes, images, native comments, and themes. Maintain sanitization/URL-scheme boundaries, SSR-safe DOM use, publication verification, and separation between Bluesky reply comments and `site.standard.*` native comments. - `supporters` processes Ko-fi/GitHub webhook events and stores them in AT Protocol. Verify webhook signatures before writes, make event ingestion replay-safe, avoid exposing donor payloads, and use only synthetic events in scripts/tests. -- `tourmaline` resolves public repositories, paginates Teal scrobbles, calls rate-limited MusicBrainz/Last.fm/Deezer APIs, caches enrichment in warm-process memory, and can publish share posts. Bound user-selected ranges/pages and upstream responses; never expose the Last.fm key client-side. +- `tourmaline` resolves public repositories, fetches Teal scrobbles via CAR export (`com.atproto.sync.getRepo`), calls rate-limited MusicBrainz/Last.fm/Deezer APIs, caches enrichment in warm-process memory, and can publish share posts. Bound user-selected ranges/pages and upstream responses; never expose the Last.fm key client-side. - `og` is not actually zero-native-dependency at runtime: it uses `@resvg/resvg-js`, Node crypto/Buffer, optional Vercel Blob reads/writes, and generated bundled fonts. Test the actual target runtime and keep cache keys sensitive to every rendering input. ## Working conventions diff --git a/packages/tourmaline/package.json b/packages/tourmaline/package.json index 4520051..f6f5ff8 100644 --- a/packages/tourmaline/package.json +++ b/packages/tourmaline/package.json @@ -1,7 +1,7 @@ { "name": "@ewanc26/tourmaline", "private": true, - "version": "0.6.1", + "version": "0.7.0", "type": "module", "scripts": { "dev": "vite dev", @@ -31,6 +31,7 @@ "dependencies": { "@atproto/api": "^0.19.16", "@atproto/oauth-client-browser": "^0.3.42", + "@ewanc26/croft-click-core": "workspace:*", "@ewanc26/landing-ui": "workspace:*", "@ewanc26/noise-avatar": "workspace:*", "@ewanc26/utils": "workspace:*", diff --git a/packages/tourmaline/src/lib/server/enrich.ts b/packages/tourmaline/src/lib/server/enrich.ts index ea819b8..85dd8db 100644 --- a/packages/tourmaline/src/lib/server/enrich.ts +++ b/packages/tourmaline/src/lib/server/enrich.ts @@ -1,7 +1,7 @@ import type { ArtistInfo } from "$lib/types"; const MB_USER_AGENT = - "Tourmaline/0.6.1 (https://github.com/ewanc26/pkgs/tree/main/packages/tourmaline)"; + "Tourmaline/0.7.0 (https://github.com/ewanc26/pkgs/tree/main/packages/tourmaline)"; const MB_BASE = "https://musicbrainz.org/ws/2"; const LFM_BASE = "https://ws.audioscrobbler.com/2.0/"; const DZ_BASE = "https://api.deezer.com"; diff --git a/packages/tourmaline/src/lib/server/rate-limit.ts b/packages/tourmaline/src/lib/server/rate-limit.ts deleted file mode 100644 index e5b6859..0000000 --- a/packages/tourmaline/src/lib/server/rate-limit.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * In-memory rate limiter ported from Malachite. - * Tracks quota from response headers and gates requests to stay within limits. - */ - -export interface RateLimitHeaders { - limit?: number; - remaining?: number; - reset?: number; - windowSeconds?: number; -} - -export function normalizeHeaders(headers: Headers): Record { - const out: Record = {}; - headers.forEach((v, k) => { - out[k.toLowerCase()] = v; - }); - return out; -} - -export function parseRateLimitHeaders(headers: Headers): RateLimitHeaders { - const h = normalizeHeaders(headers); - const get = (k: string) => h[k] ?? h[`x-${k}`] ?? ""; - - const limit = parseInt(get("ratelimit-limit"), 10); - const remaining = parseInt(get("ratelimit-remaining"), 10); - const reset = parseInt(get("ratelimit-reset"), 10); - const policy = get("ratelimit-policy"); - - let windowSeconds: number | undefined; - const m = /;w=(\d+)/.exec(policy); - if (m) windowSeconds = parseInt(m[1], 10); - else if (!isNaN(reset)) { - windowSeconds = Math.max(0, reset - Math.floor(Date.now() / 1000)); - } - - return { - limit: isNaN(limit) ? undefined : limit, - remaining: isNaN(remaining) ? undefined : remaining, - reset: isNaN(reset) ? undefined : reset, - windowSeconds, - }; -} - -export function isRateLimitError(status: number, body?: string): boolean { - if (status === 429) return true; - const msg = (body ?? "").toLowerCase(); - return ( - msg.includes("rate limit") || - msg.includes("too many requests") || - msg.includes("ratelimit") - ); -} - -interface State { - limit: number; - remaining: number; - resetAt: number; // unix seconds - windowSeconds: number; -} - -export class RateLimiter { - private state: State | null = null; - private readonly headroom: number; - - constructor(opts?: { headroom?: number }) { - this.headroom = opts?.headroom ?? 0.15; - } - - updateFromHeaders(headers: Headers): void { - const info = parseRateLimitHeaders(headers); - - if (info.limit === undefined || info.remaining === undefined) return; - - const now = Math.floor(Date.now() / 1000); - this.state = { - limit: info.limit, - remaining: info.remaining, - resetAt: info.reset ?? now + (info.windowSeconds ?? 3600), - windowSeconds: info.windowSeconds ?? 3600, - }; - } - - handleRateLimitHit(headers?: Headers): void { - if (headers) { - this.updateFromHeaders(headers); - } - const now = Math.floor(Date.now() / 1000); - if (this.state) { - this.state.remaining = 0; - } else { - // Fallback state if we hit a limit without headers - this.state = { - limit: 5000, - remaining: 0, - resetAt: now + 60, - windowSeconds: 3600, - }; - } - } - - async waitForPermit(pointsNeeded = 1): Promise { - if (!this.state) return; // Probe first - - const now = Math.floor(Date.now() / 1000); - if (now >= this.state.resetAt) { - this.state.remaining = this.state.limit; - this.state.resetAt = now + this.state.windowSeconds; - } - - const headroomPts = Math.floor(this.state.limit * this.headroom); - const effective = this.state.remaining - headroomPts; - - if (effective < pointsNeeded) { - const waitSeconds = Math.max(1, this.state.resetAt - now); - console.warn(`[rate-limit] hitting limit, waiting ${waitSeconds}s...`); - await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000)); - - this.state.remaining = this.state.limit; - this.state.resetAt = - Math.floor(Date.now() / 1000) + this.state.windowSeconds; - } - - this.state.remaining = Math.max(0, this.state.remaining - pointsNeeded); - } -} - -// Global instance for scrobble fetching -export const pdsRateLimiter = new RateLimiter({ headroom: 0.1 }); diff --git a/packages/tourmaline/src/lib/server/scrobbles.ts b/packages/tourmaline/src/lib/server/scrobbles.ts index 2b2b2ea..3c090b0 100644 --- a/packages/tourmaline/src/lib/server/scrobbles.ts +++ b/packages/tourmaline/src/lib/server/scrobbles.ts @@ -1,15 +1,6 @@ import { TEAL_LEXICON } from "@ewanc26/utils"; +import { fetchRepoViaCAR } from "@ewanc26/croft-click-core"; import type { TealScrobble } from "$lib/types"; -import { pdsRateLimiter, isRateLimitError } from "./rate-limit"; - -interface ListRecordsResponse { - records: Array<{ - uri: string; - cid: string; - value: Record; - }>; - cursor?: string; -} /** * Safely parse a playedTime value from an ATProto record. @@ -78,66 +69,29 @@ export interface ScrobbleBatchResult { } /** - * Fetch a batch of scrobbles from the PDS. - * Each call fetches up to `maxPages` pages (default 25, ~2500 scrobbles) - * to stay well under the Vercel Hobby 10s timeout. + * Fetch a user's entire `fm.teal.alpha.feed.play` collection via the CAR + * export (`com.atproto.sync.getRepo`) and parse the records locally. + * + * Replaces the previous listRecords pagination, which needed up to 25 + * AppView-ratelimited requests per 2,500 records and looped until done. The + * sync namespace has its own far more generous envelope, so one request + * downloads the whole repo and the records are extracted with Malachite's CAR + * MST walker. The response is intentionally one-shot: `cursor` is always null + * and `done` always true, so the client loop exits after a single call. */ export async function fetchScrobbleBatch( pdsUrl: string, did: string, - cursor: string | null, - maxPages = 25, ): Promise { - const limit = 100; - let currentCursor = cursor; - const batch: TealScrobble[] = []; - - for (let page = 0; page < maxPages; page++) { - const params = new URLSearchParams({ - repo: did, - collection: TEAL_LEXICON, - limit: String(limit), - }); - if (currentCursor) params.set("cursor", currentCursor); - - const url = `${pdsUrl}/xrpc/com.atproto.repo.listRecords?${params}`; - - // Wait for rate limit permit - await pdsRateLimiter.waitForPermit(1); - - const res = await fetch(url); - - if (!res.ok) { - const body = await res.text(); - if (isRateLimitError(res.status, body)) { - pdsRateLimiter.handleRateLimitHit(res.headers); - throw new Error("Rate limit exceeded. Try again in a minute."); - } - // The upstream body is logged but never propagated to the browser. - console.error( - `[tourmaline] listRecords ${res.status} from ${pdsUrl}: ${body.slice(0, 500)}`, - ); - throw new Error(`listRecords failed: ${res.status}`); - } - - // Update rate limiter stats - pdsRateLimiter.updateFromHeaders(res.headers); - - const data = (await res.json()) as Partial; - - // Remote responses are untrusted: a PDS may omit or mistype `records`. - const records = Array.isArray(data.records) ? data.records : []; - for (const record of records) { - if (record && typeof record.value === "object" && record.value !== null) { - batch.push(parseScrobble(record.value)); - } - } + const records = await fetchRepoViaCAR(pdsUrl, did, TEAL_LEXICON); - currentCursor = typeof data.cursor === "string" ? data.cursor : null; - if (!currentCursor || records.length === 0) { - return { scrobbles: batch, cursor: null, done: true }; + const scrobbles: TealScrobble[] = []; + for (const record of records) { + const value = record.value; + if (value && typeof value === "object") { + scrobbles.push(parseScrobble(value as Record)); } } - return { scrobbles: batch, cursor: currentCursor, done: false }; + return { scrobbles, cursor: null, done: true }; } diff --git a/packages/tourmaline/src/lib/server/validate.ts b/packages/tourmaline/src/lib/server/validate.ts index 44f4245..0fa17fd 100644 --- a/packages/tourmaline/src/lib/server/validate.ts +++ b/packages/tourmaline/src/lib/server/validate.ts @@ -97,10 +97,3 @@ export function safeEndpoint(raw: string | null | undefined): string | null { const normalised = url.toString(); return normalised.endsWith("/") ? normalised.slice(0, -1) : normalised; } - -/** Pagination cursors are opaque, but they are still remote input. */ -export function safeCursor(cursor: string | null | undefined): string | null { - if (!cursor) return null; - if (cursor.length > 512) return null; - return /^[\w.:~+/=-]+$/.test(cursor) ? cursor : null; -} diff --git a/packages/tourmaline/src/routes/api/scrobbles/[did]/+server.ts b/packages/tourmaline/src/routes/api/scrobbles/[did]/+server.ts index 0d50ce3..60c8885 100644 --- a/packages/tourmaline/src/routes/api/scrobbles/[did]/+server.ts +++ b/packages/tourmaline/src/routes/api/scrobbles/[did]/+server.ts @@ -1,6 +1,7 @@ import { json } from "@sveltejs/kit"; +import { CARFetchUnauthorizedError } from "@ewanc26/croft-click-core"; import { fetchScrobbleBatch } from "$lib/server/scrobbles"; -import { isValidDid, safeCursor, safeEndpoint } from "$lib/server/validate"; +import { isValidDid, safeEndpoint } from "$lib/server/validate"; import type { RequestHandler } from "./$types"; export const GET: RequestHandler = async ({ params, url }) => { @@ -20,14 +21,8 @@ export const GET: RequestHandler = async ({ params, url }) => { ); } - const rawCursor = url.searchParams.get("cursor"); - const cursor = safeCursor(rawCursor); - if (rawCursor && !cursor) { - return json({ error: "Invalid cursor." }, { status: 400 }); - } - try { - const result = await fetchScrobbleBatch(pdsUrl, did, cursor); + const result = await fetchScrobbleBatch(pdsUrl, did); return json({ scrobbles: result.scrobbles, @@ -37,13 +32,18 @@ export const GET: RequestHandler = async ({ params, url }) => { } catch (e) { // Rate-limit messages are actionable for the user; everything else may // carry upstream detail, so it is logged server-side and generalised here. - const message = e instanceof Error ? e.message : ""; - if (message.startsWith("Rate limit exceeded")) { + if (e instanceof Error && e.message.startsWith("CAR fetch failed: 429")) { return json( { error: "Rate limit exceeded. Try again in a minute." }, { status: 429 }, ); } + if (e instanceof CARFetchUnauthorizedError) { + return json( + { error: "This PDS requires authentication to read this repository." }, + { status: 403 }, + ); + } console.error("[tourmaline] scrobble fetch failed:", e); return json( { error: "Failed to fetch scrobbles from the PDS." }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 650838f..d3713db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -560,31 +560,25 @@ importers: packages/pds-landing: dependencies: - '@ewanc26/ui': - specifier: workspace:* - version: link:../ui - '@lucide/svelte': - specifier: ^0.575.0 - version: 0.575.0(svelte@5.55.4) + svelte: + specifier: ^5.53.11 + version: 5.55.4 devDependencies: - '@sveltejs/adapter-static': - specifier: ^3.0.10 - version: 3.0.10(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.6.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)))(svelte@5.55.4)(typescript@5.9.3)(vite@7.3.2(@types/node@25.6.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))) - '@sveltejs/kit': - specifier: ^2.55.0 - version: 2.57.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.6.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)))(svelte@5.55.4)(typescript@5.9.3)(vite@7.3.2(@types/node@25.6.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) - '@sveltejs/package': - specifier: ^2.5.7 - version: 2.5.7(svelte@5.55.4)(typescript@5.9.3) '@sveltejs/vite-plugin-svelte': - specifier: ^6.2.4 - version: 6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.6.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) + specifier: ^5.0.0 + version: 5.1.1(svelte@5.55.4)(vite@7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) '@tailwindcss/typography': specifier: ^0.5.19 version: 0.5.19(tailwindcss@4.2.2) '@tailwindcss/vite': specifier: ^4.2.1 - version: 4.2.2(vite@7.3.2(@types/node@25.6.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) + version: 4.2.2(vite@7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) + '@types/node': + specifier: ^22.15.0 + version: 22.20.1 + oxfmt: + specifier: ^0.57.0 + version: 0.57.0(svelte@5.55.4) prettier: specifier: ^3.8.1 version: 3.8.3 @@ -594,12 +588,6 @@ importers: prettier-plugin-tailwindcss: specifier: ^0.7.2 version: 0.7.2(prettier-plugin-svelte@3.5.1(prettier@3.8.3)(svelte@5.55.4))(prettier@3.8.3) - publint: - specifier: ^0.3.18 - version: 0.3.18 - svelte: - specifier: ^5.53.11 - version: 5.55.4 svelte-check: specifier: ^4.4.5 version: 4.4.6(picomatch@4.0.4)(svelte@5.55.4)(typescript@5.9.3) @@ -611,7 +599,7 @@ importers: version: 5.9.3 vite: specifier: ^7.3.1 - version: 7.3.2(@types/node@25.6.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) + version: 7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) packages/supporters: dependencies: @@ -807,6 +795,9 @@ importers: '@atproto/oauth-client-browser': specifier: ^0.3.42 version: 0.3.42 + '@ewanc26/croft-click-core': + specifier: workspace:* + version: link:../croft-click-core '@ewanc26/landing-ui': specifier: workspace:* version: link:../landing-ui @@ -2063,6 +2054,128 @@ packages: cpu: [x64] os: [win32] + '@oxfmt/binding-android-arm-eabi@0.57.0': + resolution: {integrity: sha512-qVBsEO+KugOsCmUHcO8iqNnqc65p7PCKpCs8M66mPZ+Ri+CWbcpoQOEJBg2OTu03+0qu++NK1jj6IzvQVs0Sig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.57.0': + resolution: {integrity: sha512-mp6PibWbao3aizijcheOeHQaYEhcUAt8pwLniYbtLfHxL/psFF0BykAwCj+s3c6qIpa8yN8keZICWrqtZ70w8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.57.0': + resolution: {integrity: sha512-T+0stuCBqmUVY+aMIvrgXhzGhHO3sD5tNiiEcYqgSdPsnukskQqn2u5qOVD0sv1l7RLdFS5Z/f5Wi9Ktyjr3Eg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.57.0': + resolution: {integrity: sha512-O+3JbqWs/mCI2oi4xfhRO2IVPFJNDDEBV8Odo+ZpmsUOeKJfjXoNH7nDmBEQcDgK7NfjDIyE7kRgYSZcTLDO0A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.57.0': + resolution: {integrity: sha512-pxwhxVC+JkLX9twOQ/8C/vbuOQcMZyKIDmiRDZfO7yITuVcIdZCiLRqqf4QOxb2+8FWrRXzQpm+1DBKcMpHSSQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.57.0': + resolution: {integrity: sha512-pxBU4zH2imB/MDBfth2rOMeVxXUMjRQLCazagwLARIFH3hVlxZJBlM4nSnHXaIHJK4/qezoFCIORN6AY8Mra4A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.57.0': + resolution: {integrity: sha512-JAprOzt8tycYou36ZgEw14DlRHTiN8qdtKANdV3VZIRIvTI/lh/cX13c9pJ/EnDk2GT3FASH7KvCgQ2AufAifQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.57.0': + resolution: {integrity: sha512-ajtjaxSaj9xl4BW7REt+Cef/ttzbAq00Bq4z7JUDZEfgFXdwSjH8K9bF+IcIJzZB9lKqMfQ4eHuSFOvvlvtqOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.57.0': + resolution: {integrity: sha512-p4Y/+RYk9Bk5WO+zHSUXAClRmZ2fbJCejMuCAsU2HhyME4jqf6Ftt/mJYEwIah1wGCBDYOB7wEGV1x5bCEZ6hA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.57.0': + resolution: {integrity: sha512-By6tRALAZsno0F4zedmtG+wdMvJiJmJoXM4d3+A9zHE4HRXLqXITwRH8mgrlcXc5yJM2g2W3riRPwTYdgemZLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.57.0': + resolution: {integrity: sha512-skYeG+RgvyzspqVEBsEprL90OYYZfoVNqB3HcCNR6QDJyXKOzfDRT3zncnHmUaFluIlBHuY23mU1b5WGgR98hA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.57.0': + resolution: {integrity: sha512-FFgACrZOXAXUh5KQh2mt1CDOVOZmn+QzHP71wM9QobNwyQvoFfyAeefVUltW83g3sm7LTiH3yfFqLLVUpA5ZFQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.57.0': + resolution: {integrity: sha512-Nm/BAOfQeFiiKd502mZn/GAVKJwtd0RdCg17G3Wz/WSOIQmDi3+7/SZH4BHn1Ye5KvTVH3ua8WvfwLLycNIuvA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.57.0': + resolution: {integrity: sha512-BiSy5Ku3mQqyxS6YIqAJgd403wEUWvI7kerfzPxc2l/txZVmZM0pSj7oDM+4bGBExowxOi7o73jEam1W0EDTZg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.57.0': + resolution: {integrity: sha512-BCRkJiotz5s9afLYD2LuMvzAoDYx9H17E/YbDyu4xK7l4zHDPeny9ErSXL//i/nJyaOwRk08x4b8cgJC00+JDg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.57.0': + resolution: {integrity: sha512-4Oaxe1qrGgXfpCJ1C/ERJ2iCtV2rN1R79ga9fsfyVHfSQRu/hVW780u2KDqZWFZ/iGTHODJji0JemxqFZ63eIQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.57.0': + resolution: {integrity: sha512-MYLAsDnhdNsSGheLYhWgbk0vfIrlS84iQYun/y21fX6u0jj8iBtYtbpZMdiqYeuf8U12eVPUjVY2xE2NrCfJ0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.57.0': + resolution: {integrity: sha512-PBwdzZALJY/jcCx2E6is0yu+cuVXeySTDmwuseD+9j0mHqlRNxwlKgsyRTBed/woPeqfVfuXfWjoq4Cx2Zt3Eg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.57.0': + resolution: {integrity: sha512-bQJdH9i4RRfw55jm7+8/xS7GzHLLTbHx4huhrrDxQJaJtbSDbsyOnODvP1ftT7EG0KFKAYO2S+q6AcioXODx8w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -2633,11 +2746,6 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/adapter-static@3.0.10': - resolution: {integrity: sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==} - peerDependencies: - '@sveltejs/kit': ^2.0.0 - '@sveltejs/adapter-vercel@6.3.3': resolution: {integrity: sha512-jI7jT/XqRyFe9oqKvFcNPQfyNBi3pXqN1iQXa2lmeKT5Vzgr9iSOqJOD3pXf/9Q2Os6SXzqYYm6osRjHYEhkyw==} engines: {node: '>=20.0'} @@ -2683,6 +2791,14 @@ packages: peerDependencies: svelte: ^3.44.0 || ^4.0.0 || ^5.0.0-next.1 + '@sveltejs/vite-plugin-svelte-inspector@4.0.1': + resolution: {integrity: sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^5.0.0 + svelte: ^5.0.0 + vite: ^6.0.0 + '@sveltejs/vite-plugin-svelte-inspector@5.0.2': resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==} engines: {node: ^20.19 || ^22.12 || >=24} @@ -2691,6 +2807,13 @@ packages: svelte: ^5.0.0 vite: ^6.3.0 || ^7.0.0 + '@sveltejs/vite-plugin-svelte@5.1.1': + resolution: {integrity: sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.0.0 + '@sveltejs/vite-plugin-svelte@6.2.4': resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==} engines: {node: ^20.19 || ^22.12 || >=24} @@ -2960,6 +3083,9 @@ packages: '@types/node@20.11.0': resolution: {integrity: sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==} + '@types/node@22.20.1': + resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + '@types/node@25.6.0': resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} @@ -4532,6 +4658,19 @@ packages: resolution: {integrity: sha512-oa5KKSDNLHZGaiqIGAbCWXeN9IJUAz9MElWcQX90epDxdKc9Hrt/BsLj3K4gDqfAYa5dwdH+ZCFJG9hR74fiGg==} engines: {node: ^20.19.0 || >=22.12.0} + oxfmt@0.57.0: + resolution: {integrity: sha512-ZB7Bi+rGDSqmVIo9jwcLyFgjxXvQhDdU+jx+ZrVy6VRiVXK2+CHc4hO3J4dUQjHe7V0ymHB+MDuv5z+NhK07HA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + p-finally@2.0.1: resolution: {integrity: sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==} engines: {node: '>=8'} @@ -5145,6 +5284,10 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} @@ -5258,6 +5401,9 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} @@ -6575,6 +6721,63 @@ snapshots: '@oxc-transform/binding-win32-x64-msvc@0.111.0': optional: true + '@oxfmt/binding-android-arm-eabi@0.57.0': + optional: true + + '@oxfmt/binding-android-arm64@0.57.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.57.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.57.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.57.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.57.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.57.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.57.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.57.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.57.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.57.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.57.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.57.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.57.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.57.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.57.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.57.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.57.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.57.0': + optional: true + '@polka/url@1.0.0-next.29': {} '@publint/pack@0.1.4': {} @@ -6924,10 +7127,6 @@ snapshots: dependencies: '@sveltejs/kit': 2.57.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.6.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)))(svelte@5.55.4)(typescript@5.9.3)(vite@7.3.2(@types/node@25.6.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.6.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)))(svelte@5.55.4)(typescript@5.9.3)(vite@7.3.2(@types/node@25.6.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)))': - dependencies: - '@sveltejs/kit': 2.57.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.6.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)))(svelte@5.55.4)(typescript@5.9.3)(vite@7.3.2(@types/node@25.6.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) - '@sveltejs/adapter-vercel@6.3.3(@sveltejs/kit@2.57.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.2)(vite@8.0.7(@types/node@25.9.1)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.2)(typescript@6.0.2)(vite@8.0.7(@types/node@25.9.1)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(rollup@4.60.1)': dependencies: '@sveltejs/kit': 2.57.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.2)(vite@8.0.7(@types/node@25.9.1)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.2)(typescript@6.0.2)(vite@8.0.7(@types/node@25.9.1)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) @@ -7079,6 +7278,15 @@ snapshots: transitivePeerDependencies: - typescript + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.4)(vite@7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)))(svelte@5.55.4)(vite@7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))': + dependencies: + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.55.4)(vite@7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) + debug: 4.4.3 + svelte: 5.55.4 + vite: 7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) + transitivePeerDependencies: + - supports-color + '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)))(svelte@5.55.4)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))': dependencies: '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) @@ -7100,6 +7308,19 @@ snapshots: svelte: 5.55.4 vite: 8.0.12(@types/node@25.9.1)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0) + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.4)(vite@7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.4)(vite@7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)))(svelte@5.55.4)(vite@7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) + debug: 4.4.3 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.21 + svelte: 5.55.4 + vite: 7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) + vitefu: 1.1.3(vite@7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) + transitivePeerDependencies: + - supports-color + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))': dependencies: '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.4)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)))(svelte@5.55.4)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) @@ -7275,6 +7496,13 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.2.2 + '@tailwindcss/vite@4.2.2(vite@7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))': + dependencies: + '@tailwindcss/node': 4.2.2 + '@tailwindcss/oxide': 4.2.2 + tailwindcss: 4.2.2 + vite: 7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) + '@tailwindcss/vite@4.2.2(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))': dependencies: '@tailwindcss/node': 4.2.2 @@ -7358,6 +7586,10 @@ snapshots: dependencies: undici-types: 5.26.5 + '@types/node@22.20.1': + dependencies: + undici-types: 6.21.0 + '@types/node@25.6.0': dependencies: undici-types: 7.19.2 @@ -9547,6 +9779,31 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' + oxfmt@0.57.0(svelte@5.55.4): + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.57.0 + '@oxfmt/binding-android-arm64': 0.57.0 + '@oxfmt/binding-darwin-arm64': 0.57.0 + '@oxfmt/binding-darwin-x64': 0.57.0 + '@oxfmt/binding-freebsd-x64': 0.57.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.57.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.57.0 + '@oxfmt/binding-linux-arm64-gnu': 0.57.0 + '@oxfmt/binding-linux-arm64-musl': 0.57.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.57.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.57.0 + '@oxfmt/binding-linux-riscv64-musl': 0.57.0 + '@oxfmt/binding-linux-s390x-gnu': 0.57.0 + '@oxfmt/binding-linux-x64-gnu': 0.57.0 + '@oxfmt/binding-linux-x64-musl': 0.57.0 + '@oxfmt/binding-openharmony-arm64': 0.57.0 + '@oxfmt/binding-win32-arm64-msvc': 0.57.0 + '@oxfmt/binding-win32-ia32-msvc': 0.57.0 + '@oxfmt/binding-win32-x64-msvc': 0.57.0 + svelte: 5.55.4 + p-finally@2.0.1: {} pac-proxy-agent@7.2.0: @@ -10265,6 +10522,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinypool@2.1.0: {} + tinyrainbow@3.1.0: {} tlds@1.261.0: {} @@ -10369,6 +10628,8 @@ snapshots: undici-types@5.26.5: {} + undici-types@6.21.0: {} + undici-types@7.19.2: {} undici-types@7.24.6: {} @@ -10579,6 +10840,21 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + vite@7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.9 + rollup: 4.60.1 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 22.20.1 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.32.0 + tsx: 4.21.0 + vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0): dependencies: esbuild: 0.27.7 @@ -10651,6 +10927,10 @@ snapshots: jiti: 2.6.1 tsx: 4.21.0 + vitefu@1.1.3(vite@7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)): + optionalDependencies: + vite: 7.3.2(@types/node@22.20.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) + vitefu@1.1.3(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)): optionalDependencies: vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)