A social internet radio platform built on AT Protocol. atradio.fm
atproto radio
7

Configure Feed

Select the types of activity you want to include in your feed.

feat: AT Proto social platform — OAuth, PDS-backed data, Jetstream AppView

Turn atradio.fm into a social, AT-Protocol-native app.

Web (apps/web):
- Browser OAuth login (@atcute/oauth-browser-client), Atmosphere login modal
- Gate favoriting/add/remove behind login; public browse/search/playback
- Favorites + user stations stored as fm.atradio.* records in the user's PDS
(optimistic writes, loaded on login)
- Profile shows real display name/handle/avatar; public /profile/:did|:handle

Lexicons (packages/lexicons):
- fm.atradio.{station,favorite,getFavorites,getStations} authored in Pkl
(pkl eval -> JSON) + TS types/zod/mappers

API (apps/api) — Express + Drizzle + Postgres AppView:
- Jetstream consumer across all 4 official hosts, idempotent upserts, cursor
- XRPC getFavorites/getStations (+ discovery), open CORS
- Production TuneIn + ICY media proxies (moved off Vite dev middleware)

Ops & tooling:
- systemd units (api + jetstream)
- tools/console: Babashka + Clojure + rebel REPL command hub, root ./console
- mise pins java/clojure/babashka; consola logging throughout

Tsiry Sandratraina (Jul 14, 2026, 8:25 PM +0300) ff4a55f3 f4281e8f

+4142 -271
+98 -101
README.md
··· 1 1 # atradio.fm 2 2 3 - A synthwave-themed internet radio player. Search stations from 4 - [radio-browser.info](https://www.radio-browser.info/) and TuneIn, stream them 5 - right in the browser, favorite the ones you love, and add your own. Installable 6 - as a PWA. 3 + A **social internet radio platform built on the AT Protocol**. Search stations 4 + from [radio-browser.info](https://www.radio-browser.info/) and TuneIn, stream 5 + them in the browser, and save favorites + your own stations to **your own PDS** 6 + (portable, user-owned data). Installable as a PWA. 7 + 8 + ## Architecture 7 9 8 - > **Frontend only.** There is no backend yet — favorites and your own stations 9 - > live in `localStorage`. Search hits the public provider APIs directly (TuneIn 10 - > via a dev proxy). 10 + ``` 11 + OAuth + reads/writes (in the browser, via atcute) 12 + apps/web (React SPA) ───────────────────────────────► your PDS (source of truth) 13 + │ reads other users' profiles │ fm.atradio.* records 14 + ▼ │ (firehose) 15 + apps/api ◄── XRPC (/xrpc/fm.atradio.*) ── discovery ▼ 16 + Express + Drizzle + Postgres ◄── Jetstream consumer ◄── Jetstream (×4 hosts) 17 + (the AppView / index) 18 + ``` 19 + 20 + - **Auth + your own data** are 100% client-side (`@atcute/oauth-browser-client`). 21 + Favorites/stations are written straight to your PDS as `fm.atradio.*` records — 22 + no auth server. 23 + - **apps/api** is a read-only **AppView**: a Jetstream consumer indexes those 24 + records into Postgres, and XRPC endpoints serve them (for public profiles and 25 + discovery). It also hosts the production TuneIn + ICY media proxies. 26 + 27 + ## Monorepo layout 28 + 29 + ``` 30 + atradio.fm/ 31 + ├─ apps/ 32 + │ ├─ web/ # Vite + React SPA (player, search, OAuth, gating, profiles) 33 + │ └─ api/ # Express + Drizzle + Postgres: Jetstream consumer, XRPC, proxies 34 + ├─ packages/ 35 + │ └─ lexicons/ # fm.atradio.* lexicons (authored in Pkl → JSON) + TS/zod/mappers 36 + ├─ tools/ 37 + │ └─ console/ # Babashka + Clojure + rebel REPL command hub 38 + ├─ systemd/ # deployment units (api + jetstream) 39 + ├─ console # ./console → launches the command REPL 40 + └─ turbo.json / mise.toml / package.json (bun workspaces) 41 + ``` 11 42 12 43 ## Stack 13 44 14 - - **Monorepo:** Turborepo + Bun workspaces 15 - - **App:** Vite + React 19 + TypeScript 16 - - **Styling:** Tailwind CSS **v4** + [HeroUI **v3**](https://www.heroui.com/), dark synthwave-80s theme 17 - - **Fonts:** JetBrains Mono (body/mono) paired with Outfit + Lexend (UI/display) 18 - - **Icons:** [@tabler/icons-react](https://tabler.io/icons) 19 - - **Routing:** TanStack Router 20 - - **Data:** TanStack Query 21 - - **State:** Jotai (with `localStorage` persistence) 22 - - **Forms:** React Hook Form + Zod (incl. live stream-URL verification) 23 - - **PWA:** vite-plugin-pwa (Workbox service worker + web manifest) 24 - - **Tests:** Vitest + Testing Library 25 - - **Toolchain:** pinned via `mise` (Node 24, Bun 1.3) 45 + - **Monorepo:** Turborepo + Bun workspaces; toolchain pinned via `mise` 46 + - **Web:** React 19, Vite, Tailwind v4, HeroUI v3, TanStack Router/Query, Jotai, Vitest 47 + - **AT Proto:** `@atcute/*` (browser OAuth, client, identity-resolver, tid) 48 + - **API:** Express, Drizzle ORM, Postgres, `ws` (Jetstream), Zod, consola 49 + - **Lexicons:** Apple **Pkl** → lexicon JSON (`pkl eval`) 50 + - **Console:** Babashka + Clojure + rebel-readline 26 51 27 52 ## Getting started 28 53 29 54 ```bash 30 - mise install # optional: install the pinned Node + Bun 55 + mise install # node, bun, java, clojure, babashka 31 56 bun install 32 - bun dev # turbo -> vite dev server on http://localhost:3000 57 + bun dev # turbo: web (:3000) + api (:8080) 33 58 ``` 34 59 35 - Other tasks (run from the repo root, orchestrated by Turbo): 60 + Set up the database (Postgres; TLS required): 36 61 37 62 ```bash 38 - bun run build # type-check + production build (also builds the PWA) 39 - bun run preview # preview the production build (PWA/service worker active) 40 - bun run test # run unit tests ⚠️ use `bun run test`, NOT `bun test` 41 - bun run typecheck # type-check only 63 + cd apps/api 64 + cp .env.example .env # set DATABASE_URL, PORT 65 + bun run db:generate # generate migration from the Drizzle schema 66 + bun run db:migrate # apply it 42 67 ``` 43 68 44 - > ⚠️ `bun test` runs Bun's **built-in** test runner and bypasses Vitest. Always 45 - > use `bun run test` (which goes through Turbo → Vitest). 46 - 47 - App-scoped scripts (in `apps/web`): 69 + ## Command console 48 70 49 71 ```bash 50 - bun run generate-icons # regenerate the PWA icon set from the synthwave mark 72 + ./console # Clojure + rebel REPL: (dev) (build) (migrate) (gen-lexicons) … 73 + # or from tools/console: 74 + bb tasks # the same commands as Babashka tasks 51 75 ``` 52 76 53 - ## Project layout 77 + ## AT Protocol 54 78 55 - ``` 56 - atradio.fm/ 57 - ├─ apps/ 58 - │ └─ web/ # the Vite React app 59 - │ ├─ src/ 60 - │ │ ├─ atoms/ # Jotai state (player, favorites, custom stations, ui) 61 - │ │ ├─ components/ # Navbar, Player, StationCard, CategoryGrid, modals, … 62 - │ │ ├─ hooks/ # search, debounce, keyboard shortcuts 63 - │ │ ├─ lib/ 64 - │ │ │ ├─ api/ # radio-browser + TuneIn clients + unified search 65 - │ │ │ ├─ audio/ # stream resolution, stream probe, ICY metadata 66 - │ │ │ └─ validation/ # Zod schema for the add-station form 67 - │ │ ├─ routes/ # SearchPage, ProfilePage 68 - │ │ └─ test/ # Vitest setup + smoke/modal tests 69 - │ ├─ scripts/ # generate-icons.mjs (sharp) 70 - │ └─ vite.config.ts # TuneIn dev proxy + PWA config 71 - ├─ turbo.json 72 - ├─ mise.toml 73 - └─ package.json # bun workspaces 74 - ``` 79 + - **Login** is browser OAuth (atcute public client). Dev uses the `127.0.0.1` 80 + loopback client — **open the app at `http://127.0.0.1:3000`** (not `localhost`). 81 + Prod uses the static `apps/web/public/client-metadata.json`. 82 + - **Gating:** favoriting, adding, and removing stations require login (they open 83 + the login modal). Browsing, search, and playback stay public. 84 + - **Lexicons** (`packages/lexicons`, NSID `fm.atradio.*`): `station`, `favorite`, 85 + and the `getFavorites` / `getStations` queries. Authored in **Pkl**: 86 + ```bash 87 + cd packages/lexicons && bun run pkl:gen # pkl/defs/**.pkl → lexicons/**.json 88 + ``` 89 + - **Data:** your favorites/stations are records in your PDS (read on login, 90 + written with optimistic UI). Any user's profile is viewable at 91 + `/profile/:did` or `/profile/:handle`. 75 92 76 - ## Providers & the TuneIn proxy 93 + ## Backend (apps/api) 77 94 78 - - **radio-browser** sends permissive CORS headers, so it's queried directly. A 79 - random mirror is chosen per session. 80 - - **TuneIn** (`opml.radiotime.com`) sends **no** CORS headers, so browser 81 - requests are blocked. All TuneIn traffic is routed through a proxy: 82 - - **dev:** Vite proxies `/api/tunein/*` → `https://opml.radiotime.com/*`. 83 - - **prod:** set `VITE_TUNEIN_PROXY` to an equivalent server-side proxy. 95 + - **Jetstream consumer** connects to **all four** official instances 96 + simultaneously (`jetstream{1,2}.us-{east,west}.bsky.network`) for redundancy, 97 + filtering `fm.atradio.favorite` / `fm.atradio.station`, and upserts into 98 + Postgres (`users`, `favorites`, `stations`) with a resumable cursor. Duplicate 99 + events across hosts are harmless (idempotent upserts keyed by record `uri`). 100 + - **XRPC** (open CORS, read-only): `fm.atradio.getFavorites`, 101 + `fm.atradio.getStations` (`?actor=<did|handle>&limit&cursor`), plus 102 + `getRecentStations` / `getPopularStations` for discovery. 103 + - **Media proxies:** `GET /api/tunein/*` (TuneIn CORS) and `GET /api/icy?url=…` 104 + (ICY "now playing"). The web app points `VITE_TUNEIN_PROXY` / `VITE_ICY_PROXY` 105 + at the deployed API in prod; in dev it uses Vite's built-in proxy. 84 106 85 - If the proxy is unreachable, search degrades gracefully — radio-browser 86 - results still show, with an inline notice. 107 + ## Deployment 87 108 88 - ## Adding your own station 89 - 90 - The "Add station" form is validated with **React Hook Form + Zod**. Beyond 91 - format checks, it **verifies the stream is actually playable**: the URL is 92 - loaded into a throwaway `<audio>` element (media loads aren't blocked by CORS 93 - the way `fetch` is). If the probe fails — e.g. an HLS stream that only plays via 94 - hls.js — an **"add it anyway"** option appears. An optional logo/picture URL is 95 - supported, with a live preview. 96 - 97 - ## "Now playing" (ICY metadata) 98 - 99 - The player reads **ICY** (`StreamTitle`) metadata directly from the byte stream 100 - (`Icy-MetaData: 1` + `icy-metaint` parsing) and shows the current track when 101 - available. This is best-effort: many stream hosts don't expose CORS/`icy-metaint`, 102 - in which case no title is shown. HLS streams carry no ICY metadata. 103 - 104 - ## PWA 105 - 106 - Built with `vite-plugin-pwa` (Workbox `generateSW`): 107 - 108 - - Installable with a web manifest + maskable icons. 109 - - Offline app shell (precached build + fonts), `NetworkFirst` caching for 110 - radio-browser responses and `CacheFirst` for station artwork. 111 - - Auto-updates when a new build is deployed. 112 - 113 - The service worker is disabled in dev; test it via `bun run build && bun run preview`. 109 + - Static-host `apps/web/dist/` (includes the PWA service worker + client-metadata). 110 + - Run the API via the `systemd/` units (`atradio-api` + `atradio-jetstream`) — see 111 + `systemd/README.md`. 114 112 115 113 ## Keyboard shortcuts 116 114 117 - | Key | Action | 118 - | -------------- | -------------------------- | 119 - | `/` | Focus search | 120 - | `Space` / `K` | Play / pause | 121 - | `M` | Mute / unmute | 122 - | `F` | Favorite current station | 123 - | `A` | Add your own station | 124 - | `↑` / `↓` | Volume up / down | 125 - | `?` | Show the shortcuts overlay | 126 - | `Esc` | Close dialogs / blur input | 115 + | Key | Action | 116 + | ------------- | -------------------------- | 117 + | `/` | Open search palette | 118 + | `Space` / `K` | Play / pause | 119 + | `M` | Mute / unmute | 120 + | `F` | Favorite (requires login) | 121 + | `A` | Add station (requires login) | 122 + | `↑` / `↓` | Volume up / down | 123 + | `?` | Shortcuts overlay | 124 + | `Esc` | Close dialogs | 127 125 128 - ## Roadmap 126 + ## Notes 129 127 130 - - Real backend API (accounts, synced favorites, server-side TuneIn proxy) 131 - - Recently played history & sleep timer 132 - - Route-level code splitting to shrink the initial bundle 128 + - Use **`bun run test`**, not `bun test` (the latter runs Bun's built-in runner). 129 + - ICY "now playing" is best-effort — many stations expose no metadata.
+8
apps/api/.env.example
··· 1 + # Postgres (Xata or any Postgres). TLS is required. 2 + DATABASE_URL=postgresql://user:pass@host/db?sslmode=require 3 + 4 + # HTTP port for the Express server. 5 + PORT=8080 6 + 7 + # Optional: comma-separated Jetstream hosts (defaults to all four official ones). 8 + # JETSTREAM_HOSTS=jetstream1.us-east.bsky.network,jetstream2.us-east.bsky.network,jetstream1.us-west.bsky.network,jetstream2.us-west.bsky.network
+5
apps/api/.gitignore
··· 1 + node_modules 2 + dist 3 + .env 4 + .turbo 5 + *.log
+11
apps/api/drizzle.config.ts
··· 1 + import "dotenv/config"; 2 + import { defineConfig } from "drizzle-kit"; 3 + 4 + export default defineConfig({ 5 + schema: "./src/db/schema.ts", 6 + out: "./drizzle", 7 + dialect: "postgresql", 8 + dbCredentials: { 9 + url: process.env.DATABASE_URL ?? "", 10 + }, 11 + });
+45
apps/api/drizzle/0000_silly_stryfe.sql
··· 1 + CREATE TABLE "favorites" ( 2 + "uri" text PRIMARY KEY NOT NULL, 3 + "did" text NOT NULL, 4 + "rkey" text NOT NULL, 5 + "station_id" text NOT NULL, 6 + "station" jsonb NOT NULL, 7 + "subject_uri" text, 8 + "created_at" timestamp with time zone, 9 + "indexed_at" timestamp with time zone DEFAULT now() NOT NULL 10 + ); 11 + --> statement-breakpoint 12 + CREATE TABLE "jetstream_cursor" ( 13 + "id" text PRIMARY KEY NOT NULL, 14 + "time_us" bigint, 15 + "updated_at" timestamp with time zone DEFAULT now() NOT NULL 16 + ); 17 + --> statement-breakpoint 18 + CREATE TABLE "stations" ( 19 + "uri" text PRIMARY KEY NOT NULL, 20 + "did" text NOT NULL, 21 + "rkey" text NOT NULL, 22 + "name" text NOT NULL, 23 + "stream_url" text NOT NULL, 24 + "description" text, 25 + "genre" text, 26 + "homepage" text, 27 + "logo_url" text, 28 + "tags" jsonb, 29 + "created_at" timestamp with time zone, 30 + "indexed_at" timestamp with time zone DEFAULT now() NOT NULL 31 + ); 32 + --> statement-breakpoint 33 + CREATE TABLE "users" ( 34 + "did" text PRIMARY KEY NOT NULL, 35 + "handle" text, 36 + "display_name" text, 37 + "avatar_url" text, 38 + "description" text, 39 + "indexed_at" timestamp with time zone DEFAULT now() NOT NULL, 40 + "updated_at" timestamp with time zone DEFAULT now() NOT NULL 41 + ); 42 + --> statement-breakpoint 43 + CREATE INDEX "favorites_did_idx" ON "favorites" USING btree ("did");--> statement-breakpoint 44 + CREATE INDEX "favorites_station_id_idx" ON "favorites" USING btree ("station_id");--> statement-breakpoint 45 + CREATE INDEX "stations_did_idx" ON "stations" USING btree ("did");
+303
apps/api/drizzle/meta/0000_snapshot.json
··· 1 + { 2 + "id": "fcec9add-0d2c-45a4-9996-a8123319656e", 3 + "prevId": "00000000-0000-0000-0000-000000000000", 4 + "version": "7", 5 + "dialect": "postgresql", 6 + "tables": { 7 + "public.favorites": { 8 + "name": "favorites", 9 + "schema": "", 10 + "columns": { 11 + "uri": { 12 + "name": "uri", 13 + "type": "text", 14 + "primaryKey": true, 15 + "notNull": true 16 + }, 17 + "did": { 18 + "name": "did", 19 + "type": "text", 20 + "primaryKey": false, 21 + "notNull": true 22 + }, 23 + "rkey": { 24 + "name": "rkey", 25 + "type": "text", 26 + "primaryKey": false, 27 + "notNull": true 28 + }, 29 + "station_id": { 30 + "name": "station_id", 31 + "type": "text", 32 + "primaryKey": false, 33 + "notNull": true 34 + }, 35 + "station": { 36 + "name": "station", 37 + "type": "jsonb", 38 + "primaryKey": false, 39 + "notNull": true 40 + }, 41 + "subject_uri": { 42 + "name": "subject_uri", 43 + "type": "text", 44 + "primaryKey": false, 45 + "notNull": false 46 + }, 47 + "created_at": { 48 + "name": "created_at", 49 + "type": "timestamp with time zone", 50 + "primaryKey": false, 51 + "notNull": false 52 + }, 53 + "indexed_at": { 54 + "name": "indexed_at", 55 + "type": "timestamp with time zone", 56 + "primaryKey": false, 57 + "notNull": true, 58 + "default": "now()" 59 + } 60 + }, 61 + "indexes": { 62 + "favorites_did_idx": { 63 + "name": "favorites_did_idx", 64 + "columns": [ 65 + { 66 + "expression": "did", 67 + "isExpression": false, 68 + "asc": true, 69 + "nulls": "last" 70 + } 71 + ], 72 + "isUnique": false, 73 + "concurrently": false, 74 + "method": "btree", 75 + "with": {} 76 + }, 77 + "favorites_station_id_idx": { 78 + "name": "favorites_station_id_idx", 79 + "columns": [ 80 + { 81 + "expression": "station_id", 82 + "isExpression": false, 83 + "asc": true, 84 + "nulls": "last" 85 + } 86 + ], 87 + "isUnique": false, 88 + "concurrently": false, 89 + "method": "btree", 90 + "with": {} 91 + } 92 + }, 93 + "foreignKeys": {}, 94 + "compositePrimaryKeys": {}, 95 + "uniqueConstraints": {}, 96 + "policies": {}, 97 + "checkConstraints": {}, 98 + "isRLSEnabled": false 99 + }, 100 + "public.jetstream_cursor": { 101 + "name": "jetstream_cursor", 102 + "schema": "", 103 + "columns": { 104 + "id": { 105 + "name": "id", 106 + "type": "text", 107 + "primaryKey": true, 108 + "notNull": true 109 + }, 110 + "time_us": { 111 + "name": "time_us", 112 + "type": "bigint", 113 + "primaryKey": false, 114 + "notNull": false 115 + }, 116 + "updated_at": { 117 + "name": "updated_at", 118 + "type": "timestamp with time zone", 119 + "primaryKey": false, 120 + "notNull": true, 121 + "default": "now()" 122 + } 123 + }, 124 + "indexes": {}, 125 + "foreignKeys": {}, 126 + "compositePrimaryKeys": {}, 127 + "uniqueConstraints": {}, 128 + "policies": {}, 129 + "checkConstraints": {}, 130 + "isRLSEnabled": false 131 + }, 132 + "public.stations": { 133 + "name": "stations", 134 + "schema": "", 135 + "columns": { 136 + "uri": { 137 + "name": "uri", 138 + "type": "text", 139 + "primaryKey": true, 140 + "notNull": true 141 + }, 142 + "did": { 143 + "name": "did", 144 + "type": "text", 145 + "primaryKey": false, 146 + "notNull": true 147 + }, 148 + "rkey": { 149 + "name": "rkey", 150 + "type": "text", 151 + "primaryKey": false, 152 + "notNull": true 153 + }, 154 + "name": { 155 + "name": "name", 156 + "type": "text", 157 + "primaryKey": false, 158 + "notNull": true 159 + }, 160 + "stream_url": { 161 + "name": "stream_url", 162 + "type": "text", 163 + "primaryKey": false, 164 + "notNull": true 165 + }, 166 + "description": { 167 + "name": "description", 168 + "type": "text", 169 + "primaryKey": false, 170 + "notNull": false 171 + }, 172 + "genre": { 173 + "name": "genre", 174 + "type": "text", 175 + "primaryKey": false, 176 + "notNull": false 177 + }, 178 + "homepage": { 179 + "name": "homepage", 180 + "type": "text", 181 + "primaryKey": false, 182 + "notNull": false 183 + }, 184 + "logo_url": { 185 + "name": "logo_url", 186 + "type": "text", 187 + "primaryKey": false, 188 + "notNull": false 189 + }, 190 + "tags": { 191 + "name": "tags", 192 + "type": "jsonb", 193 + "primaryKey": false, 194 + "notNull": false 195 + }, 196 + "created_at": { 197 + "name": "created_at", 198 + "type": "timestamp with time zone", 199 + "primaryKey": false, 200 + "notNull": false 201 + }, 202 + "indexed_at": { 203 + "name": "indexed_at", 204 + "type": "timestamp with time zone", 205 + "primaryKey": false, 206 + "notNull": true, 207 + "default": "now()" 208 + } 209 + }, 210 + "indexes": { 211 + "stations_did_idx": { 212 + "name": "stations_did_idx", 213 + "columns": [ 214 + { 215 + "expression": "did", 216 + "isExpression": false, 217 + "asc": true, 218 + "nulls": "last" 219 + } 220 + ], 221 + "isUnique": false, 222 + "concurrently": false, 223 + "method": "btree", 224 + "with": {} 225 + } 226 + }, 227 + "foreignKeys": {}, 228 + "compositePrimaryKeys": {}, 229 + "uniqueConstraints": {}, 230 + "policies": {}, 231 + "checkConstraints": {}, 232 + "isRLSEnabled": false 233 + }, 234 + "public.users": { 235 + "name": "users", 236 + "schema": "", 237 + "columns": { 238 + "did": { 239 + "name": "did", 240 + "type": "text", 241 + "primaryKey": true, 242 + "notNull": true 243 + }, 244 + "handle": { 245 + "name": "handle", 246 + "type": "text", 247 + "primaryKey": false, 248 + "notNull": false 249 + }, 250 + "display_name": { 251 + "name": "display_name", 252 + "type": "text", 253 + "primaryKey": false, 254 + "notNull": false 255 + }, 256 + "avatar_url": { 257 + "name": "avatar_url", 258 + "type": "text", 259 + "primaryKey": false, 260 + "notNull": false 261 + }, 262 + "description": { 263 + "name": "description", 264 + "type": "text", 265 + "primaryKey": false, 266 + "notNull": false 267 + }, 268 + "indexed_at": { 269 + "name": "indexed_at", 270 + "type": "timestamp with time zone", 271 + "primaryKey": false, 272 + "notNull": true, 273 + "default": "now()" 274 + }, 275 + "updated_at": { 276 + "name": "updated_at", 277 + "type": "timestamp with time zone", 278 + "primaryKey": false, 279 + "notNull": true, 280 + "default": "now()" 281 + } 282 + }, 283 + "indexes": {}, 284 + "foreignKeys": {}, 285 + "compositePrimaryKeys": {}, 286 + "uniqueConstraints": {}, 287 + "policies": {}, 288 + "checkConstraints": {}, 289 + "isRLSEnabled": false 290 + } 291 + }, 292 + "enums": {}, 293 + "schemas": {}, 294 + "sequences": {}, 295 + "roles": {}, 296 + "policies": {}, 297 + "views": {}, 298 + "_meta": { 299 + "columns": {}, 300 + "schemas": {}, 301 + "tables": {} 302 + } 303 + }
+13
apps/api/drizzle/meta/_journal.json
··· 1 + { 2 + "version": "7", 3 + "dialect": "postgresql", 4 + "entries": [ 5 + { 6 + "idx": 0, 7 + "version": "7", 8 + "when": 1784048739927, 9 + "tag": "0000_silly_stryfe", 10 + "breakpoints": true 11 + } 12 + ] 13 + }
+41
apps/api/package.json
··· 1 + { 2 + "name": "@atradio/api", 3 + "version": "0.1.0", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "dev": "tsx watch src/index.ts", 8 + "start": "tsx src/index.ts", 9 + "start:server": "tsx src/bin/server.ts", 10 + "start:consumer": "tsx src/bin/consumer.ts", 11 + "typecheck": "tsc --noEmit", 12 + "build": "tsc", 13 + "db:generate": "drizzle-kit generate", 14 + "db:migrate": "drizzle-kit migrate", 15 + "db:push": "drizzle-kit push" 16 + }, 17 + "dependencies": { 18 + "@atcute/atproto": "^3.1.10", 19 + "@atcute/client": "^4.2.1", 20 + "@atcute/identity-resolver": "^1.2.2", 21 + "@atcute/lexicons": "^1.3.1", 22 + "@atradio/lexicons": "workspace:*", 23 + "consola": "^3.3.3", 24 + "cors": "^2.8.5", 25 + "dotenv": "^16.4.7", 26 + "drizzle-orm": "^0.38.3", 27 + "express": "^4.21.2", 28 + "postgres": "^3.4.5", 29 + "ws": "^8.18.0", 30 + "zod": "^3.24.1" 31 + }, 32 + "devDependencies": { 33 + "@types/cors": "^2.8.17", 34 + "@types/express": "^4.17.21", 35 + "@types/node": "^22.10.5", 36 + "@types/ws": "^8.5.13", 37 + "drizzle-kit": "^0.30.1", 38 + "tsx": "^4.19.2", 39 + "typescript": "^5.7.2" 40 + } 41 + }
+24
apps/api/src/app.ts
··· 1 + import express, { type Express } from "express"; 2 + import cors from "cors"; 3 + import { xrpcRouter } from "./xrpc"; 4 + import { proxyRouter } from "./proxy"; 5 + 6 + /** Build the Express app (XRPC read API + media proxies). Open CORS for all. */ 7 + export function createApp(): Express { 8 + const app = express(); 9 + 10 + app.use(cors({ origin: "*" })); 11 + app.use(express.json()); 12 + 13 + app.get("/health", (_req, res) => { 14 + res.json({ ok: true, service: "atradio-api" }); 15 + }); 16 + 17 + // XRPC query endpoints: /xrpc/fm.atradio.* 18 + app.use("/xrpc", xrpcRouter); 19 + 20 + // Media proxies: /api/tunein/*, /api/icy 21 + app.use("/api", proxyRouter); 22 + 23 + return app; 24 + }
+7
apps/api/src/bin/consumer.ts
··· 1 + import { consola } from "consola"; 2 + import { startJetstream } from "../jetstream/consumer"; 3 + 4 + startJetstream().catch((err) => { 5 + consola.error("[jetstream] failed to start", err); 6 + process.exit(1); 7 + });
+8
apps/api/src/bin/server.ts
··· 1 + import { consola } from "consola"; 2 + import { env } from "../env"; 3 + import { createApp } from "../app"; 4 + 5 + const app = createApp(); 6 + app.listen(env.PORT, () => { 7 + consola.success(`[api] listening on :${env.PORT}`); 8 + });
+10
apps/api/src/db/index.ts
··· 1 + import postgres from "postgres"; 2 + import { drizzle } from "drizzle-orm/postgres-js"; 3 + import { env } from "../env"; 4 + import * as schema from "./schema"; 5 + 6 + /** Xata/Postgres requires TLS (`sslmode=require`). */ 7 + const sql = postgres(env.DATABASE_URL, { ssl: "require", max: 5 }); 8 + 9 + export const db = drizzle(sql, { schema }); 10 + export { schema, sql };
+76
apps/api/src/db/schema.ts
··· 1 + import { 2 + pgTable, 3 + text, 4 + jsonb, 5 + bigint, 6 + timestamp, 7 + index, 8 + } from "drizzle-orm/pg-core"; 9 + import type { StationInfo } from "@atradio/lexicons"; 10 + 11 + /** Actors we've indexed (enriched from app.bsky.actor.getProfile). */ 12 + export const users = pgTable("users", { 13 + did: text("did").primaryKey(), 14 + handle: text("handle"), 15 + displayName: text("display_name"), 16 + avatarUrl: text("avatar_url"), 17 + description: text("description"), 18 + indexedAt: timestamp("indexed_at", { withTimezone: true }) 19 + .defaultNow() 20 + .notNull(), 21 + updatedAt: timestamp("updated_at", { withTimezone: true }) 22 + .defaultNow() 23 + .notNull(), 24 + }); 25 + 26 + /** fm.atradio.station records (from Jetstream). */ 27 + export const stations = pgTable( 28 + "stations", 29 + { 30 + uri: text("uri").primaryKey(), 31 + did: text("did").notNull(), 32 + rkey: text("rkey").notNull(), 33 + name: text("name").notNull(), 34 + streamUrl: text("stream_url").notNull(), 35 + description: text("description"), 36 + genre: text("genre"), 37 + homepage: text("homepage"), 38 + logoUrl: text("logo_url"), 39 + tags: jsonb("tags").$type<string[]>(), 40 + createdAt: timestamp("created_at", { withTimezone: true }), 41 + indexedAt: timestamp("indexed_at", { withTimezone: true }) 42 + .defaultNow() 43 + .notNull(), 44 + }, 45 + (t) => [index("stations_did_idx").on(t.did)], 46 + ); 47 + 48 + /** fm.atradio.favorite records (from Jetstream). */ 49 + export const favorites = pgTable( 50 + "favorites", 51 + { 52 + uri: text("uri").primaryKey(), 53 + did: text("did").notNull(), 54 + rkey: text("rkey").notNull(), 55 + stationId: text("station_id").notNull(), 56 + station: jsonb("station").$type<StationInfo>().notNull(), 57 + subjectUri: text("subject_uri"), 58 + createdAt: timestamp("created_at", { withTimezone: true }), 59 + indexedAt: timestamp("indexed_at", { withTimezone: true }) 60 + .defaultNow() 61 + .notNull(), 62 + }, 63 + (t) => [ 64 + index("favorites_did_idx").on(t.did), 65 + index("favorites_station_id_idx").on(t.stationId), 66 + ], 67 + ); 68 + 69 + /** Single-row resumable Jetstream cursor (max time_us seen). */ 70 + export const jetstreamCursor = pgTable("jetstream_cursor", { 71 + id: text("id").primaryKey(), 72 + timeUs: bigint("time_us", { mode: "number" }), 73 + updatedAt: timestamp("updated_at", { withTimezone: true }) 74 + .defaultNow() 75 + .notNull(), 76 + });
+23
apps/api/src/env.ts
··· 1 + import "dotenv/config"; 2 + import { consola } from "consola"; 3 + 4 + const DEFAULT_JETSTREAMS = [ 5 + "jetstream1.us-east.bsky.network", 6 + "jetstream2.us-east.bsky.network", 7 + "jetstream1.us-west.bsky.network", 8 + "jetstream2.us-west.bsky.network", 9 + ]; 10 + 11 + export const env = { 12 + DATABASE_URL: process.env.DATABASE_URL ?? "", 13 + PORT: Number(process.env.PORT ?? 8080), 14 + /** All Jetstream hosts we connect to simultaneously for redundancy. */ 15 + JETSTREAM_HOSTS: (process.env.JETSTREAM_HOSTS 16 + ? process.env.JETSTREAM_HOSTS.split(",") 17 + : DEFAULT_JETSTREAMS 18 + ).map((h) => h.trim()), 19 + }; 20 + 21 + if (!env.DATABASE_URL) { 22 + consola.warn("[env] DATABASE_URL is not set — the API cannot reach Postgres."); 23 + }
+14
apps/api/src/index.ts
··· 1 + import { consola } from "consola"; 2 + import { env } from "./env"; 3 + import { createApp } from "./app"; 4 + import { startJetstream } from "./jetstream/consumer"; 5 + 6 + /** Combined entrypoint: Express server + Jetstream consumer in one process. */ 7 + const app = createApp(); 8 + app.listen(env.PORT, () => { 9 + consola.success(`[api] listening on :${env.PORT}`); 10 + }); 11 + 12 + startJetstream().catch((err) => { 13 + consola.error("[jetstream] failed to start", err); 14 + });
+232
apps/api/src/jetstream/consumer.ts
··· 1 + import WebSocket from "ws"; 2 + import { consola } from "consola"; 3 + import { eq } from "drizzle-orm"; 4 + import { 5 + NSID, 6 + favoriteRecordSchema, 7 + stationRecordSchema, 8 + } from "@atradio/lexicons"; 9 + import { env } from "../env"; 10 + import { db, schema } from "../db"; 11 + import { getProfile } from "../lib/profile"; 12 + 13 + const WANTED: string[] = [NSID.favorite, NSID.station]; 14 + const CURSOR_ID = "global"; 15 + 16 + interface JetstreamEvent { 17 + did: string; 18 + time_us: number; 19 + kind: "commit" | "identity" | "account"; 20 + commit?: { 21 + operation: "create" | "update" | "delete"; 22 + collection: string; 23 + rkey: string; 24 + record?: unknown; 25 + cid?: string; 26 + }; 27 + identity?: { handle?: string }; 28 + } 29 + 30 + /** Highest time_us processed across all connections (for resume). */ 31 + let cursor = 0; 32 + /** DIDs whose profile we've already enriched this run. */ 33 + const enrichedDids = new Set<string>(); 34 + 35 + function atUri(did: string, collection: string, rkey: string): string { 36 + return `at://${did}/${collection}/${rkey}`; 37 + } 38 + 39 + async function ensureUser(did: string): Promise<void> { 40 + if (enrichedDids.has(did)) return; 41 + enrichedDids.add(did); 42 + const profile = await getProfile(did); 43 + await db 44 + .insert(schema.users) 45 + .values({ 46 + did, 47 + handle: profile?.handle ?? null, 48 + displayName: profile?.displayName ?? null, 49 + avatarUrl: profile?.avatar ?? null, 50 + description: profile?.description ?? null, 51 + updatedAt: new Date(), 52 + }) 53 + .onConflictDoUpdate({ 54 + target: schema.users.did, 55 + set: { 56 + handle: profile?.handle ?? null, 57 + displayName: profile?.displayName ?? null, 58 + avatarUrl: profile?.avatar ?? null, 59 + description: profile?.description ?? null, 60 + updatedAt: new Date(), 61 + }, 62 + }); 63 + } 64 + 65 + async function processCommit(evt: JetstreamEvent): Promise<void> { 66 + const c = evt.commit; 67 + if (!c || !WANTED.includes(c.collection)) return; 68 + const uri = atUri(evt.did, c.collection, c.rkey); 69 + 70 + if (c.operation === "delete") { 71 + if (c.collection === NSID.favorite) { 72 + await db.delete(schema.favorites).where(eq(schema.favorites.uri, uri)); 73 + } else { 74 + await db.delete(schema.stations).where(eq(schema.stations.uri, uri)); 75 + } 76 + return; 77 + } 78 + 79 + // create / update 80 + if (c.collection === NSID.favorite) { 81 + const parsed = favoriteRecordSchema.safeParse(c.record); 82 + if (!parsed.success) return; 83 + const r = parsed.data; 84 + await ensureUser(evt.did); 85 + await db 86 + .insert(schema.favorites) 87 + .values({ 88 + uri, 89 + did: evt.did, 90 + rkey: c.rkey, 91 + stationId: r.station.stationId, 92 + station: r.station, 93 + subjectUri: r.subject?.uri ?? null, 94 + createdAt: new Date(r.createdAt), 95 + indexedAt: new Date(), 96 + }) 97 + .onConflictDoUpdate({ 98 + target: schema.favorites.uri, 99 + set: { 100 + stationId: r.station.stationId, 101 + station: r.station, 102 + subjectUri: r.subject?.uri ?? null, 103 + createdAt: new Date(r.createdAt), 104 + indexedAt: new Date(), 105 + }, 106 + }); 107 + } else { 108 + const parsed = stationRecordSchema.safeParse(c.record); 109 + if (!parsed.success) return; 110 + const r = parsed.data; 111 + await ensureUser(evt.did); 112 + const values = { 113 + uri, 114 + did: evt.did, 115 + rkey: c.rkey, 116 + name: r.name, 117 + streamUrl: r.streamUrl, 118 + description: r.description ?? null, 119 + genre: r.genre ?? null, 120 + homepage: r.homepage ?? null, 121 + logoUrl: r.logo ?? null, 122 + tags: r.tags ?? null, 123 + createdAt: new Date(r.createdAt), 124 + indexedAt: new Date(), 125 + }; 126 + await db 127 + .insert(schema.stations) 128 + .values(values) 129 + .onConflictDoUpdate({ 130 + target: schema.stations.uri, 131 + set: { 132 + name: values.name, 133 + streamUrl: values.streamUrl, 134 + description: values.description, 135 + genre: values.genre, 136 + homepage: values.homepage, 137 + logoUrl: values.logoUrl, 138 + tags: values.tags, 139 + createdAt: values.createdAt, 140 + indexedAt: values.indexedAt, 141 + }, 142 + }); 143 + } 144 + } 145 + 146 + async function handleEvent(evt: JetstreamEvent): Promise<void> { 147 + if (evt.time_us > cursor) cursor = evt.time_us; 148 + if (evt.kind === "commit") { 149 + try { 150 + await processCommit(evt); 151 + } catch (err) { 152 + consola.error("[jetstream] processing error", err); 153 + } 154 + } 155 + } 156 + 157 + function subscribeUrl(host: string): string { 158 + const params = new URLSearchParams(); 159 + for (const c of WANTED) params.append("wantedCollections", c); 160 + if (cursor > 0) params.set("cursor", String(cursor)); 161 + return `wss://${host}/subscribe?${params.toString()}`; 162 + } 163 + 164 + /** Connect to a single Jetstream host with auto-reconnect. */ 165 + function connect(host: string, signal: AbortSignal): void { 166 + if (signal.aborted) return; 167 + const ws = new WebSocket(subscribeUrl(host)); 168 + 169 + ws.on("open", () => consola.success(`[jetstream] connected ${host}`)); 170 + ws.on("message", (data) => { 171 + try { 172 + void handleEvent(JSON.parse(data.toString()) as JetstreamEvent); 173 + } catch { 174 + /* ignore malformed frames */ 175 + } 176 + }); 177 + const reconnect = (why: string) => { 178 + if (signal.aborted) return; 179 + consola.warn(`[jetstream] ${host} ${why}; reconnecting in 3s`); 180 + setTimeout(() => connect(host, signal), 3000); 181 + }; 182 + ws.on("close", () => reconnect("closed")); 183 + ws.on("error", (err: NodeJS.ErrnoException) => { 184 + consola.error(`[jetstream] ${host} error`, err.code ?? err.message); 185 + ws.close(); 186 + }); 187 + } 188 + 189 + async function loadCursor(): Promise<void> { 190 + const [row] = await db 191 + .select() 192 + .from(schema.jetstreamCursor) 193 + .where(eq(schema.jetstreamCursor.id, CURSOR_ID)); 194 + if (row?.timeUs) { 195 + cursor = row.timeUs; 196 + consola.info(`[jetstream] resuming from cursor ${cursor}`); 197 + } 198 + } 199 + 200 + async function persistCursor(): Promise<void> { 201 + if (cursor <= 0) return; 202 + await db 203 + .insert(schema.jetstreamCursor) 204 + .values({ id: CURSOR_ID, timeUs: cursor, updatedAt: new Date() }) 205 + .onConflictDoUpdate({ 206 + target: schema.jetstreamCursor.id, 207 + set: { timeUs: cursor, updatedAt: new Date() }, 208 + }); 209 + } 210 + 211 + /** Start consuming all Jetstream hosts simultaneously. */ 212 + export async function startJetstream(): Promise<() => void> { 213 + await loadCursor(); 214 + const controller = new AbortController(); 215 + for (const host of env.JETSTREAM_HOSTS) connect(host, controller.signal); 216 + 217 + const timer = setInterval(() => { 218 + void persistCursor().catch((err) => 219 + consola.error("[jetstream] cursor persist failed", err), 220 + ); 221 + }, 5000); 222 + 223 + consola.info( 224 + `[jetstream] subscribed to ${env.JETSTREAM_HOSTS.length} hosts for ${WANTED.join(", ")}`, 225 + ); 226 + 227 + return () => { 228 + controller.abort(); 229 + clearInterval(timer); 230 + void persistCursor(); 231 + }; 232 + }
+35
apps/api/src/lib/profile.ts
··· 1 + export interface ProfileData { 2 + did: string; 3 + handle: string; 4 + displayName?: string; 5 + avatar?: string; 6 + description?: string; 7 + } 8 + 9 + /** Resolve a DID or handle to a profile via the public Bluesky AppView. */ 10 + export async function getProfile(actor: string): Promise<ProfileData | null> { 11 + try { 12 + const res = await fetch( 13 + "https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=" + 14 + encodeURIComponent(actor), 15 + ); 16 + if (!res.ok) return null; 17 + const d = (await res.json()) as ProfileData; 18 + return { 19 + did: d.did, 20 + handle: d.handle, 21 + displayName: d.displayName, 22 + avatar: d.avatar, 23 + description: d.description, 24 + }; 25 + } catch { 26 + return null; 27 + } 28 + } 29 + 30 + /** Resolve an actor (DID or handle) to a DID; returns null if unresolvable. */ 31 + export async function resolveDid(actor: string): Promise<string | null> { 32 + if (actor.startsWith("did:")) return actor; 33 + const profile = await getProfile(actor); 34 + return profile?.did ?? null; 35 + }
+139
apps/api/src/proxy/index.ts
··· 1 + import { Router, type Request, type Response } from "express"; 2 + import { consola } from "consola"; 3 + 4 + export const proxyRouter = Router(); 5 + 6 + /* ----------------------------- TuneIn proxy ----------------------------- */ 7 + 8 + /** Reverse-proxy /api/tunein/* to opml.radiotime.com/* (TuneIn sends no CORS). */ 9 + proxyRouter.get(/^\/tunein\/.*/, async (req: Request, res: Response) => { 10 + const path = req.originalUrl.replace(/^\/api\/tunein/, ""); 11 + const target = `https://opml.radiotime.com${path}`; 12 + try { 13 + const upstream = await fetch(target, { 14 + headers: { Accept: req.headers.accept ?? "application/json" }, 15 + }); 16 + res.status(upstream.status); 17 + const type = upstream.headers.get("content-type"); 18 + if (type) res.setHeader("Content-Type", type); 19 + const body = await upstream.text(); 20 + res.send(body); 21 + } catch (err) { 22 + consola.error("[proxy] tunein error", err); 23 + res.status(502).json({ error: "BadGateway" }); 24 + } 25 + }); 26 + 27 + /* ------------------------------- ICY proxy ------------------------------ */ 28 + 29 + async function resolvePlaylist(target: string): Promise<string> { 30 + if (!/\.(pls|m3u|m3u8)(\?|$)/i.test(target)) return target; 31 + const ctrl = new AbortController(); 32 + const t = setTimeout(() => ctrl.abort(), 5000); 33 + try { 34 + const res = await fetch(target, { redirect: "follow", signal: ctrl.signal }); 35 + if (!res.ok) return target; 36 + const body = await res.text(); 37 + const pls = body.match(/^\s*File\d+\s*=\s*(\S+)/im); 38 + if (pls) return pls[1].trim(); 39 + for (const line of body.split(/\r?\n/)) { 40 + const s = line.trim(); 41 + if (s && !s.startsWith("#") && /^https?:\/\//i.test(s)) return s; 42 + } 43 + } catch { 44 + /* fall through */ 45 + } finally { 46 + clearTimeout(t); 47 + } 48 + return target; 49 + } 50 + 51 + async function readIcyTitle(rawTarget: string): Promise<string | null> { 52 + const target = await resolvePlaylist(rawTarget); 53 + const controller = new AbortController(); 54 + const timer = setTimeout(() => controller.abort(), 8000); 55 + try { 56 + const resp = await fetch(target, { 57 + headers: { "Icy-MetaData": "1", "User-Agent": "atradio.fm/1.0" }, 58 + signal: controller.signal, 59 + redirect: "follow", 60 + }); 61 + const metaint = Number(resp.headers.get("icy-metaint")); 62 + if (!resp.body || !metaint || Number.isNaN(metaint)) { 63 + try { 64 + await resp.body?.cancel(); 65 + } catch { 66 + /* ignore */ 67 + } 68 + return null; 69 + } 70 + 71 + const reader = resp.body.getReader(); 72 + const maxBytes = metaint * 2 + 4096; 73 + let bytesUntilMeta = metaint; 74 + let metaLength = -1; 75 + let metaCollected = 0; 76 + let metaBuffer = new Uint8Array(0); 77 + let total = 0; 78 + 79 + try { 80 + while (total < maxBytes) { 81 + const { done, value } = await reader.read(); 82 + if (done || !value) break; 83 + total += value.length; 84 + let i = 0; 85 + while (i < value.length) { 86 + if (bytesUntilMeta > 0) { 87 + const skip = Math.min(bytesUntilMeta, value.length - i); 88 + i += skip; 89 + bytesUntilMeta -= skip; 90 + } else if (metaLength === -1) { 91 + metaLength = value[i] * 16; 92 + i += 1; 93 + if (metaLength === 0) { 94 + bytesUntilMeta = metaint; 95 + metaLength = -1; 96 + } else { 97 + metaBuffer = new Uint8Array(metaLength); 98 + metaCollected = 0; 99 + } 100 + } else { 101 + const take = Math.min(metaLength - metaCollected, value.length - i); 102 + metaBuffer.set(value.subarray(i, i + take), metaCollected); 103 + metaCollected += take; 104 + i += take; 105 + if (metaCollected >= metaLength) { 106 + const text = Buffer.from(metaBuffer).toString("utf8"); 107 + const m = text.match(/StreamTitle='((?:[^']|'(?!;))*)'/); 108 + return m?.[1]?.trim() || null; 109 + } 110 + } 111 + } 112 + } 113 + return null; 114 + } finally { 115 + try { 116 + await reader.cancel(); 117 + } catch { 118 + /* ignore */ 119 + } 120 + } 121 + } finally { 122 + clearTimeout(timer); 123 + } 124 + } 125 + 126 + /** GET /api/icy?url=<stream> -> { title }. */ 127 + proxyRouter.get("/icy", async (req: Request, res: Response) => { 128 + const target = String(req.query.url ?? ""); 129 + if (!/^https?:\/\//i.test(target)) { 130 + return res.json({ title: null }); 131 + } 132 + try { 133 + const title = await readIcyTitle(target); 134 + return res.json({ title }); 135 + } catch (err) { 136 + consola.error("[proxy] icy error", err); 137 + return res.json({ title: null }); 138 + } 139 + });
+148
apps/api/src/xrpc/index.ts
··· 1 + import { Router, type Request, type Response } from "express"; 2 + import { and, desc, eq, lt, sql } from "drizzle-orm"; 3 + import type { StationInfo, StationView } from "@atradio/lexicons"; 4 + import { db, schema } from "../db"; 5 + import { resolveDid } from "../lib/profile"; 6 + 7 + export const xrpcRouter = Router(); 8 + 9 + function parseLimit(v: unknown, def = 50, max = 100): number { 10 + const n = Number(v); 11 + if (!Number.isFinite(n)) return def; 12 + return Math.min(Math.max(1, Math.floor(n)), max); 13 + } 14 + 15 + function xrpcError(res: Response, status: number, error: string, message: string) { 16 + return res.status(status).json({ error, message }); 17 + } 18 + 19 + type StationRow = typeof schema.stations.$inferSelect; 20 + 21 + function stationRowToInfo(row: StationRow): StationInfo { 22 + return { 23 + stationId: `custom:${row.rkey}`, 24 + name: row.name, 25 + streamUrl: row.streamUrl, 26 + source: "custom", 27 + description: row.description ?? undefined, 28 + genre: row.genre ?? undefined, 29 + homepage: row.homepage ?? undefined, 30 + logo: row.logoUrl ?? undefined, 31 + tags: row.tags ?? undefined, 32 + }; 33 + } 34 + 35 + /** GET /xrpc/fm.atradio.getFavorites?actor=&limit=&cursor= */ 36 + xrpcRouter.get("/fm.atradio.getFavorites", async (req: Request, res: Response) => { 37 + const actor = String(req.query.actor ?? ""); 38 + if (!actor) return xrpcError(res, 400, "InvalidRequest", "actor is required"); 39 + const did = await resolveDid(actor); 40 + if (!did) return xrpcError(res, 404, "NotFound", "actor not found"); 41 + 42 + const limit = parseLimit(req.query.limit); 43 + const cursor = req.query.cursor ? new Date(String(req.query.cursor)) : null; 44 + 45 + const rows = await db 46 + .select() 47 + .from(schema.favorites) 48 + .where( 49 + cursor 50 + ? and( 51 + eq(schema.favorites.did, did), 52 + lt(schema.favorites.indexedAt, cursor), 53 + ) 54 + : eq(schema.favorites.did, did), 55 + ) 56 + .orderBy(desc(schema.favorites.indexedAt)) 57 + .limit(limit + 1); 58 + 59 + const hasMore = rows.length > limit; 60 + const page = rows.slice(0, limit); 61 + const items: StationView[] = page.map((row) => ({ 62 + uri: row.uri, 63 + station: row.station, 64 + createdAt: (row.createdAt ?? row.indexedAt).toISOString(), 65 + })); 66 + const nextCursor = hasMore 67 + ? page[page.length - 1]?.indexedAt.toISOString() 68 + : undefined; 69 + 70 + return res.json({ cursor: nextCursor, items }); 71 + }); 72 + 73 + /** GET /xrpc/fm.atradio.getStations?actor=&limit=&cursor= */ 74 + xrpcRouter.get("/fm.atradio.getStations", async (req: Request, res: Response) => { 75 + const actor = String(req.query.actor ?? ""); 76 + if (!actor) return xrpcError(res, 400, "InvalidRequest", "actor is required"); 77 + const did = await resolveDid(actor); 78 + if (!did) return xrpcError(res, 404, "NotFound", "actor not found"); 79 + 80 + const limit = parseLimit(req.query.limit); 81 + const cursor = req.query.cursor ? new Date(String(req.query.cursor)) : null; 82 + 83 + const rows = await db 84 + .select() 85 + .from(schema.stations) 86 + .where( 87 + cursor 88 + ? and( 89 + eq(schema.stations.did, did), 90 + lt(schema.stations.indexedAt, cursor), 91 + ) 92 + : eq(schema.stations.did, did), 93 + ) 94 + .orderBy(desc(schema.stations.indexedAt)) 95 + .limit(limit + 1); 96 + 97 + const hasMore = rows.length > limit; 98 + const page = rows.slice(0, limit); 99 + const items: StationView[] = page.map((row) => ({ 100 + uri: row.uri, 101 + station: stationRowToInfo(row), 102 + createdAt: (row.createdAt ?? row.indexedAt).toISOString(), 103 + })); 104 + const nextCursor = hasMore 105 + ? page[page.length - 1]?.indexedAt.toISOString() 106 + : undefined; 107 + 108 + return res.json({ cursor: nextCursor, items }); 109 + }); 110 + 111 + /** GET /xrpc/fm.atradio.getRecentStations?limit= — global discovery. */ 112 + xrpcRouter.get( 113 + "/fm.atradio.getRecentStations", 114 + async (req: Request, res: Response) => { 115 + const limit = parseLimit(req.query.limit, 30, 100); 116 + const rows = await db 117 + .select() 118 + .from(schema.stations) 119 + .orderBy(desc(schema.stations.indexedAt)) 120 + .limit(limit); 121 + const items: StationView[] = rows.map((row) => ({ 122 + uri: row.uri, 123 + station: stationRowToInfo(row), 124 + createdAt: (row.createdAt ?? row.indexedAt).toISOString(), 125 + })); 126 + return res.json({ items }); 127 + }, 128 + ); 129 + 130 + /** GET /xrpc/fm.atradio.getPopularStations?limit= — most-favorited. */ 131 + xrpcRouter.get( 132 + "/fm.atradio.getPopularStations", 133 + async (req: Request, res: Response) => { 134 + const limit = parseLimit(req.query.limit, 30, 100); 135 + const rows = await db 136 + .select({ 137 + stationId: schema.favorites.stationId, 138 + station: sql<StationInfo>`(array_agg(${schema.favorites.station}))[1]`, 139 + count: sql<number>`count(*)::int`, 140 + }) 141 + .from(schema.favorites) 142 + .groupBy(schema.favorites.stationId) 143 + .orderBy(desc(sql`count(*)`)) 144 + .limit(limit); 145 + const items = rows.map((r) => ({ station: r.station, count: r.count })); 146 + return res.json({ items }); 147 + }, 148 + );
+10
apps/api/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.base.json", 3 + "compilerOptions": { 4 + "lib": ["ES2023"], 5 + "types": ["node"], 6 + "module": "ESNext", 7 + "moduleResolution": "bundler" 8 + }, 9 + "include": ["src", "drizzle.config.ts"] 10 + }
+8
apps/web/package.json
··· 14 14 "clean": "rm -rf dist .turbo node_modules" 15 15 }, 16 16 "dependencies": { 17 + "@atcute/atproto": "^3.1.10", 18 + "@atcute/client": "^4.2.1", 19 + "@atcute/identity-resolver": "^1.2.2", 20 + "@atcute/lexicons": "^1.3.1", 21 + "@atcute/oauth-browser-client": "^3.0.0", 22 + "@atcute/tid": "^1.1.2", 23 + "@atradio/lexicons": "workspace:*", 17 24 "@fontsource/jetbrains-mono": "^5.1.1", 18 25 "@fontsource/lexend": "^5.1.1", 19 26 "@fontsource/outfit": "^5.1.1", 20 27 "@heroui/react": "^3.2.2", 21 28 "@heroui/styles": "^3.2.2", 22 29 "@tabler/icons-react": "^3.26.0", 30 + "consola": "^3.3.3", 23 31 "@tanstack/react-query": "^5.62.11", 24 32 "@tanstack/react-router": "^1.95.1", 25 33 "@hookform/resolvers": "^3.10.0",
+13
apps/web/public/client-metadata.json
··· 1 + { 2 + "client_id": "https://atradio.fm/client-metadata.json", 3 + "client_name": "atradio.fm", 4 + "client_uri": "https://atradio.fm", 5 + "logo_uri": "https://atradio.fm/pwa-192x192.png", 6 + "redirect_uris": ["https://atradio.fm/oauth/callback"], 7 + "scope": "atproto transition:generic", 8 + "grant_types": ["authorization_code", "refresh_token"], 9 + "response_types": ["code"], 10 + "application_type": "web", 11 + "token_endpoint_auth_method": "none", 12 + "dpop_bound_access_tokens": true 13 + }
+26
apps/web/src/atoms/auth.ts
··· 1 + import { atom } from "jotai"; 2 + import { Client } from "@atcute/client"; 3 + import { OAuthUserAgent, type Session } from "@atcute/oauth-browser-client"; 4 + import type { Did } from "@atcute/lexicons"; 5 + import type { ActorProfile } from "@/lib/atproto/profile"; 6 + 7 + /** Current OAuth session (in-memory; the atcute store persists it in IndexedDB). */ 8 + export const sessionAtom = atom<Session | null>(null); 9 + 10 + /** The logged-in user's public profile (display name / handle / avatar). */ 11 + export const authProfileAtom = atom<ActorProfile | null>(null); 12 + 13 + /** True while the initial session restore is in flight. */ 14 + export const authLoadingAtom = atom(true); 15 + 16 + export const didAtom = atom<Did | null>( 17 + (get) => get(sessionAtom)?.info.sub ?? null, 18 + ); 19 + 20 + export const isLoggedInAtom = atom((get) => get(sessionAtom) !== null); 21 + 22 + /** Authenticated XRPC client, memoized per session. Null when logged out. */ 23 + export const clientAtom = atom<Client | null>((get) => { 24 + const s = get(sessionAtom); 25 + return s ? new Client({ handler: new OAuthUserAgent(s) }) : null; 26 + });
+67 -23
apps/web/src/atoms/customStations.ts
··· 1 1 import { atom } from "jotai"; 2 - import { atomWithStorage } from "jotai/utils"; 2 + import { consola } from "consola"; 3 + import { NSID } from "@atradio/lexicons"; 3 4 import type { CustomStationInput, Station } from "@/lib/types"; 5 + import { clientAtom, didAtom } from "./auth"; 6 + import { 7 + putStation, 8 + deleteAtradioRecord, 9 + type StoredStation, 10 + } from "@/lib/atproto/records"; 11 + 12 + /** The logged-in user's `fm.atradio.station` records (loaded from the PDS). */ 13 + export const customStationsAtom = atom<Station[]>([]); 4 14 5 - /** User-entered stations, persisted locally (no backend yet). */ 6 - export const customStationsAtom = atomWithStorage<Station[]>( 7 - "atradio:custom", 8 - [], 15 + /** station.id -> record rkey. */ 16 + export const customRkeysAtom = atom<Record<string, string>>({}); 17 + 18 + export const setCustomStationsAtom = atom( 19 + null, 20 + (_get, set, items: StoredStation[]) => { 21 + set( 22 + customStationsAtom, 23 + items.map((i) => i.station), 24 + ); 25 + set( 26 + customRkeysAtom, 27 + Object.fromEntries(items.map((i) => [i.station.id, i.rkey])), 28 + ); 29 + }, 9 30 ); 10 31 11 - function createId(): string { 12 - // Stable, unique-enough id without pulling in a uuid dep. 13 - const rand = Math.random().toString(36).slice(2, 10); 14 - return `custom:${Date.now().toString(36)}-${rand}`; 15 - } 32 + export const clearCustomStationsAtom = atom(null, (_get, set) => { 33 + set(customStationsAtom, []); 34 + set(customRkeysAtom, {}); 35 + }); 16 36 37 + /** Add a station: write the record to the PDS, then reflect it locally. */ 17 38 export const addCustomStationAtom = atom( 18 39 null, 19 - (get, set, input: CustomStationInput) => { 20 - const station: Station = { 21 - id: createId(), 22 - name: input.name.trim(), 23 - description: input.description?.trim() || undefined, 24 - genre: input.genre?.trim() || undefined, 25 - streamUrl: input.streamUrl.trim(), 26 - homepage: input.homepage?.trim() || undefined, 27 - favicon: input.logoUrl?.trim() || undefined, 28 - source: "custom", 29 - }; 40 + async (get, set, input: CustomStationInput): Promise<Station> => { 41 + const client = get(clientAtom); 42 + const did = get(didAtom); 43 + if (!client || !did) throw new Error("Not authenticated"); 44 + 45 + const { rkey, station } = await putStation(client, did, { 46 + name: input.name, 47 + streamUrl: input.streamUrl, 48 + description: input.description, 49 + genre: input.genre, 50 + homepage: input.homepage, 51 + logoUrl: input.logoUrl, 52 + }); 30 53 set(customStationsAtom, [station, ...get(customStationsAtom)]); 54 + set(customRkeysAtom, { ...get(customRkeysAtom), [station.id]: rkey }); 31 55 return station; 32 56 }, 33 57 ); 34 58 59 + /** Remove a station: optimistic local delete + PDS delete, rollback on error. */ 35 60 export const removeCustomStationAtom = atom( 36 61 null, 37 - (get, set, id: string) => { 62 + async (get, set, id: string) => { 63 + const client = get(clientAtom); 64 + const did = get(didAtom); 65 + const current = get(customStationsAtom); 66 + const rkeys = get(customRkeysAtom); 67 + const rkey = rkeys[id]; 68 + 38 69 set( 39 70 customStationsAtom, 40 - get(customStationsAtom).filter((s) => s.id !== id), 71 + current.filter((s) => s.id !== id), 41 72 ); 73 + const nextRkeys = { ...rkeys }; 74 + delete nextRkeys[id]; 75 + set(customRkeysAtom, nextRkeys); 76 + 77 + if (client && did && rkey) { 78 + try { 79 + await deleteAtradioRecord(client, did, NSID.station, rkey); 80 + } catch (err) { 81 + consola.error("[stations] remove failed", err); 82 + set(customStationsAtom, current); 83 + set(customRkeysAtom, rkeys); 84 + } 85 + } 42 86 }, 43 87 );
+80 -20
apps/web/src/atoms/favorites.ts
··· 1 1 import { atom } from "jotai"; 2 - import { atomWithStorage } from "jotai/utils"; 2 + import { consola } from "consola"; 3 + import { NSID } from "@atradio/lexicons"; 3 4 import type { Station } from "@/lib/types"; 5 + import { clientAtom, didAtom } from "./auth"; 6 + import { 7 + putFavorite, 8 + deleteAtradioRecord, 9 + type StoredStation, 10 + } from "@/lib/atproto/records"; 4 11 5 12 /** 6 - * Favorites are stored as full Station objects (not just ids) so the profile 7 - * page can render them without re-querying any provider — important since we 8 - * have no backend and TuneIn/radio-browser results aren't otherwise persisted. 13 + * Favorites are the logged-in user's `fm.atradio.favorite` records, held in 14 + * memory (loaded from the PDS on login). Writes go straight to the PDS with an 15 + * optimistic local update. 9 16 */ 10 - export const favoritesAtom = atomWithStorage<Station[]>( 11 - "atradio:favorites", 12 - [], 17 + export const favoritesAtom = atom<Station[]>([]); 18 + 19 + /** station.id -> record rkey, so we can delete the right record. */ 20 + export const favoriteRkeysAtom = atom<Record<string, string>>({}); 21 + 22 + /** Derived set of favorited station ids for O(1) membership checks. */ 23 + export const favoriteIdsAtom = atom( 24 + (get) => new Set(get(favoritesAtom).map((s) => s.id)), 13 25 ); 14 26 15 - /** Set of favorited station ids, derived for O(1) membership checks. */ 16 - export const favoriteIdsAtom = atom((get) => { 17 - return new Set(get(favoritesAtom).map((s) => s.id)); 18 - }); 27 + /** Replace the favorites list (used by the on-login loader). */ 28 + export const setFavoritesAtom = atom( 29 + null, 30 + (_get, set, items: StoredStation[]) => { 31 + set( 32 + favoritesAtom, 33 + items.map((i) => i.station), 34 + ); 35 + set( 36 + favoriteRkeysAtom, 37 + Object.fromEntries(items.map((i) => [i.station.id, i.rkey])), 38 + ); 39 + }, 40 + ); 19 41 20 - export const toggleFavoriteAtom = atom(null, (get, set, station: Station) => { 21 - const current = get(favoritesAtom); 22 - const exists = current.some((s) => s.id === station.id); 23 - set( 24 - favoritesAtom, 25 - exists 26 - ? current.filter((s) => s.id !== station.id) 27 - : [station, ...current], 28 - ); 42 + export const clearFavoritesAtom = atom(null, (_get, set) => { 43 + set(favoritesAtom, []); 44 + set(favoriteRkeysAtom, {}); 29 45 }); 46 + 47 + /** Toggle a favorite: optimistic local update + PDS write, rollback on error. */ 48 + export const toggleFavoriteAtom = atom( 49 + null, 50 + async (get, set, station: Station) => { 51 + const client = get(clientAtom); 52 + const did = get(didAtom); 53 + if (!client || !did) return; // gated at the UI; nothing to do 54 + 55 + const current = get(favoritesAtom); 56 + const rkeys = get(favoriteRkeysAtom); 57 + const exists = current.some((s) => s.id === station.id); 58 + 59 + if (exists) { 60 + const rkey = rkeys[station.id]; 61 + set( 62 + favoritesAtom, 63 + current.filter((s) => s.id !== station.id), 64 + ); 65 + const nextRkeys = { ...rkeys }; 66 + delete nextRkeys[station.id]; 67 + set(favoriteRkeysAtom, nextRkeys); 68 + try { 69 + if (rkey) await deleteAtradioRecord(client, did, NSID.favorite, rkey); 70 + } catch (err) { 71 + consola.error("[favorites] remove failed", err); 72 + set(favoritesAtom, current); 73 + set(favoriteRkeysAtom, rkeys); 74 + } 75 + } else { 76 + set(favoritesAtom, [station, ...current]); 77 + try { 78 + const rkey = await putFavorite(client, did, station); 79 + set(favoriteRkeysAtom, { 80 + ...get(favoriteRkeysAtom), 81 + [station.id]: rkey, 82 + }); 83 + } catch (err) { 84 + consola.error("[favorites] add failed", err); 85 + set(favoritesAtom, current); 86 + } 87 + } 88 + }, 89 + );
+3
apps/web/src/atoms/ui.ts
··· 9 9 /** Controls the Raycast-style quick-search palette. */ 10 10 export const searchPaletteOpenAtom = atom(false); 11 11 12 + /** Controls the AT Proto login modal. */ 13 + export const loginModalOpenAtom = atom(false); 14 + 12 15 /** Seed query for the palette (e.g. when opened from a category tile). */ 13 16 export const searchPaletteQueryAtom = atom(""); 14 17
+20 -11
apps/web/src/components/AddStationModal.tsx
··· 12 12 FieldError, 13 13 useOverlayState, 14 14 } from "@heroui/react"; 15 + import { consola } from "consola"; 15 16 import { InlineLoader } from "./Skeletons"; 16 17 import { IconPlus, IconBroadcast, IconPhoto } from "@tabler/icons-react"; 17 18 import { addStationOpenAtom } from "@/atoms/ui"; ··· 43 44 const addStation = useSetAtom(addCustomStationAtom); 44 45 const play = useSetAtom(playStationAtom); 45 46 const [logoError, setLogoError] = useState(false); 47 + const [saveError, setSaveError] = useState<string | null>(null); 46 48 47 49 const { 48 50 register, ··· 61 63 setOpen(false); 62 64 reset(DEFAULTS); 63 65 setLogoError(false); 66 + setSaveError(null); 64 67 }; 65 68 66 69 const state = useOverlayState({ ··· 68 71 onOpenChange: (open) => (open ? setOpen(true) : closeAndReset()), 69 72 }); 70 73 71 - const persist = (values: StationFormValues, playNow: boolean) => { 72 - const station = addStation({ 73 - name: values.name, 74 - description: values.description, 75 - genre: values.genre, 76 - streamUrl: values.streamUrl, 77 - homepage: values.homepage || undefined, 78 - logoUrl: values.logoUrl || undefined, 79 - }); 80 - if (playNow) play(station); 81 - closeAndReset(); 74 + const persist = async (values: StationFormValues, playNow: boolean) => { 75 + try { 76 + const station = await addStation({ 77 + name: values.name, 78 + description: values.description, 79 + genre: values.genre, 80 + streamUrl: values.streamUrl, 81 + homepage: values.homepage || undefined, 82 + logoUrl: values.logoUrl || undefined, 83 + }); 84 + if (playNow) play(station); 85 + closeAndReset(); 86 + } catch (err) { 87 + consola.error("[stations] save failed", err); 88 + setSaveError("Couldn't save the station to your account. Try again."); 89 + } 82 90 }; 83 91 84 92 const onSave = handleSubmit((values) => persist(values, false)); ··· 223 231 Add it anyway (skip the stream check) 224 232 </button> 225 233 )} 234 + {saveError && <p className="text-xs text-danger">{saveError}</p>} 226 235 </Modal.Body> 227 236 228 237 <Modal.Footer className="flex justify-end gap-2 border-t border-white/10 pt-3">
+39
apps/web/src/components/AuthInitializer.tsx
··· 1 + import { useEffect } from "react"; 2 + import { useSetAtom } from "jotai"; 3 + import { 4 + sessionAtom, 5 + authProfileAtom, 6 + authLoadingAtom, 7 + } from "@/atoms/auth"; 8 + import { restoreSession } from "@/lib/atproto/session"; 9 + import { getProfile } from "@/lib/atproto/profile"; 10 + 11 + /** Restores any stored OAuth session on app start; renders nothing. */ 12 + export function AuthInitializer() { 13 + const setSession = useSetAtom(sessionAtom); 14 + const setProfile = useSetAtom(authProfileAtom); 15 + const setLoading = useSetAtom(authLoadingAtom); 16 + 17 + useEffect(() => { 18 + let cancelled = false; 19 + (async () => { 20 + const session = await restoreSession(); 21 + if (cancelled) return; 22 + setSession(session); 23 + if (session) { 24 + try { 25 + const profile = await getProfile(session.info.sub); 26 + if (!cancelled) setProfile(profile); 27 + } catch { 28 + /* profile is best-effort */ 29 + } 30 + } 31 + if (!cancelled) setLoading(false); 32 + })(); 33 + return () => { 34 + cancelled = true; 35 + }; 36 + }, [setSession, setProfile, setLoading]); 37 + 38 + return null; 39 + }
+4
apps/web/src/components/Layout.tsx
··· 6 6 import { AddStationModal } from "./AddStationModal"; 7 7 import { ShortcutsHelp } from "./ShortcutsHelp"; 8 8 import { SearchPalette } from "./SearchPalette"; 9 + import { LoginModal } from "./LoginModal"; 10 + import { UserDataSync } from "./UserDataSync"; 9 11 import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts"; 10 12 import { shortcutsOpenAtom } from "@/atoms/ui"; 11 13 ··· 34 36 <AddStationModal /> 35 37 <ShortcutsHelp /> 36 38 <SearchPalette /> 39 + <LoginModal /> 40 + <UserDataSync /> 37 41 </div> 38 42 ); 39 43 }
+90
apps/web/src/components/LoginModal.tsx
··· 1 + import { useState } from "react"; 2 + import { useAtom } from "jotai"; 3 + import { Modal, Button, useOverlayState } from "@heroui/react"; 4 + import { IconAt, IconBrandBluesky } from "@tabler/icons-react"; 5 + import { consola } from "consola"; 6 + import { loginModalOpenAtom } from "@/atoms/ui"; 7 + import { startLogin } from "@/lib/atproto/session"; 8 + 9 + export function LoginModal() { 10 + const [isOpen, setOpen] = useAtom(loginModalOpenAtom); 11 + const state = useOverlayState({ isOpen, onOpenChange: setOpen }); 12 + const [handle, setHandle] = useState(""); 13 + const [busy, setBusy] = useState(false); 14 + const [error, setError] = useState<string | null>(null); 15 + 16 + const submit = async () => { 17 + const h = handle.trim().replace(/^@/, ""); 18 + if (!h) return; 19 + setBusy(true); 20 + setError(null); 21 + try { 22 + await startLogin(h); // full-page redirect to the PDS 23 + } catch (err) { 24 + consola.error("[auth] login failed", err); 25 + setBusy(false); 26 + setError("Couldn't start login. Check your handle and try again."); 27 + } 28 + }; 29 + 30 + return ( 31 + <Modal state={state}> 32 + <Modal.Backdrop variant="blur"> 33 + <Modal.Container placement="center" size="sm"> 34 + <Modal.Dialog className="mx-4 w-[calc(100vw-2rem)] max-w-sm border border-white/10 bg-synth-surface"> 35 + <Modal.Header className="border-b border-white/10 pb-3"> 36 + <Modal.Heading className="font-display text-lg"> 37 + Log in with Atmosphere account 38 + </Modal.Heading> 39 + </Modal.Header> 40 + <Modal.Body className="flex flex-col gap-4 py-4"> 41 + <div className="flex flex-col gap-2"> 42 + <div className="flex items-center gap-2 rounded-lg border border-white/15 bg-synth-panel px-3 focus-within:border-synth-cyan"> 43 + <IconAt size={16} className="text-foreground/40" /> 44 + <input 45 + autoFocus 46 + value={handle} 47 + onChange={(e) => setHandle(e.target.value)} 48 + onKeyDown={(e) => { 49 + if (e.key === "Enter") void submit(); 50 + }} 51 + placeholder="atmosphere.handle" 52 + className="h-10 w-full bg-transparent text-sm text-foreground placeholder:text-foreground/30 focus:outline-none" 53 + autoComplete="username" 54 + spellCheck={false} 55 + /> 56 + </div> 57 + {error && <p className="text-xs text-danger">{error}</p>} 58 + <Button 59 + variant="primary" 60 + className="w-full" 61 + isDisabled={busy || !handle.trim()} 62 + onPress={() => void submit()} 63 + > 64 + Log in 65 + </Button> 66 + </div> 67 + 68 + {/* Signup */} 69 + <div className="flex flex-col gap-2 rounded-lg border border-white/10 bg-synth-panel/50 p-3"> 70 + <p className="text-xs text-foreground/60"> 71 + atradio is part of the Atmosphere. Create an Atmosphere account 72 + on Bluesky to get started! 73 + </p> 74 + <a 75 + href="https://bsky.app" 76 + target="_blank" 77 + rel="noreferrer" 78 + className="flex items-center justify-center gap-1.5 rounded-full bg-synth-blue/15 px-3 py-2 text-sm font-medium text-synth-blue transition-colors hover:bg-synth-blue/25" 79 + > 80 + <IconBrandBluesky size={16} /> 81 + Signup via Bluesky 82 + </a> 83 + </div> 84 + </Modal.Body> 85 + </Modal.Dialog> 86 + </Modal.Container> 87 + </Modal.Backdrop> 88 + </Modal> 89 + ); 90 + }
+45 -15
apps/web/src/components/Navbar.tsx
··· 1 1 import { Link } from "@tanstack/react-router"; 2 2 import { useSetAtom } from "jotai"; 3 3 import { Button } from "@heroui/react"; 4 - import { IconSearch, IconUser, IconPlus } from "@tabler/icons-react"; 4 + import { 5 + IconSearch, 6 + IconPlus, 7 + IconLogin2, 8 + IconUserCircle, 9 + } from "@tabler/icons-react"; 5 10 import { addStationOpenAtom, openSearchPaletteAtom } from "@/atoms/ui"; 11 + import { useRequireAuth } from "@/hooks/useRequireAuth"; 12 + import { useAuth } from "@/hooks/useAuth"; 6 13 7 14 const navLinkBase = 8 15 "flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm text-foreground/60 transition-colors hover:text-foreground"; 9 - const navLinkActive = "bg-white/10 !text-synth-cyan"; 10 16 11 17 export function Navbar() { 12 18 const openAddStation = useSetAtom(addStationOpenAtom); 13 19 const openSearch = useSetAtom(openSearchPaletteAtom); 14 - 15 - const triggerSearch = () => openSearch(); 20 + const ensureAuth = useRequireAuth(); 21 + const { isLoggedIn, profile, openLogin } = useAuth(); 16 22 17 23 return ( 18 24 <header className="sticky top-0 z-40 border-b border-white/10 bg-synth-bg/80 backdrop-blur-xl"> ··· 24 30 </Link> 25 31 26 32 <nav className="flex items-center gap-1"> 27 - <button type="button" onClick={triggerSearch} className={navLinkBase}> 33 + <button 34 + type="button" 35 + onClick={() => openSearch()} 36 + className={navLinkBase} 37 + > 28 38 <IconSearch size={16} /> 29 39 <span className="hidden items-center gap-1.5 sm:inline-flex"> 30 40 Search ··· 33 43 </kbd> 34 44 </span> 35 45 </button> 36 - <Link 37 - to="/profile" 38 - className={navLinkBase} 39 - activeProps={{ className: navLinkActive }} 40 - > 41 - <IconUser size={16} /> 42 - <span className="hidden sm:inline">Profile</span> 43 - </Link> 44 46 45 47 <Button 46 48 size="sm" 47 49 variant="primary" 48 - className="ml-1 gap-1.5 rounded-full" 49 - onPress={() => openAddStation(true)} 50 + className="gap-1.5 rounded-full" 51 + onPress={() => ensureAuth(() => openAddStation(true))} 50 52 > 51 53 <IconPlus size={16} /> 52 54 <span className="hidden sm:inline">Add station</span> 53 55 <span className="sm:hidden">Add</span> 54 56 </Button> 57 + 58 + {isLoggedIn ? ( 59 + <Link 60 + to="/profile" 61 + className="ml-1 flex h-9 w-9 items-center justify-center overflow-hidden rounded-full border border-white/15 bg-synth-panel" 62 + title="Your profile" 63 + > 64 + {profile?.avatar ? ( 65 + <img 66 + src={profile.avatar} 67 + alt={profile.handle} 68 + className="h-full w-full object-cover" 69 + /> 70 + ) : ( 71 + <IconUserCircle size={20} className="text-foreground/70" /> 72 + )} 73 + </Link> 74 + ) : ( 75 + <Button 76 + size="sm" 77 + variant="tertiary" 78 + className="ml-1 gap-1.5 rounded-full !bg-white/5" 79 + onPress={() => openLogin(true)} 80 + > 81 + <IconLogin2 size={16} /> 82 + <span className="hidden sm:inline">Sign in</span> 83 + </Button> 84 + )} 55 85 </nav> 56 86 </div> 57 87 </header>
+3 -1
apps/web/src/components/Player.tsx
··· 22 22 volumeAtom, 23 23 } from "@/atoms/player"; 24 24 import { favoriteIdsAtom, toggleFavoriteAtom } from "@/atoms/favorites"; 25 + import { useRequireAuth } from "@/hooks/useRequireAuth"; 25 26 import { resolveStream } from "@/lib/audio/resolve"; 26 27 import { watchIcyMetadata } from "@/lib/audio/icyMetadata"; 27 28 import { registerRadioBrowserClick } from "@/lib/api/radioBrowser"; ··· 44 45 const [nowPlaying, setNowPlaying] = useAtom(nowPlayingAtom); 45 46 const favoriteIds = useAtomValue(favoriteIdsAtom); 46 47 const toggleFavorite = useSetAtom(toggleFavoriteAtom); 48 + const ensureAuth = useRequireAuth(); 47 49 48 50 const audioRef = useRef<HTMLAudioElement>(null); 49 51 const hlsRef = useRef<Hls | null>(null); ··· 242 244 variant="tertiary" 243 245 className="rounded-full" 244 246 aria-label="Toggle favorite" 245 - onPress={() => toggleFavorite(station)} 247 + onPress={() => ensureAuth(() => toggleFavorite(station))} 246 248 > 247 249 {isFavorite ? ( 248 250 <IconHeartFilled size={16} className="text-synth-pink" />
+3 -1
apps/web/src/components/StationCard.tsx
··· 16 16 togglePlayAtom, 17 17 } from "@/atoms/player"; 18 18 import { favoriteIdsAtom, toggleFavoriteAtom } from "@/atoms/favorites"; 19 + import { useRequireAuth } from "@/hooks/useRequireAuth"; 19 20 import { StationLogo } from "./StationLogo"; 20 21 import { AudioBars } from "./AudioBars"; 21 22 ··· 42 43 const togglePlay = useSetAtom(togglePlayAtom); 43 44 const favoriteIds = useAtomValue(favoriteIdsAtom); 44 45 const toggleFavorite = useSetAtom(toggleFavoriteAtom); 46 + const ensureAuth = useRequireAuth(); 45 47 46 48 const isCurrent = current?.id === station.id; 47 49 const isActive = isCurrent && isPlaying; ··· 127 129 : "!bg-white/5 hover:!bg-white/10" 128 130 }`} 129 131 aria-label={isFavorite ? "Remove favorite" : "Add to favorites"} 130 - onPress={() => toggleFavorite(station)} 132 + onPress={() => ensureAuth(() => toggleFavorite(station))} 131 133 > 132 134 {isFavorite ? ( 133 135 <IconHeartFilled size={16} className="text-synth-pink" />
+54
apps/web/src/components/UserDataSync.tsx
··· 1 + import { useEffect } from "react"; 2 + import { useAtomValue, useSetAtom } from "jotai"; 3 + import { consola } from "consola"; 4 + import { clientAtom, didAtom } from "@/atoms/auth"; 5 + import { setFavoritesAtom, clearFavoritesAtom } from "@/atoms/favorites"; 6 + import { 7 + setCustomStationsAtom, 8 + clearCustomStationsAtom, 9 + } from "@/atoms/customStations"; 10 + import { listFavorites, listStations } from "@/lib/atproto/records"; 11 + 12 + /** Loads the logged-in user's favorites/stations from their PDS; renders nothing. */ 13 + export function UserDataSync() { 14 + const client = useAtomValue(clientAtom); 15 + const did = useAtomValue(didAtom); 16 + const setFavorites = useSetAtom(setFavoritesAtom); 17 + const setStations = useSetAtom(setCustomStationsAtom); 18 + const clearFavorites = useSetAtom(clearFavoritesAtom); 19 + const clearStations = useSetAtom(clearCustomStationsAtom); 20 + 21 + useEffect(() => { 22 + if (!client || !did) { 23 + clearFavorites(); 24 + clearStations(); 25 + return; 26 + } 27 + let cancelled = false; 28 + (async () => { 29 + try { 30 + const [favorites, stations] = await Promise.all([ 31 + listFavorites(client, did), 32 + listStations(client, did), 33 + ]); 34 + if (cancelled) return; 35 + setFavorites(favorites); 36 + setStations(stations); 37 + } catch (err) { 38 + consola.error("[userdata] failed to load records", err); 39 + } 40 + })(); 41 + return () => { 42 + cancelled = true; 43 + }; 44 + }, [ 45 + client, 46 + did, 47 + setFavorites, 48 + setStations, 49 + clearFavorites, 50 + clearStations, 51 + ]); 52 + 53 + return null; 54 + }
+32
apps/web/src/hooks/useAuth.ts
··· 1 + import { useCallback } from "react"; 2 + import { useAtomValue, useSetAtom } from "jotai"; 3 + import { consola } from "consola"; 4 + import { 5 + sessionAtom, 6 + authProfileAtom, 7 + authLoadingAtom, 8 + didAtom, 9 + isLoggedInAtom, 10 + } from "@/atoms/auth"; 11 + import { loginModalOpenAtom } from "@/atoms/ui"; 12 + import { endSession } from "@/lib/atproto/session"; 13 + 14 + export function useAuth() { 15 + const session = useAtomValue(sessionAtom); 16 + const setSession = useSetAtom(sessionAtom); 17 + const setProfile = useSetAtom(authProfileAtom); 18 + const profile = useAtomValue(authProfileAtom); 19 + const did = useAtomValue(didAtom); 20 + const isLoggedIn = useAtomValue(isLoggedInAtom); 21 + const loading = useAtomValue(authLoadingAtom); 22 + const openLogin = useSetAtom(loginModalOpenAtom); 23 + 24 + const logout = useCallback(async () => { 25 + if (session) await endSession(session); 26 + setSession(null); 27 + setProfile(null); 28 + consola.info("[auth] signed out"); 29 + }, [session, setSession, setProfile]); 30 + 31 + return { isLoggedIn, did, profile, loading, logout, openLogin }; 32 + }
+12 -2
apps/web/src/hooks/useKeyboardShortcuts.ts
··· 7 7 volumeAtom, 8 8 } from "@/atoms/player"; 9 9 import { toggleFavoriteAtom } from "@/atoms/favorites"; 10 + import { isLoggedInAtom } from "@/atoms/auth"; 10 11 import { 11 12 addStationOpenAtom, 12 13 shortcutsOpenAtom, 13 14 openSearchPaletteAtom, 15 + loginModalOpenAtom, 14 16 } from "@/atoms/ui"; 15 17 16 18 function isTypingTarget(el: EventTarget | null): boolean { ··· 35 37 const setVolume = useSetAtom(volumeAtom); 36 38 const setAddOpen = useSetAtom(addStationOpenAtom); 37 39 const openSearch = useSetAtom(openSearchPaletteAtom); 40 + const openLogin = useSetAtom(loginModalOpenAtom); 41 + const isLoggedIn = useAtomValue(isLoggedInAtom); 38 42 const [shortcutsOpen, setShortcutsOpen] = useAtom(shortcutsOpenAtom); 39 43 40 44 useEffect(() => { ··· 75 79 break; 76 80 case "f": 77 81 case "F": 78 - if (currentStation) toggleFavorite(currentStation); 82 + if (currentStation) { 83 + if (isLoggedIn) toggleFavorite(currentStation); 84 + else openLogin(true); 85 + } 79 86 break; 80 87 case "a": 81 88 case "A": 82 89 e.preventDefault(); 83 - setAddOpen(true); 90 + if (isLoggedIn) setAddOpen(true); 91 + else openLogin(true); 84 92 break; 85 93 case "ArrowUp": 86 94 e.preventDefault(); ··· 110 118 setVolume, 111 119 setAddOpen, 112 120 openSearch, 121 + openLogin, 122 + isLoggedIn, 113 123 shortcutsOpen, 114 124 setShortcutsOpen, 115 125 ]);
+21
apps/web/src/hooks/useRequireAuth.ts
··· 1 + import { useCallback } from "react"; 2 + import { useAtomValue, useSetAtom } from "jotai"; 3 + import { isLoggedInAtom } from "@/atoms/auth"; 4 + import { loginModalOpenAtom } from "@/atoms/ui"; 5 + 6 + /** 7 + * Returns `ensureAuth(fn)`: runs `fn` when logged in, otherwise opens the login 8 + * modal. Used to gate the saving actions (favorite / add / remove). 9 + */ 10 + export function useRequireAuth() { 11 + const isLoggedIn = useAtomValue(isLoggedInAtom); 12 + const openLogin = useSetAtom(loginModalOpenAtom); 13 + 14 + return useCallback( 15 + (fn: () => void) => { 16 + if (isLoggedIn) fn(); 17 + else openLogin(true); 18 + }, 19 + [isLoggedIn, openLogin], 20 + ); 21 + }
+51
apps/web/src/lib/atproto/client.ts
··· 1 + import { configureOAuth } from "@atcute/oauth-browser-client"; 2 + import { actorResolver } from "./resolver"; 3 + 4 + /** 5 + * `transition:generic` grants broad repo read/write today and is the most 6 + * widely-supported scope; combined with `atproto` it lets us read/write our 7 + * `fm.atradio.*` records in the user's PDS. 8 + */ 9 + export const OAUTH_SCOPE = "atproto transition:generic"; 10 + 11 + /** Production origin (client-metadata + redirect must live here). */ 12 + const PROD_ORIGIN = "https://atradio.fm"; 13 + 14 + function appOrigin(): string { 15 + return import.meta.env.DEV ? window.location.origin : PROD_ORIGIN; 16 + } 17 + 18 + export function redirectUri(): string { 19 + return `${appOrigin()}/oauth/callback`; 20 + } 21 + 22 + function clientId(): string { 23 + if (import.meta.env.DEV) { 24 + // AT Proto loopback dev client: `client_id` is `http://localhost` with the 25 + // redirect_uri + scope carried as query params. Open the app at 26 + // http://127.0.0.1:<port> so the redirect stays on a loopback IP (the spec 27 + // disallows the `localhost` hostname for redirect URIs). 28 + const params = new URLSearchParams({ 29 + redirect_uri: redirectUri(), 30 + scope: OAUTH_SCOPE, 31 + }); 32 + return `http://localhost?${params.toString()}`; 33 + } 34 + return `${appOrigin()}/client-metadata.json`; 35 + } 36 + 37 + let configured = false; 38 + 39 + /** Configure the atcute browser OAuth client exactly once. */ 40 + export function configureAtradioOAuth(): void { 41 + if (configured) return; 42 + configured = true; 43 + 44 + configureOAuth({ 45 + metadata: { 46 + client_id: clientId(), 47 + redirect_uri: redirectUri(), 48 + }, 49 + identityResolver: actorResolver, 50 + }); 51 + }
+33
apps/web/src/lib/atproto/profile.ts
··· 1 + export interface ActorProfile { 2 + did: string; 3 + handle: string; 4 + displayName?: string; 5 + avatar?: string; 6 + description?: string; 7 + } 8 + 9 + /** 10 + * Fetch a public profile (display name / handle / avatar) from the unauthenticated 11 + * Bluesky AppView. Plain fetch keeps us off the `@atcute/bluesky` lexicon dep. 12 + */ 13 + export async function getProfile(actor: string): Promise<ActorProfile> { 14 + const url = 15 + "https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=" + 16 + encodeURIComponent(actor); 17 + const res = await fetch(url, { headers: { Accept: "application/json" } }); 18 + if (!res.ok) throw new Error(`getProfile ${res.status}`); 19 + const d = (await res.json()) as { 20 + did: string; 21 + handle: string; 22 + displayName?: string; 23 + avatar?: string; 24 + description?: string; 25 + }; 26 + return { 27 + did: d.did, 28 + handle: d.handle, 29 + displayName: d.displayName, 30 + avatar: d.avatar, 31 + description: d.description, 32 + }; 33 + }
+29
apps/web/src/lib/atproto/publicReader.ts
··· 1 + import { Client, simpleFetchHandler } from "@atcute/client"; 2 + import type { Did } from "@atcute/lexicons"; 3 + import { resolveActor } from "./resolver"; 4 + import { listFavorites, listStations, type StoredStation } from "./records"; 5 + 6 + export interface PublicUser { 7 + did: Did; 8 + handle: string; 9 + favorites: StoredStation[]; 10 + stations: StoredStation[]; 11 + } 12 + 13 + /** 14 + * Read any user's atradio records straight from their PDS (public, no auth). 15 + * `com.atproto.repo.listRecords` is a public endpoint, so this works for any 16 + * actor and keeps the profile page independent of our AppView being online. 17 + */ 18 + export async function readPublicUser(actor: string): Promise<PublicUser> { 19 + const resolved = await resolveActor(actor); 20 + const did = resolved.did as Did; 21 + const client = new Client({ 22 + handler: simpleFetchHandler({ service: resolved.pds }), 23 + }); 24 + const [favorites, stations] = await Promise.all([ 25 + listFavorites(client, did), 26 + listStations(client, did), 27 + ]); 28 + return { did, handle: resolved.handle, favorites, stations }; 29 + }
+123
apps/web/src/lib/atproto/records.ts
··· 1 + import "@atcute/atproto"; 2 + import { ok, type Client } from "@atcute/client"; 3 + import { now as tidNow } from "@atcute/tid"; 4 + import type { Did, Nsid } from "@atcute/lexicons"; 5 + import { 6 + NSID, 7 + buildFavoriteRecord, 8 + buildStationRecord, 9 + favoriteRecordToStation, 10 + stationRecordToStation, 11 + rkeyFromUri, 12 + type FavoriteRecord, 13 + type StationRecord, 14 + type Station, 15 + type StationDraft, 16 + } from "@atradio/lexicons"; 17 + 18 + export interface StoredStation { 19 + station: Station; 20 + rkey: string; 21 + uri: string; 22 + } 23 + 24 + async function listAll( 25 + client: Client, 26 + repo: Did, 27 + collection: string, 28 + ): Promise<{ uri: string; value: unknown }[]> { 29 + const out: { uri: string; value: unknown }[] = []; 30 + let cursor: string | undefined; 31 + do { 32 + const page = await ok( 33 + client.get("com.atproto.repo.listRecords", { 34 + params: { repo, collection: collection as Nsid, limit: 100, cursor }, 35 + }), 36 + ); 37 + for (const r of page.records) out.push({ uri: r.uri, value: r.value }); 38 + cursor = page.cursor; 39 + } while (cursor); 40 + return out; 41 + } 42 + 43 + export async function listFavorites( 44 + client: Client, 45 + did: Did, 46 + ): Promise<StoredStation[]> { 47 + const records = await listAll(client, did, NSID.favorite); 48 + return records.map((r) => { 49 + const rec = r.value as unknown as FavoriteRecord; 50 + return { 51 + station: favoriteRecordToStation(rec), 52 + rkey: rkeyFromUri(r.uri), 53 + uri: r.uri, 54 + }; 55 + }); 56 + } 57 + 58 + export async function listStations( 59 + client: Client, 60 + did: Did, 61 + ): Promise<StoredStation[]> { 62 + const records = await listAll(client, did, NSID.station); 63 + return records.map((r) => { 64 + const rec = r.value as unknown as StationRecord; 65 + const rkey = rkeyFromUri(r.uri); 66 + return { station: stationRecordToStation(rec, rkey), rkey, uri: r.uri }; 67 + }); 68 + } 69 + 70 + export async function putFavorite( 71 + client: Client, 72 + did: Did, 73 + station: Station, 74 + ): Promise<string> { 75 + const rkey = tidNow(); 76 + await ok( 77 + client.post("com.atproto.repo.putRecord", { 78 + input: { 79 + repo: did, 80 + collection: NSID.favorite as Nsid, 81 + rkey, 82 + record: buildFavoriteRecord(station) as unknown as Record< 83 + string, 84 + unknown 85 + >, 86 + }, 87 + }), 88 + ); 89 + return rkey; 90 + } 91 + 92 + export async function putStation( 93 + client: Client, 94 + did: Did, 95 + draft: StationDraft, 96 + ): Promise<{ rkey: string; station: Station }> { 97 + const rkey = tidNow(); 98 + const record = buildStationRecord(draft); 99 + await ok( 100 + client.post("com.atproto.repo.putRecord", { 101 + input: { 102 + repo: did, 103 + collection: NSID.station as Nsid, 104 + rkey, 105 + record: record as unknown as Record<string, unknown>, 106 + }, 107 + }), 108 + ); 109 + return { rkey, station: stationRecordToStation(record, rkey) }; 110 + } 111 + 112 + export async function deleteAtradioRecord( 113 + client: Client, 114 + did: Did, 115 + collection: string, 116 + rkey: string, 117 + ): Promise<void> { 118 + await ok( 119 + client.post("com.atproto.repo.deleteRecord", { 120 + input: { repo: did, collection: collection as Nsid, rkey }, 121 + }), 122 + ); 123 + }
+34
apps/web/src/lib/atproto/resolver.ts
··· 1 + import { 2 + LocalActorResolver, 3 + CompositeHandleResolver, 4 + WellKnownHandleResolver, 5 + DohJsonHandleResolver, 6 + CompositeDidDocumentResolver, 7 + PlcDidDocumentResolver, 8 + WebDidDocumentResolver, 9 + } from "@atcute/identity-resolver"; 10 + import type { ActorIdentifier } from "@atcute/lexicons"; 11 + 12 + /** Shared handle→DID→PDS resolver used by OAuth config and public reads. */ 13 + export const actorResolver = new LocalActorResolver({ 14 + handleResolver: new CompositeHandleResolver({ 15 + strategy: "race", 16 + methods: { 17 + http: new WellKnownHandleResolver(), 18 + dns: new DohJsonHandleResolver({ 19 + dohUrl: "https://mozilla.cloudflare-dns.com/dns-query", 20 + }), 21 + }, 22 + }), 23 + didDocumentResolver: new CompositeDidDocumentResolver({ 24 + methods: { 25 + plc: new PlcDidDocumentResolver(), 26 + web: new WebDidDocumentResolver(), 27 + }, 28 + }), 29 + }); 30 + 31 + /** Resolve a handle or DID into `{ did, handle, pds }`. */ 32 + export function resolveActor(actor: string) { 33 + return actorResolver.resolve(actor as ActorIdentifier); 34 + }
+61
apps/web/src/lib/atproto/session.ts
··· 1 + import "@atcute/atproto"; // side-effect: augments the ambient com.atproto.* XRPC maps 2 + import { 3 + createAuthorizationUrl, 4 + finalizeAuthorization, 5 + getSession, 6 + listStoredSessions, 7 + OAuthUserAgent, 8 + type Session, 9 + } from "@atcute/oauth-browser-client"; 10 + import { Client } from "@atcute/client"; 11 + import type { ActorIdentifier } from "@atcute/lexicons"; 12 + import { consola } from "consola"; 13 + import { OAUTH_SCOPE, configureAtradioOAuth } from "./client"; 14 + 15 + export type { Session }; 16 + 17 + /** Begin login for a handle/DID — redirects the browser to the PDS. */ 18 + export async function startLogin(handle: string): Promise<void> { 19 + configureAtradioOAuth(); 20 + const identifier = handle.replace(/^@/, "").trim(); 21 + const url = await createAuthorizationUrl({ 22 + target: { type: "account", identifier: identifier as ActorIdentifier }, 23 + scope: OAUTH_SCOPE, 24 + }); 25 + window.location.assign(url.toString()); 26 + } 27 + 28 + /** Finalize the OAuth callback (on the /oauth/callback route). */ 29 + export async function finishLogin(): Promise<Session> { 30 + configureAtradioOAuth(); 31 + const params = new URLSearchParams(window.location.search); 32 + const { session } = await finalizeAuthorization(params); 33 + return session; 34 + } 35 + 36 + /** Restore a previously-stored session (IndexedDB), or null. */ 37 + export async function restoreSession(): Promise<Session | null> { 38 + try { 39 + configureAtradioOAuth(); 40 + const dids = listStoredSessions(); 41 + if (dids.length === 0) return null; 42 + return await getSession(dids[0], { allowStale: true }); 43 + } catch (err) { 44 + consola.warn("[atproto] failed to restore session", err); 45 + return null; 46 + } 47 + } 48 + 49 + /** Build an authenticated XRPC client for a session. */ 50 + export function makeClient(session: Session): Client { 51 + return new Client({ handler: new OAuthUserAgent(session) }); 52 + } 53 + 54 + /** Revoke + clear the session server-side and locally. */ 55 + export async function endSession(session: Session): Promise<void> { 56 + try { 57 + await new OAuthUserAgent(session).signOut(); 58 + } catch (err) { 59 + consola.warn("[atproto] sign-out error", err); 60 + } 61 + }
+2
apps/web/src/providers.tsx
··· 1 1 import { useState, type ReactNode } from "react"; 2 2 import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 3 3 import { Provider as JotaiProvider } from "jotai"; 4 + import { AuthInitializer } from "@/components/AuthInitializer"; 4 5 5 6 // HeroUI v3 requires no Provider wrapper — theming is CSS-driven via the 6 7 // `.dark` / `data-theme="dark"` attributes on <html>. ··· 20 21 return ( 21 22 <JotaiProvider> 22 23 <QueryClientProvider client={queryClient}> 24 + <AuthInitializer /> 23 25 <div className="min-h-screen overflow-x-hidden">{children}</div> 24 26 </QueryClientProvider> 25 27 </JotaiProvider>
+21 -1
apps/web/src/router.tsx
··· 6 6 import { Layout } from "@/components/Layout"; 7 7 import { SearchPage } from "@/routes/SearchPage"; 8 8 import { ProfilePage } from "@/routes/ProfilePage"; 9 + import { OAuthCallback } from "@/routes/OAuthCallback"; 9 10 10 11 const rootRoute = createRootRoute({ 11 12 component: Layout, ··· 17 18 component: SearchPage, 18 19 }); 19 20 21 + /** The logged-in user's own profile. */ 20 22 const profileRoute = createRoute({ 21 23 getParentRoute: () => rootRoute, 22 24 path: "/profile", 23 25 component: ProfilePage, 24 26 }); 25 27 26 - const routeTree = rootRoute.addChildren([indexRoute, profileRoute]); 28 + /** A public profile by DID or handle. */ 29 + const actorProfileRoute = createRoute({ 30 + getParentRoute: () => rootRoute, 31 + path: "/profile/$actor", 32 + component: ProfilePage, 33 + }); 34 + 35 + const oauthCallbackRoute = createRoute({ 36 + getParentRoute: () => rootRoute, 37 + path: "/oauth/callback", 38 + component: OAuthCallback, 39 + }); 40 + 41 + const routeTree = rootRoute.addChildren([ 42 + indexRoute, 43 + profileRoute, 44 + actorProfileRoute, 45 + oauthCallbackRoute, 46 + ]); 27 47 28 48 export const router = createRouter({ 29 49 routeTree,
+54
apps/web/src/routes/OAuthCallback.tsx
··· 1 + import { useEffect, useState } from "react"; 2 + import { useSetAtom } from "jotai"; 3 + import { useNavigate } from "@tanstack/react-router"; 4 + import { Spinner } from "@heroui/react"; 5 + import { consola } from "consola"; 6 + import { sessionAtom, authProfileAtom, authLoadingAtom } from "@/atoms/auth"; 7 + import { finishLogin } from "@/lib/atproto/session"; 8 + import { getProfile } from "@/lib/atproto/profile"; 9 + 10 + export function OAuthCallback() { 11 + const setSession = useSetAtom(sessionAtom); 12 + const setProfile = useSetAtom(authProfileAtom); 13 + const setLoading = useSetAtom(authLoadingAtom); 14 + const navigate = useNavigate(); 15 + const [error, setError] = useState<string | null>(null); 16 + 17 + useEffect(() => { 18 + let cancelled = false; 19 + (async () => { 20 + try { 21 + const session = await finishLogin(); 22 + if (cancelled) return; 23 + setSession(session); 24 + setLoading(false); 25 + try { 26 + const profile = await getProfile(session.info.sub); 27 + if (!cancelled) setProfile(profile); 28 + } catch { 29 + /* best-effort */ 30 + } 31 + navigate({ to: "/" }); 32 + } catch (err) { 33 + consola.error("[auth] callback failed", err); 34 + if (!cancelled) setError("Login failed. Please try again."); 35 + } 36 + })(); 37 + return () => { 38 + cancelled = true; 39 + }; 40 + }, [setSession, setProfile, setLoading, navigate]); 41 + 42 + return ( 43 + <div className="flex min-h-[60vh] flex-col items-center justify-center gap-3"> 44 + {error ? ( 45 + <p className="text-sm text-danger">{error}</p> 46 + ) : ( 47 + <> 48 + <Spinner color="accent" size="lg" /> 49 + <p className="text-sm text-foreground/50">Signing you in…</p> 50 + </> 51 + )} 52 + </div> 53 + ); 54 + }
+210 -57
apps/web/src/routes/ProfilePage.tsx
··· 1 1 import { useMemo, useState } from "react"; 2 - import { useAtom, useAtomValue, useSetAtom } from "jotai"; 3 - import { Button } from "@heroui/react"; 2 + import { useAtomValue, useSetAtom } from "jotai"; 3 + import { useParams } from "@tanstack/react-router"; 4 + import { useQuery } from "@tanstack/react-query"; 5 + import { Button, Spinner } from "@heroui/react"; 4 6 import { 5 7 IconHeart, 6 8 IconBroadcast, 7 9 IconPlus, 8 10 IconUserCircle, 11 + IconLogin2, 12 + IconLogout, 9 13 } from "@tabler/icons-react"; 10 14 import { favoritesAtom } from "@/atoms/favorites"; 11 15 import { ··· 13 17 removeCustomStationAtom, 14 18 } from "@/atoms/customStations"; 15 19 import { addStationOpenAtom } from "@/atoms/ui"; 20 + import { useAuth } from "@/hooks/useAuth"; 21 + import { getProfile, type ActorProfile } from "@/lib/atproto/profile"; 22 + import { readPublicUser } from "@/lib/atproto/publicReader"; 16 23 import { StationGrid } from "@/components/StationGrid"; 17 24 import { EmptyState } from "@/components/EmptyState"; 18 25 import type { Station } from "@/lib/types"; 19 - 20 - type TabKey = "favorites" | "custom"; 21 26 22 27 export function ProfilePage() { 28 + const params = useParams({ strict: false }) as { actor?: string }; 29 + return params.actor ? ( 30 + <PublicProfile actor={params.actor} /> 31 + ) : ( 32 + <SelfProfile /> 33 + ); 34 + } 35 + 36 + /* ---------------- self ---------------- */ 37 + 38 + function SelfProfile() { 39 + const { isLoggedIn, profile, loading, logout, openLogin } = useAuth(); 23 40 const favorites = useAtomValue(favoritesAtom); 24 - const [customStations] = useAtom(customStationsAtom); 41 + const stations = useAtomValue(customStationsAtom); 25 42 const removeCustom = useSetAtom(removeCustomStationAtom); 26 43 const openAddStation = useSetAtom(addStationOpenAtom); 27 - const [tab, setTab] = useState<TabKey>("favorites"); 44 + 45 + if (loading) { 46 + return <CenteredSpinner />; 47 + } 48 + 49 + if (!isLoggedIn) { 50 + return ( 51 + <EmptyState 52 + icon={<IconUserCircle size={44} stroke={1.5} />} 53 + title="Sign in to see your dial" 54 + description="Log in with your Atmosphere account to save favorites and add your own stations." 55 + action={ 56 + <Button 57 + variant="primary" 58 + className="gap-1.5 rounded-full" 59 + onPress={() => openLogin(true)} 60 + > 61 + <IconLogin2 size={16} /> 62 + Sign in 63 + </Button> 64 + } 65 + /> 66 + ); 67 + } 28 68 29 - const handleRemoveCustom = (station: Station) => removeCustom(station.id); 69 + return ( 70 + <ProfileView 71 + profile={profile} 72 + favorites={favorites} 73 + stations={stations} 74 + editable 75 + onRemove={(s) => removeCustom(s.id)} 76 + onAddStation={() => openAddStation(true)} 77 + onLogout={logout} 78 + /> 79 + ); 80 + } 81 + 82 + /* ---------------- public (any actor) ---------------- */ 30 83 31 - const stats = useMemo( 32 - () => [ 33 - { label: "Favorites", value: favorites.length }, 34 - { label: "Your stations", value: customStations.length }, 35 - ], 36 - [favorites.length, customStations.length], 84 + function PublicProfile({ actor }: { actor: string }) { 85 + const profileQuery = useQuery({ 86 + queryKey: ["profile", actor], 87 + queryFn: () => getProfile(actor), 88 + }); 89 + const dataQuery = useQuery({ 90 + queryKey: ["public-user", actor], 91 + queryFn: () => readPublicUser(actor), 92 + }); 93 + 94 + if (profileQuery.isLoading || dataQuery.isLoading) { 95 + return <CenteredSpinner />; 96 + } 97 + if (profileQuery.isError || !profileQuery.data) { 98 + return ( 99 + <EmptyState 100 + icon={<IconUserCircle size={44} stroke={1.5} />} 101 + title="Profile not found" 102 + description={`Couldn't find “${actor}”.`} 103 + /> 104 + ); 105 + } 106 + 107 + return ( 108 + <ProfileView 109 + profile={profileQuery.data} 110 + favorites={(dataQuery.data?.favorites ?? []).map((f) => f.station)} 111 + stations={(dataQuery.data?.stations ?? []).map((s) => s.station)} 112 + /> 113 + ); 114 + } 115 + 116 + /* ---------------- shared view ---------------- */ 117 + 118 + interface ProfileViewProps { 119 + profile: ActorProfile | null; 120 + favorites: Station[]; 121 + stations: Station[]; 122 + editable?: boolean; 123 + onRemove?: (station: Station) => void; 124 + onAddStation?: () => void; 125 + onLogout?: () => void; 126 + } 127 + 128 + function ProfileView({ 129 + profile, 130 + favorites, 131 + stations, 132 + editable = false, 133 + onRemove, 134 + onAddStation, 135 + onLogout, 136 + }: ProfileViewProps) { 137 + const [tab, setTab] = useState<"favorites" | "custom">("favorites"); 138 + 139 + const tabs = useMemo( 140 + () => 141 + [ 142 + { 143 + key: "favorites" as const, 144 + label: "Favorites", 145 + count: favorites.length, 146 + icon: IconHeart, 147 + }, 148 + { 149 + key: "custom" as const, 150 + label: "Stations", 151 + count: stations.length, 152 + icon: IconBroadcast, 153 + }, 154 + ], 155 + [favorites.length, stations.length], 37 156 ); 38 157 39 - const tabs: { key: TabKey; label: string; count: number; icon: typeof IconHeart }[] = 40 - [ 41 - { 42 - key: "favorites", 43 - label: "Favorites", 44 - count: favorites.length, 45 - icon: IconHeart, 46 - }, 47 - { 48 - key: "custom", 49 - label: "Your stations", 50 - count: customStations.length, 51 - icon: IconBroadcast, 52 - }, 53 - ]; 158 + const displayName = profile?.displayName || profile?.handle || "Listener"; 54 159 55 160 return ( 56 161 <div className="flex flex-col gap-8"> 57 - {/* Profile header */} 162 + {/* Header */} 58 163 <section className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> 59 164 <div className="flex items-center gap-4"> 60 - <span className="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-synth-purple to-synth-cyan shadow-neon"> 61 - <IconUserCircle size={40} className="text-white" stroke={1.5} /> 165 + <span className="flex h-16 w-16 items-center justify-center overflow-hidden rounded-2xl border border-white/10 bg-synth-panel"> 166 + {profile?.avatar ? ( 167 + <img 168 + src={profile.avatar} 169 + alt={displayName} 170 + className="h-full w-full object-cover" 171 + /> 172 + ) : ( 173 + <IconUserCircle size={40} className="text-synth-cyan" stroke={1.5} /> 174 + )} 62 175 </span> 63 - <div> 64 - <h1 className="font-display text-2xl font-bold"> 65 - <span className="synth-gradient-text">Your dial</span> 176 + <div className="min-w-0"> 177 + <h1 className="truncate font-display text-2xl font-bold"> 178 + {displayName} 66 179 </h1> 67 - <div className="mt-1 flex gap-4 text-sm text-foreground/60"> 68 - {stats.map((s) => ( 69 - <span key={s.label}> 70 - <span className="font-semibold text-synth-cyan"> 71 - {s.value} 72 - </span>{" "} 73 - {s.label} 74 - </span> 75 - ))} 180 + {profile?.handle && ( 181 + <p className="truncate text-sm text-synth-cyan/90"> 182 + @{profile.handle} 183 + </p> 184 + )} 185 + <div className="mt-1 flex gap-4 text-xs text-foreground/50"> 186 + <span> 187 + <span className="font-semibold text-synth-cyan"> 188 + {favorites.length} 189 + </span>{" "} 190 + favorites 191 + </span> 192 + <span> 193 + <span className="font-semibold text-synth-cyan"> 194 + {stations.length} 195 + </span>{" "} 196 + stations 197 + </span> 76 198 </div> 77 199 </div> 78 200 </div> 201 + 202 + {editable && ( 203 + <div className="flex items-center gap-2"> 204 + <Button 205 + variant="tertiary" 206 + className="gap-1.5 rounded-full !bg-white/5" 207 + onPress={onLogout} 208 + > 209 + <IconLogout size={16} /> 210 + Log out 211 + </Button> 212 + </div> 213 + )} 79 214 </section> 80 215 81 216 {/* Tabs */} ··· 113 248 <EmptyState 114 249 icon={<IconHeart size={40} stroke={1.5} />} 115 250 title="No favorites yet" 116 - description="Tap the heart on any station to save it here for quick access." 251 + description={ 252 + editable 253 + ? "Tap the heart on any station to save it here." 254 + : "This user hasn't favorited any stations." 255 + } 117 256 /> 118 257 ) : ( 119 258 <StationGrid stations={favorites} /> 120 259 ))} 121 260 122 261 {tab === "custom" && 123 - (customStations.length === 0 ? ( 262 + (stations.length === 0 ? ( 124 263 <EmptyState 125 264 icon={<IconBroadcast size={40} stroke={1.5} />} 126 - title="You haven't added any stations" 127 - description="Know a stream that isn't listed? Add it with its name and stream URL." 265 + title="No stations yet" 266 + description={ 267 + editable 268 + ? "Add a stream that isn't listed with its name and URL." 269 + : "This user hasn't added any stations." 270 + } 128 271 action={ 129 - <Button 130 - variant="primary" 131 - className="gap-1.5 rounded-full" 132 - onPress={() => openAddStation(true)} 133 - > 134 - <IconPlus size={16} /> 135 - Add your first station 136 - </Button> 272 + editable ? ( 273 + <Button 274 + variant="primary" 275 + className="gap-1.5 rounded-full" 276 + onPress={onAddStation} 277 + > 278 + <IconPlus size={16} /> 279 + Add your first station 280 + </Button> 281 + ) : undefined 137 282 } 138 283 /> 139 284 ) : ( 140 285 <StationGrid 141 - stations={customStations} 142 - onRemove={handleRemoveCustom} 286 + stations={stations} 287 + onRemove={editable ? onRemove : undefined} 143 288 /> 144 289 ))} 145 290 </div> ··· 147 292 </div> 148 293 ); 149 294 } 295 + 296 + function CenteredSpinner() { 297 + return ( 298 + <div className="flex min-h-[50vh] items-center justify-center"> 299 + <Spinner color="accent" size="lg" /> 300 + </div> 301 + ); 302 + }
+4 -2
apps/web/src/test/app.smoke.test.tsx
··· 58 58 59 59 it("renders the profile page with empty states", async () => { 60 60 renderApp("/profile"); 61 + // Logged out, the profile page shows a sign-in call to action. 61 62 await waitFor(() => 62 - expect(screen.getByText("Your dial")).toBeInTheDocument(), 63 + expect( 64 + screen.getByText(/sign in to see your dial/i), 65 + ).toBeInTheDocument(), 63 66 ); 64 - expect(screen.getByText(/no favorites yet/i)).toBeInTheDocument(); 65 67 }); 66 68 });
+11 -4
apps/web/src/test/modal.test.tsx
··· 44 44 const user = userEvent.setup(); 45 45 renderApp(); 46 46 47 - // Wait for the app to mount, then confirm the modal isn't shown yet. 47 + // Add-station is gated behind login, so a logged-out click opens the 48 + // login modal. This still verifies a nested HeroUI modal renders correctly. 48 49 const addButton = await screen.findByRole("button", { 49 50 name: /add station/i, 50 51 }); 51 - expect(screen.queryByText("Add your own station")).not.toBeInTheDocument(); 52 + expect( 53 + screen.queryByText(/Log in with Atmosphere account/i), 54 + ).not.toBeInTheDocument(); 52 55 53 56 await user.click(addButton); 54 57 55 58 await waitFor(() => 56 - expect(screen.getByText("Add your own station")).toBeInTheDocument(), 59 + expect( 60 + screen.getByText(/Log in with Atmosphere account/i), 61 + ).toBeInTheDocument(), 57 62 ); 58 63 // A field from the modal body is present -> the dialog actually rendered. 59 - expect(screen.getByText("Stream URL")).toBeInTheDocument(); 64 + expect( 65 + screen.getByPlaceholderText("atmosphere.handle"), 66 + ).toBeInTheDocument(); 60 67 }); 61 68 });
+486 -33
bun.lock
··· 9 9 "typescript": "^5.7.2", 10 10 }, 11 11 }, 12 + "apps/api": { 13 + "name": "@atradio/api", 14 + "version": "0.1.0", 15 + "dependencies": { 16 + "@atcute/atproto": "^3.1.10", 17 + "@atcute/client": "^4.2.1", 18 + "@atcute/identity-resolver": "^1.2.2", 19 + "@atcute/lexicons": "^1.3.1", 20 + "@atradio/lexicons": "workspace:*", 21 + "consola": "^3.3.3", 22 + "cors": "^2.8.5", 23 + "dotenv": "^16.4.7", 24 + "drizzle-orm": "^0.38.3", 25 + "express": "^4.21.2", 26 + "postgres": "^3.4.5", 27 + "ws": "^8.18.0", 28 + "zod": "^3.24.1", 29 + }, 30 + "devDependencies": { 31 + "@types/cors": "^2.8.17", 32 + "@types/express": "^4.17.21", 33 + "@types/node": "^22.10.5", 34 + "@types/ws": "^8.5.13", 35 + "drizzle-kit": "^0.30.1", 36 + "tsx": "^4.19.2", 37 + "typescript": "^5.7.2", 38 + }, 39 + }, 12 40 "apps/web": { 13 41 "name": "@atradio/web", 14 42 "version": "0.1.0", 15 43 "dependencies": { 44 + "@atcute/atproto": "^3.1.10", 45 + "@atcute/client": "^4.2.1", 46 + "@atcute/identity-resolver": "^1.2.2", 47 + "@atcute/lexicons": "^1.3.1", 48 + "@atcute/oauth-browser-client": "^3.0.0", 49 + "@atcute/tid": "^1.1.2", 50 + "@atradio/lexicons": "workspace:*", 16 51 "@fontsource/jetbrains-mono": "^5.1.1", 17 52 "@fontsource/lexend": "^5.1.1", 18 53 "@fontsource/outfit": "^5.1.1", ··· 22 57 "@tabler/icons-react": "^3.26.0", 23 58 "@tanstack/react-query": "^5.62.11", 24 59 "@tanstack/react-router": "^1.95.1", 60 + "consola": "^3.3.3", 25 61 "framer-motion": "^11.15.0", 26 62 "hls.js": "^1.5.18", 27 63 "jotai": "^2.11.0", ··· 52 88 "vitest": "^3.0.0", 53 89 }, 54 90 }, 91 + "packages/lexicons": { 92 + "name": "@atradio/lexicons", 93 + "version": "0.0.0", 94 + "dependencies": { 95 + "zod": "^3.24.1", 96 + }, 97 + "devDependencies": { 98 + "@pkl-community/pkl": "^0.28.2", 99 + "chalk": "^5.4.1", 100 + "consola": "^3.3.3", 101 + "tsx": "^4.19.2", 102 + "typescript": "^5.7.2", 103 + "zx": "^8.3.0", 104 + }, 105 + }, 55 106 }, 56 107 "packages": { 57 108 "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], ··· 66 117 67 118 "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], 68 119 120 + "@atcute/atproto": ["@atcute/atproto@3.1.12", "", { "dependencies": { "@atcute/lexicons": "^1.3.1" } }, "sha512-SaHY0vV5+VfS2ViOcbYtxPmmh82vbxoK5ccHTGn5+ciHNY2arEVcBUFbIQKtsQP4PPZ+lNAVooH+Wh62flvCzg=="], 121 + 122 + "@atcute/client": ["@atcute/client@4.2.2", "", { "dependencies": { "@atcute/identity": "^1.1.5", "@atcute/lexicons": "^1.3.1" } }, "sha512-z16BaGgdO6WIkDCxqeI+zhnh2KmW9jsjd312PJ6YYsoDBpPPqL+WkBmxQ7eO9C6CMFxXsZpYcM81RzZEETA4PQ=="], 123 + 124 + "@atcute/identity": ["@atcute/identity@1.1.5", "", { "dependencies": { "@atcute/lexicons": "^1.3.1", "@badrap/valita": "^0.4.6" } }, "sha512-5i9nl1UVnBDPCumUwrLNl4BZpGvQ/XABEXbjhiw3PQwRUfpQA8FqByDGxXy2gWpFDrNvQ9yVuOoNsjzxJgjjVA=="], 125 + 126 + "@atcute/identity-resolver": ["@atcute/identity-resolver@1.2.3", "", { "dependencies": { "@atcute/lexicons": "^1.3.1", "@atcute/util-fetch": "^1.0.5", "@badrap/valita": "^0.4.6" }, "peerDependencies": { "@atcute/identity": "^1.0.0" } }, "sha512-q891zLUMtgoD60y5Pv2YmTg4wIf4ipFuIhZlIuQTTB7qM9sGxmBbe2quzTRCTl3pnsLEmTz7l+3Dszgoutsp5w=="], 127 + 128 + "@atcute/lexicons": ["@atcute/lexicons@1.3.1", "", { "dependencies": { "@atcute/uint8array": "^1.1.1", "@atcute/util-text": "^1.3.1", "@standard-schema/spec": "^1.1.0", "esm-env": "^1.2.2" } }, "sha512-2JVxDmHt+QwsUoPyVYWIN7ZLRLfLx4GeJxKFjA9ofStuby9hCMv7Q4GAPIXuJD8wPv8vrnhr1yRNQhiJX+bthw=="], 129 + 130 + "@atcute/multibase": ["@atcute/multibase@1.2.4", "", { "dependencies": { "@atcute/uint8array": "^1.1.3" } }, "sha512-WeX12hvFZEim6C+cyv7Eqd93w6DzubNWQGmTFBghjsEuXvMe4HbBCYvsti0OUnbA5qLBPlsTyssQUJeLlHCzIw=="], 131 + 132 + "@atcute/oauth-browser-client": ["@atcute/oauth-browser-client@3.0.1", "", { "dependencies": { "@atcute/client": "^4.2.2", "@atcute/identity-resolver": "^1.2.3", "@atcute/lexicons": "^1.3.1", "@atcute/multibase": "^1.2.0", "@atcute/oauth-crypto": "^0.1.0", "@atcute/oauth-types": "^0.1.1", "nanoid": "^5.1.11" } }, "sha512-1eyqsPUyGzjsclyrScr73L/0I8iyv2kJlBD9Atyq8Fwhkv6DLCIMf3wTCbjOE0uZOmQMk3tRGzVC33fEIoktxw=="], 133 + 134 + "@atcute/oauth-crypto": ["@atcute/oauth-crypto@0.1.0", "", { "dependencies": { "@atcute/multibase": "^1.1.7", "@atcute/uint8array": "^1.1.0", "@badrap/valita": "^0.4.6", "nanoid": "^5.1.6" } }, "sha512-qZYDCNLF/4B6AndYT1rsQelN8621AC5u/sL5PHvlr/qqAbmmUwCBGjEgRSyZtHE1AqD60VNiSMlOgAuEQTSl3w=="], 135 + 136 + "@atcute/oauth-keyset": ["@atcute/oauth-keyset@0.1.2", "", { "dependencies": { "@atcute/oauth-crypto": "^1.0.1" } }, "sha512-2CUazWBWRjSEoYZDaLS4zEPgCOcr9LgnMhPr1pzr9I+DytRnoXmrdbRs4xBVg4iUdXtdzU48DsMaGi6bHXaFcg=="], 137 + 138 + "@atcute/oauth-types": ["@atcute/oauth-types@0.1.1", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/lexicons": "^1.2.7", "@atcute/oauth-keyset": "^0.1.0", "@badrap/valita": "^0.4.6" } }, "sha512-u+3KMjse3Uc/9hDyilu1QVN7IpcnjVXgRzhddzBB8Uh6wePHNVBDdi9wQvFTVVA3zmxtMJVptXRyLLg6Ou9bqg=="], 139 + 140 + "@atcute/tid": ["@atcute/tid@1.1.3", "", { "dependencies": { "@atcute/time-ms": "^1.3.3" } }, "sha512-Ox9KRz8i1QC2ILNKP4JY7q2s1Vnf0xIRiS2CL5RVhQ+k248462mOUdZJkPfPnlprALv6sATDIJMbWioGkXoQRA=="], 141 + 142 + "@atcute/time-ms": ["@atcute/time-ms@1.3.3", "", {}, "sha512-FeK2Xsv4ymP+PoTWtmMFuJmpxDBQsj+jNMnd/YpIm3etcIPj1YFb7DeTAG1c/Xvp8AM38MLdqUCc35/mANnBtw=="], 143 + 144 + "@atcute/uint8array": ["@atcute/uint8array@1.1.4", "", {}, "sha512-rSW5AFVCIN4ooH7vEZB+J60+uWjn5fRBQAQL58qHLiDm8+xDPmHfEU5GfYOJuuD+7UBj8KKiQugIzohFn8/xPw=="], 145 + 146 + "@atcute/util-fetch": ["@atcute/util-fetch@1.0.5", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig=="], 147 + 148 + "@atcute/util-text": ["@atcute/util-text@1.3.3", "", { "dependencies": { "unicode-segmenter": "^0.14.5" } }, "sha512-WhedTmg/msFhrdwXw9RjnNcDl8Vmisxl4+Vzyf5k3+8Gj5TKQg72dLSDtBNmNLd61RbHjgfQRBgE0ez6q/jciw=="], 149 + 150 + "@atradio/api": ["@atradio/api@workspace:apps/api"], 151 + 152 + "@atradio/lexicons": ["@atradio/lexicons@workspace:packages/lexicons"], 153 + 69 154 "@atradio/web": ["@atradio/web@workspace:apps/web"], 70 155 71 156 "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], ··· 254 339 255 340 "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], 256 341 342 + "@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="], 343 + 257 344 "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], 258 345 259 346 "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], ··· 264 351 265 352 "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], 266 353 354 + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], 355 + 267 356 "@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], 268 357 269 - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 358 + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], 270 359 271 - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], 360 + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], 272 361 273 - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], 362 + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], 274 363 275 - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], 364 + "@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], 276 365 277 - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], 366 + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], 278 367 279 - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], 368 + "@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], 280 369 281 - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], 370 + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], 282 371 283 - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], 372 + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], 284 373 285 - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], 374 + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], 286 375 287 - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], 376 + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], 288 377 289 - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], 378 + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], 290 379 291 - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], 380 + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], 292 381 293 - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], 382 + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], 294 383 295 - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], 384 + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], 296 385 297 - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], 386 + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], 298 387 299 - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], 388 + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], 300 389 301 - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], 390 + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], 302 391 303 - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], 392 + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], 304 393 305 - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], 394 + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], 306 395 307 - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], 396 + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], 308 397 309 - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], 398 + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], 310 399 311 - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], 400 + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], 312 401 313 - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], 402 + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], 403 + 404 + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], 405 + 406 + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], 314 407 315 - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], 408 + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], 316 409 317 - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], 410 + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], 318 411 319 - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], 412 + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], 320 413 321 414 "@fontsource/jetbrains-mono": ["@fontsource/jetbrains-mono@5.2.8", "", {}, "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ=="], 322 415 ··· 400 493 401 494 "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], 402 495 496 + "@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="], 497 + 498 + "@pkl-community/pkl": ["@pkl-community/pkl@0.28.2", "", { "optionalDependencies": { "@pkl-community/pkl-darwin-arm64": "0.28.2", "@pkl-community/pkl-darwin-x64": "0.28.2", "@pkl-community/pkl-linux-arm64": "0.28.2", "@pkl-community/pkl-linux-x64": "0.28.2" }, "bin": { "pkl": "lib/main.js" } }, "sha512-seSZrwGvDEd1BeT9+dRRnqvyNit8vpFL2I+YBEJ+t3pBYpnpHaVHwQNPlMLvDQ0KZUCQkiWGZnbCMF9WtaxHNA=="], 499 + 500 + "@pkl-community/pkl-darwin-arm64": ["@pkl-community/pkl-darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1CP5VayKnZeL1BUTaQUltiBu5wbn/pNizP3aSK1zbWMHDGK/S/rIs1X0GWEyOHWG7P/h9EBsNzHmJqg4kCn5jw=="], 501 + 502 + "@pkl-community/pkl-darwin-x64": ["@pkl-community/pkl-darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-gGV3S9Hnyclga3/8wvspXWxKKEPMJrWzgeafIPpFPDa49DDlPCrf4zNkN0T29yylWd4aLwMUGRE15vswkjhdjA=="], 503 + 504 + "@pkl-community/pkl-linux-arm64": ["@pkl-community/pkl-linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-IGJDm0maESl7Q+x2XukC7ia39m7Z++/C92cknrvTtyNdEROufGDobUv96ekq4e1gdcEgDvvDqCXLEQNC871Trg=="], 505 + 506 + "@pkl-community/pkl-linux-x64": ["@pkl-community/pkl-linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-9MndyTuiRhBRdrDqgcNadi6mT8mCNcLChwh28D5t5EDko8maUDTKHwHeuIzdBI5P0ZLNYPnT4McPufkeBu67vQ=="], 507 + 403 508 "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="], 404 509 405 510 "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], ··· 502 607 503 608 "@spectrum-icons/workflow": ["@spectrum-icons/workflow@4.3.1", "", { "dependencies": { "@adobe/react-spectrum-workflow": "2.3.5", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "@adobe/react-spectrum": "^3.47.0", "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-kDF+/EbFVyLGytotqqdYt4uSij4j/PQmDQO5km/C6DyzKjyuic3FnSBFinR+mA6oFv1OjMcLvrrDBqK3wbqRlA=="], 504 609 610 + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], 611 + 505 612 "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="], 506 613 507 614 "@tabler/icons": ["@tabler/icons@3.44.0", "", {}, "sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA=="], ··· 594 701 595 702 "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], 596 703 704 + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], 705 + 597 706 "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], 598 707 708 + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], 709 + 710 + "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], 711 + 599 712 "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], 600 713 601 714 "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], 602 715 716 + "@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="], 717 + 718 + "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.9", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg=="], 719 + 720 + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], 721 + 722 + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], 723 + 603 724 "@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="], 604 725 726 + "@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="], 727 + 728 + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], 729 + 605 730 "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], 606 731 607 732 "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], 608 733 609 734 "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], 610 735 736 + "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], 737 + 738 + "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], 739 + 611 740 "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], 741 + 742 + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], 612 743 613 744 "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], 614 745 ··· 626 757 627 758 "@vitest/utils": ["@vitest/utils@3.2.7", "", { "dependencies": { "@vitest/pretty-format": "3.2.7", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw=="], 628 759 760 + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], 761 + 629 762 "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], 630 763 631 764 "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], ··· 641 774 "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], 642 775 643 776 "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], 777 + 778 + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], 644 779 645 780 "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], 646 781 ··· 666 801 667 802 "baseline-browser-mapping": ["baseline-browser-mapping@2.10.43", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ=="], 668 803 804 + "body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="], 805 + 669 806 "brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="], 670 807 671 808 "browserslist": ["browserslist@4.28.6", "", { "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", "electron-to-chromium": "^1.5.389", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw=="], 672 809 673 810 "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], 811 + 812 + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], 674 813 675 814 "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], 676 815 ··· 684 823 685 824 "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], 686 825 826 + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], 827 + 687 828 "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], 688 829 689 830 "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], ··· 704 845 705 846 "common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="], 706 847 848 + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], 849 + 850 + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], 851 + 852 + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], 853 + 707 854 "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], 708 855 856 + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], 857 + 709 858 "cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], 710 859 860 + "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], 861 + 711 862 "core-js-compat": ["core-js-compat@3.49.0", "", { "dependencies": { "browserslist": "^4.28.1" } }, "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA=="], 712 863 864 + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], 865 + 713 866 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 714 867 715 868 "crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="], ··· 728 881 729 882 "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], 730 883 731 - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 884 + "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], 732 885 733 886 "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], 734 887 ··· 742 895 743 896 "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], 744 897 898 + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], 899 + 745 900 "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], 901 + 902 + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], 746 903 747 904 "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 748 905 ··· 750 907 751 908 "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], 752 909 910 + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], 911 + 912 + "drizzle-kit": ["drizzle-kit@0.30.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g=="], 913 + 914 + "drizzle-orm": ["drizzle-orm@0.38.4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/react": ">=18", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "react": ">=18", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/react", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "react", "sql.js", "sqlite3"] }, "sha512-s7/5BpLKO+WJRHspvpqTydxFob8i1vo2rEx4pY6TGY7QSMuUfWUuzaY0DIpXCkgHOo37BaFC+SJQb99dDUXT3Q=="], 915 + 753 916 "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], 917 + 918 + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], 754 919 755 920 "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], 756 921 757 922 "electron-to-chromium": ["electron-to-chromium@1.5.389", "", {}, "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg=="], 758 923 924 + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], 925 + 759 926 "enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="], 760 927 761 928 "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], 929 + 930 + "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], 762 931 763 932 "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], 764 933 ··· 776 945 777 946 "es-to-primitive": ["es-to-primitive@1.3.4", "", { "dependencies": { "es-abstract-get": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "is-callable": "^1.2.7", "is-date-object": "^1.1.0", "is-symbol": "^1.1.1" } }, "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw=="], 778 947 779 - "esbuild": ["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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 948 + "esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], 949 + 950 + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], 780 951 781 952 "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 782 953 954 + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], 955 + 956 + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], 957 + 783 958 "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], 784 959 785 960 "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], 786 961 787 962 "eta": ["eta@4.6.0", "", {}, "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA=="], 788 963 964 + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], 965 + 789 966 "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], 967 + 968 + "express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], 790 969 791 970 "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 792 971 ··· 798 977 799 978 "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], 800 979 980 + "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], 981 + 801 982 "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], 802 983 803 984 "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], 804 985 805 986 "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], 806 987 988 + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], 989 + 807 990 "framer-motion": ["framer-motion@11.18.2", "", { "dependencies": { "motion-dom": "^11.18.1", "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w=="], 808 991 992 + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], 993 + 809 994 "fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], 810 995 811 996 "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], ··· 815 1000 "function.prototype.name": ["function.prototype.name@1.2.0", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2", "hasown": "^2.0.4", "is-callable": "^1.2.7", "is-document.all": "^1.0.0" } }, "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew=="], 816 1001 817 1002 "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], 1003 + 1004 + "gel": ["gel@2.2.0", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ=="], 818 1005 819 1006 "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], 820 1007 ··· 828 1015 829 1016 "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], 830 1017 1018 + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], 1019 + 831 1020 "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], 832 1021 833 1022 "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], ··· 854 1043 855 1044 "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="], 856 1045 1046 + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], 1047 + 857 1048 "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], 858 1049 859 1050 "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], 860 1051 861 - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], 1052 + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], 862 1053 863 1054 "idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="], 864 1055 865 1056 "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], 866 1057 1058 + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], 1059 + 867 1060 "input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="], 868 1061 869 1062 "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], 870 1063 871 1064 "intl-messageformat": ["intl-messageformat@10.7.18", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.4", "tslib": "^2.8.0" } }, "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g=="], 872 1065 1066 + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], 1067 + 873 1068 "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], 874 1069 875 1070 "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], ··· 932 1127 933 1128 "isbot": ["isbot@5.2.0", "", {}, "sha512-gbZiGCb4B5xaoxg9mS7koAyRdvJnArk10VLSHOgz6rtBG93/pi1xOFaVvXMKZ7JXgyZ8zAbNRK5uIBdIUTFSqw=="], 934 1129 935 - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 1130 + "isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], 936 1131 937 1132 "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], 938 1133 ··· 998 1193 999 1194 "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], 1000 1195 1196 + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], 1197 + 1198 + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], 1199 + 1200 + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], 1201 + 1202 + "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], 1203 + 1001 1204 "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 1002 1205 1003 1206 "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], ··· 1012 1215 1013 1216 "motion-utils": ["motion-utils@11.18.1", "", {}, "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA=="], 1014 1217 1015 - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1218 + "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 1016 1219 1017 - "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], 1220 + "nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="], 1221 + 1222 + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], 1018 1223 1019 1224 "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], 1020 1225 ··· 1028 1233 1029 1234 "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], 1030 1235 1236 + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], 1237 + 1031 1238 "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], 1032 1239 1033 1240 "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], 1034 1241 1035 1242 "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], 1243 + 1244 + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], 1036 1245 1037 1246 "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], 1038 1247 ··· 1040 1249 1041 1250 "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], 1042 1251 1252 + "path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], 1253 + 1043 1254 "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], 1044 1255 1045 1256 "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], ··· 1051 1262 "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], 1052 1263 1053 1264 "postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="], 1265 + 1266 + "postgres": ["postgres@3.4.9", "", {}, "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="], 1054 1267 1055 1268 "pretty-bytes": ["pretty-bytes@6.1.1", "", {}, "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="], 1056 1269 ··· 1058 1271 1059 1272 "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], 1060 1273 1274 + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], 1275 + 1061 1276 "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 1277 + 1278 + "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], 1279 + 1280 + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], 1281 + 1282 + "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], 1062 1283 1063 1284 "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], 1064 1285 ··· 1100 1321 1101 1322 "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], 1102 1323 1324 + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], 1325 + 1103 1326 "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], 1104 1327 1105 1328 "rrweb-cssom": ["rrweb-cssom@0.7.1", "", {}, "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg=="], 1106 1329 1107 1330 "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], 1331 + 1332 + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], 1108 1333 1109 1334 "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], 1110 1335 ··· 1118 1343 1119 1344 "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], 1120 1345 1346 + "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], 1347 + 1121 1348 "serialize-javascript": ["serialize-javascript@7.0.7", "", {}, "sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g=="], 1122 1349 1123 1350 "seroval": ["seroval@1.5.5", "", {}, "sha512-bSjOuPcwPKLSJNhr9+bZxA20nQxVle5J5MNsYRVE6cIg7KpRLXGupymePavu0jrxlPiPsr4xGZSB8yUY2sH2sw=="], 1124 1351 1125 1352 "seroval-plugins": ["seroval-plugins@1.5.5", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-+BDhqYM6CEn3x09v44dpa9p6974FuUB2dxk+Ctn04k0cO1Zt6QODTXfmEZK0eBaTe/fJBvP4NMGuNJ+R8T+QMg=="], 1353 + 1354 + "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], 1126 1355 1127 1356 "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], 1128 1357 ··· 1130 1359 1131 1360 "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], 1132 1361 1362 + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], 1363 + 1133 1364 "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], 1134 1365 1135 1366 "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], 1136 1367 1137 1368 "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], 1369 + 1370 + "shell-quote": ["shell-quote@1.10.0", "", {}, "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA=="], 1138 1371 1139 1372 "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], 1140 1373 ··· 1159 1392 "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], 1160 1393 1161 1394 "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], 1395 + 1396 + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], 1162 1397 1163 1398 "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], 1164 1399 ··· 1213 1448 "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], 1214 1449 1215 1450 "tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="], 1451 + 1452 + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], 1216 1453 1217 1454 "tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="], 1218 1455 ··· 1220 1457 1221 1458 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 1222 1459 1460 + "tsx": ["tsx@4.23.1", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ=="], 1461 + 1223 1462 "turbo": ["turbo@2.10.5", "", { "optionalDependencies": { "@turbo/darwin-64": "2.10.5", "@turbo/darwin-arm64": "2.10.5", "@turbo/linux-64": "2.10.5", "@turbo/linux-arm64": "2.10.5", "@turbo/windows-64": "2.10.5", "@turbo/windows-arm64": "2.10.5" }, "bin": { "turbo": "bin/turbo" } }, "sha512-07Y/C7OUp23l4P92PJoYtFNbHjLhftrZH5Ce7dbczS4kX2Re+wtbXvZLoxn/pUtzgsQaRCBaRuZPJp4zmAn0WQ=="], 1224 1463 1225 1464 "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], 1226 1465 1227 1466 "type-fest": ["type-fest@0.16.0", "", {}, "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg=="], 1467 + 1468 + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], 1228 1469 1229 1470 "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], 1230 1471 ··· 1248 1489 1249 1490 "unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.2.0", "", {}, "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ=="], 1250 1491 1492 + "unicode-segmenter": ["unicode-segmenter@0.14.5", "", {}, "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g=="], 1493 + 1251 1494 "unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="], 1252 1495 1253 1496 "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], 1497 + 1498 + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], 1254 1499 1255 1500 "upath": ["upath@1.2.0", "", {}, "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="], 1256 1501 ··· 1258 1503 1259 1504 "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], 1260 1505 1506 + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], 1507 + 1508 + "valibot": ["valibot@1.4.2", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg=="], 1509 + 1510 + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], 1511 + 1261 1512 "vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="], 1262 1513 1263 1514 "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], ··· 1276 1527 1277 1528 "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], 1278 1529 1279 - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 1530 + "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], 1280 1531 1281 1532 "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], 1282 1533 ··· 1330 1581 1331 1582 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 1332 1583 1584 + "zx": ["zx@8.8.5", "", { "bin": { "zx": "build/cli.js" } }, "sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA=="], 1585 + 1333 1586 "@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], 1334 1587 1588 + "@atcute/oauth-keyset/@atcute/oauth-crypto": ["@atcute/oauth-crypto@1.0.1", "", { "dependencies": { "@atcute/multibase": "^1.2.4", "@atcute/uint8array": "^1.1.3", "nanoid": "^5.1.16", "valibot": "^1.4.1" } }, "sha512-ghC8ceFx0r6pi5X3dNoET1K6PNg8afeAOZLD4PKLhrpVYdRtRrgY4uqAe3ojtSIjoS6GXmVm063CoJ2xtg/MoQ=="], 1589 + 1590 + "@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1591 + 1335 1592 "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], 1336 1593 1337 1594 "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], ··· 1339 1596 "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], 1340 1597 1341 1598 "@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], 1599 + 1600 + "@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1342 1601 1343 1602 "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], 1344 1603 1604 + "@babel/traverse/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1605 + 1606 + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], 1607 + 1345 1608 "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], 1346 1609 1347 1610 "@tailwindcss/node/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], ··· 1362 1625 1363 1626 "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], 1364 1627 1628 + "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 1629 + 1365 1630 "cssstyle/rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], 1366 1631 1632 + "esbuild-register/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1633 + 1367 1634 "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], 1368 1635 1636 + "gel/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1637 + 1638 + "http-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1639 + 1640 + "https-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1641 + 1369 1642 "path-scurry/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], 1370 1643 1644 + "postcss/nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], 1645 + 1371 1646 "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], 1372 1647 1648 + "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1649 + 1373 1650 "source-map/whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="], 1374 1651 1375 1652 "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], 1376 1653 1377 1654 "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], 1378 1655 1656 + "tsx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], 1657 + 1658 + "vite/esbuild": ["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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 1659 + 1660 + "vite-node/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1661 + 1662 + "vite-plugin-pwa/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1663 + 1664 + "vitest/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1665 + 1666 + "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], 1667 + 1379 1668 "workbox-build/pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], 1380 1669 1670 + "@babel/core/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1671 + 1672 + "@babel/helper-define-polyfill-provider/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1673 + 1674 + "@babel/traverse/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1675 + 1676 + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], 1677 + 1678 + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], 1679 + 1680 + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], 1681 + 1682 + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], 1683 + 1684 + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], 1685 + 1686 + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], 1687 + 1688 + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], 1689 + 1690 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], 1691 + 1692 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], 1693 + 1694 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], 1695 + 1696 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], 1697 + 1698 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], 1699 + 1700 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], 1701 + 1702 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], 1703 + 1704 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], 1705 + 1706 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], 1707 + 1708 + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], 1709 + 1710 + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], 1711 + 1712 + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], 1713 + 1714 + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], 1715 + 1716 + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], 1717 + 1718 + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], 1719 + 1720 + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 1721 + 1722 + "esbuild-register/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1723 + 1381 1724 "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="], 1725 + 1726 + "gel/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1727 + 1728 + "http-proxy-agent/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1729 + 1730 + "https-proxy-agent/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1382 1731 1383 1732 "source-map/whatwg-url/tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="], 1384 1733 1385 1734 "source-map/whatwg-url/webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], 1735 + 1736 + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], 1737 + 1738 + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], 1739 + 1740 + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], 1741 + 1742 + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], 1743 + 1744 + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], 1745 + 1746 + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], 1747 + 1748 + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], 1749 + 1750 + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], 1751 + 1752 + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], 1753 + 1754 + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], 1755 + 1756 + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], 1757 + 1758 + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], 1759 + 1760 + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], 1761 + 1762 + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], 1763 + 1764 + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], 1765 + 1766 + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], 1767 + 1768 + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], 1769 + 1770 + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], 1771 + 1772 + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], 1773 + 1774 + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], 1775 + 1776 + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], 1777 + 1778 + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], 1779 + 1780 + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], 1781 + 1782 + "vite-node/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1783 + 1784 + "vite-plugin-pwa/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1785 + 1786 + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 1787 + 1788 + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], 1789 + 1790 + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], 1791 + 1792 + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], 1793 + 1794 + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], 1795 + 1796 + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], 1797 + 1798 + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], 1799 + 1800 + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], 1801 + 1802 + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], 1803 + 1804 + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], 1805 + 1806 + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], 1807 + 1808 + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], 1809 + 1810 + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], 1811 + 1812 + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], 1813 + 1814 + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], 1815 + 1816 + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], 1817 + 1818 + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], 1819 + 1820 + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], 1821 + 1822 + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], 1823 + 1824 + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], 1825 + 1826 + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], 1827 + 1828 + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], 1829 + 1830 + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], 1831 + 1832 + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], 1833 + 1834 + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], 1835 + 1836 + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], 1837 + 1838 + "vitest/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1386 1839 1387 1840 "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 1388 1841 }
+8
console
··· 1 + #!/usr/bin/env bash 2 + # atradio.fm command console — Clojure + rebel REPL that centralizes every 3 + # monorepo command. Requires the toolchain from mise (`mise install`). 4 + set -euo pipefail 5 + ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" 6 + export ATRADIO_ROOT="$ROOT" 7 + cd "$ROOT/tools/console" 8 + exec clojure -M:console
+4
mise.toml
··· 1 1 [tools] 2 2 node = "24.12.0" 3 3 bun = "1.3.14" 4 + # Toolchain for the tools/console command hub (Babashka + Clojure + rebel REPL) 5 + java = "temurin-21" 6 + clojure = "latest" 7 + babashka = "latest"
+97
packages/lexicons/lexicons/atradio/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.atradio.defs", 4 + "description": "Shared objects for atradio.fm lexicons.", 5 + "defs": { 6 + "stationInfo": { 7 + "type": "object", 8 + "description": "A self-contained snapshot of a radio station.", 9 + "required": [ 10 + "stationId", 11 + "name", 12 + "streamUrl", 13 + "source" 14 + ], 15 + "properties": { 16 + "stationId": { 17 + "type": "string", 18 + "description": "Source-prefixed stable id, e.g. rb:uuid, tunein:s123, custom:xyz.", 19 + "maxLength": 512 20 + }, 21 + "name": { 22 + "type": "string", 23 + "minLength": 1, 24 + "maxLength": 512 25 + }, 26 + "streamUrl": { 27 + "type": "string", 28 + "description": "Direct, playable stream URL.", 29 + "format": "uri" 30 + }, 31 + "source": { 32 + "type": "string", 33 + "description": "Provider: radio-browser | tunein | custom." 34 + }, 35 + "description": { 36 + "type": "string", 37 + "maxLength": 2000 38 + }, 39 + "genre": { 40 + "type": "string", 41 + "maxLength": 256 42 + }, 43 + "homepage": { 44 + "type": "string", 45 + "format": "uri" 46 + }, 47 + "logo": { 48 + "type": "string", 49 + "description": "Logo/artwork URL.", 50 + "format": "uri" 51 + }, 52 + "country": { 53 + "type": "string" 54 + }, 55 + "language": { 56 + "type": "string" 57 + }, 58 + "bitrate": { 59 + "type": "integer", 60 + "minimum": 0 61 + }, 62 + "codec": { 63 + "type": "string" 64 + }, 65 + "tags": { 66 + "type": "array", 67 + "items": { 68 + "type": "string" 69 + } 70 + } 71 + } 72 + }, 73 + "stationView": { 74 + "type": "object", 75 + "description": "A station snapshot paired with its record uri (query output item).", 76 + "required": [ 77 + "uri", 78 + "station", 79 + "createdAt" 80 + ], 81 + "properties": { 82 + "uri": { 83 + "type": "string", 84 + "format": "at-uri" 85 + }, 86 + "station": { 87 + "type": "ref", 88 + "ref": "fm.atradio.defs#stationInfo" 89 + }, 90 + "createdAt": { 91 + "type": "string", 92 + "format": "datetime" 93 + } 94 + } 95 + } 96 + } 97 + }
+32
packages/lexicons/lexicons/atradio/favorite.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.atradio.favorite", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A favorited radio station.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": [ 12 + "station", 13 + "createdAt" 14 + ], 15 + "properties": { 16 + "station": { 17 + "type": "ref", 18 + "ref": "fm.atradio.defs#stationInfo" 19 + }, 20 + "subject": { 21 + "type": "ref", 22 + "ref": "com.atproto.repo.strongRef" 23 + }, 24 + "createdAt": { 25 + "type": "string", 26 + "format": "datetime" 27 + } 28 + } 29 + } 30 + } 31 + } 32 + }
+52
packages/lexicons/lexicons/atradio/getFavorites.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.atradio.getFavorites", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List a user's favorited radio stations.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "actor" 12 + ], 13 + "properties": { 14 + "actor": { 15 + "type": "string", 16 + "description": "DID or handle of the user.", 17 + "format": "at-identifier" 18 + }, 19 + "limit": { 20 + "type": "integer", 21 + "maximum": 100, 22 + "minimum": 1 23 + }, 24 + "cursor": { 25 + "type": "string" 26 + } 27 + } 28 + }, 29 + "output": { 30 + "encoding": "application/json", 31 + "schema": { 32 + "type": "object", 33 + "required": [ 34 + "items" 35 + ], 36 + "properties": { 37 + "cursor": { 38 + "type": "string" 39 + }, 40 + "items": { 41 + "type": "array", 42 + "items": { 43 + "type": "ref", 44 + "ref": "fm.atradio.defs#stationView" 45 + } 46 + } 47 + } 48 + } 49 + } 50 + } 51 + } 52 + }
+52
packages/lexicons/lexicons/atradio/getStations.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.atradio.getStations", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List a user's own radio stations.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "actor" 12 + ], 13 + "properties": { 14 + "actor": { 15 + "type": "string", 16 + "description": "DID or handle of the user.", 17 + "format": "at-identifier" 18 + }, 19 + "limit": { 20 + "type": "integer", 21 + "maximum": 100, 22 + "minimum": 1 23 + }, 24 + "cursor": { 25 + "type": "string" 26 + } 27 + } 28 + }, 29 + "output": { 30 + "encoding": "application/json", 31 + "schema": { 32 + "type": "object", 33 + "required": [ 34 + "items" 35 + ], 36 + "properties": { 37 + "cursor": { 38 + "type": "string" 39 + }, 40 + "items": { 41 + "type": "array", 42 + "items": { 43 + "type": "ref", 44 + "ref": "fm.atradio.defs#stationView" 45 + } 46 + } 47 + } 48 + } 49 + } 50 + } 51 + } 52 + }
+58
packages/lexicons/lexicons/atradio/station.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.atradio.station", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A user-defined radio station.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": [ 12 + "name", 13 + "streamUrl", 14 + "createdAt" 15 + ], 16 + "properties": { 17 + "name": { 18 + "type": "string", 19 + "minLength": 1, 20 + "maxLength": 512 21 + }, 22 + "streamUrl": { 23 + "type": "string", 24 + "description": "Direct, playable stream URL.", 25 + "format": "uri" 26 + }, 27 + "description": { 28 + "type": "string", 29 + "maxLength": 2000 30 + }, 31 + "genre": { 32 + "type": "string", 33 + "maxLength": 256 34 + }, 35 + "homepage": { 36 + "type": "string", 37 + "format": "uri" 38 + }, 39 + "logo": { 40 + "type": "string", 41 + "description": "Logo/artwork URL.", 42 + "format": "uri" 43 + }, 44 + "tags": { 45 + "type": "array", 46 + "items": { 47 + "type": "string" 48 + } 49 + }, 50 + "createdAt": { 51 + "type": "string", 52 + "format": "datetime" 53 + } 54 + } 55 + } 56 + } 57 + } 58 + }
+26
packages/lexicons/package.json
··· 1 + { 2 + "name": "@atradio/lexicons", 3 + "version": "0.0.0", 4 + "private": true, 5 + "type": "module", 6 + "exports": { 7 + ".": "./src/index.ts", 8 + "./lexicons/*.json": "./lexicons/*.json" 9 + }, 10 + "scripts": { 11 + "pkl:eval": "pkl eval -f json", 12 + "pkl:gen": "tsx ./scripts/pkl.ts", 13 + "typecheck": "tsc --noEmit" 14 + }, 15 + "dependencies": { 16 + "zod": "^3.24.1" 17 + }, 18 + "devDependencies": { 19 + "@pkl-community/pkl": "^0.28.2", 20 + "chalk": "^5.4.1", 21 + "consola": "^3.3.3", 22 + "tsx": "^4.19.2", 23 + "typescript": "^5.7.2", 24 + "zx": "^8.3.0" 25 + } 26 + }
+88
packages/lexicons/pkl/defs/atradio/defs.pkl
··· 1 + amends "../../schema/lexicon.pkl" 2 + 3 + lexicon = 1 4 + id = "fm.atradio.defs" 5 + description = "Shared objects for atradio.fm lexicons." 6 + defs = new Mapping<String, ObjectType> { 7 + ["stationInfo"] { 8 + type = "object" 9 + description = "A self-contained snapshot of a radio station." 10 + required = List("stationId", "name", "streamUrl", "source") 11 + properties { 12 + ["stationId"] = new StringType { 13 + type = "string" 14 + description = "Source-prefixed stable id, e.g. rb:uuid, tunein:s123, custom:xyz." 15 + maxLength = 512 16 + } 17 + ["name"] = new StringType { 18 + type = "string" 19 + minLength = 1 20 + maxLength = 512 21 + } 22 + ["streamUrl"] = new StringType { 23 + type = "string" 24 + description = "Direct, playable stream URL." 25 + format = "uri" 26 + } 27 + ["source"] = new StringType { 28 + type = "string" 29 + description = "Provider: radio-browser | tunein | custom." 30 + } 31 + ["description"] = new StringType { 32 + type = "string" 33 + maxLength = 2000 34 + } 35 + ["genre"] = new StringType { 36 + type = "string" 37 + maxLength = 256 38 + } 39 + ["homepage"] = new StringType { 40 + type = "string" 41 + format = "uri" 42 + } 43 + ["logo"] = new StringType { 44 + type = "string" 45 + description = "Logo/artwork URL." 46 + format = "uri" 47 + } 48 + ["country"] = new StringType { 49 + type = "string" 50 + } 51 + ["language"] = new StringType { 52 + type = "string" 53 + } 54 + ["bitrate"] = new IntegerType { 55 + type = "integer" 56 + minimum = 0 57 + } 58 + ["codec"] = new StringType { 59 + type = "string" 60 + } 61 + ["tags"] = new Array { 62 + type = "array" 63 + items = new StringType { 64 + type = "string" 65 + } 66 + } 67 + } 68 + } 69 + ["stationView"] { 70 + type = "object" 71 + description = "A station snapshot paired with its record uri (query output item)." 72 + required = List("uri", "station", "createdAt") 73 + properties { 74 + ["uri"] = new StringType { 75 + type = "string" 76 + format = "at-uri" 77 + } 78 + ["station"] = new Ref { 79 + type = "ref" 80 + ref = "fm.atradio.defs#stationInfo" 81 + } 82 + ["createdAt"] = new StringType { 83 + type = "string" 84 + format = "datetime" 85 + } 86 + } 87 + } 88 + }
+29
packages/lexicons/pkl/defs/atradio/favorite.pkl
··· 1 + amends "../../schema/lexicon.pkl" 2 + 3 + lexicon = 1 4 + id = "fm.atradio.favorite" 5 + defs = new Mapping<String, Record> { 6 + ["main"] { 7 + type = "record" 8 + key = "tid" 9 + description = "A favorited radio station." 10 + `record` { 11 + type = "object" 12 + required = List("station", "createdAt") 13 + properties { 14 + ["station"] = new Ref { 15 + type = "ref" 16 + ref = "fm.atradio.defs#stationInfo" 17 + } 18 + ["subject"] = new Ref { 19 + type = "ref" 20 + ref = "com.atproto.repo.strongRef" 21 + } 22 + ["createdAt"] = new StringType { 23 + type = "string" 24 + format = "datetime" 25 + } 26 + } 27 + } 28 + } 29 + }
+47
packages/lexicons/pkl/defs/atradio/getFavorites.pkl
··· 1 + amends "../../schema/lexicon.pkl" 2 + 3 + lexicon = 1 4 + id = "fm.atradio.getFavorites" 5 + defs = new Mapping<String, Query> { 6 + ["main"] { 7 + type = "query" 8 + description = "List a user's favorited radio stations." 9 + parameters = new Params { 10 + required = List("actor") 11 + properties { 12 + ["actor"] = new StringType { 13 + type = "string" 14 + description = "DID or handle of the user." 15 + format = "at-identifier" 16 + } 17 + ["limit"] = new IntegerType { 18 + type = "integer" 19 + minimum = 1 20 + maximum = 100 21 + } 22 + ["cursor"] = new StringType { 23 + type = "string" 24 + } 25 + } 26 + } 27 + output { 28 + encoding = "application/json" 29 + schema = new ObjectType { 30 + type = "object" 31 + required = List("items") 32 + properties { 33 + ["cursor"] = new StringType { 34 + type = "string" 35 + } 36 + ["items"] = new Array { 37 + type = "array" 38 + items = new Ref { 39 + type = "ref" 40 + ref = "fm.atradio.defs#stationView" 41 + } 42 + } 43 + } 44 + } 45 + } 46 + } 47 + }
+47
packages/lexicons/pkl/defs/atradio/getStations.pkl
··· 1 + amends "../../schema/lexicon.pkl" 2 + 3 + lexicon = 1 4 + id = "fm.atradio.getStations" 5 + defs = new Mapping<String, Query> { 6 + ["main"] { 7 + type = "query" 8 + description = "List a user's own radio stations." 9 + parameters = new Params { 10 + required = List("actor") 11 + properties { 12 + ["actor"] = new StringType { 13 + type = "string" 14 + description = "DID or handle of the user." 15 + format = "at-identifier" 16 + } 17 + ["limit"] = new IntegerType { 18 + type = "integer" 19 + minimum = 1 20 + maximum = 100 21 + } 22 + ["cursor"] = new StringType { 23 + type = "string" 24 + } 25 + } 26 + } 27 + output { 28 + encoding = "application/json" 29 + schema = new ObjectType { 30 + type = "object" 31 + required = List("items") 32 + properties { 33 + ["cursor"] = new StringType { 34 + type = "string" 35 + } 36 + ["items"] = new Array { 37 + type = "array" 38 + items = new Ref { 39 + type = "ref" 40 + ref = "fm.atradio.defs#stationView" 41 + } 42 + } 43 + } 44 + } 45 + } 46 + } 47 + }
+54
packages/lexicons/pkl/defs/atradio/station.pkl
··· 1 + amends "../../schema/lexicon.pkl" 2 + 3 + lexicon = 1 4 + id = "fm.atradio.station" 5 + defs = new Mapping<String, Record> { 6 + ["main"] { 7 + type = "record" 8 + key = "tid" 9 + description = "A user-defined radio station." 10 + `record` { 11 + type = "object" 12 + required = List("name", "streamUrl", "createdAt") 13 + properties { 14 + ["name"] = new StringType { 15 + type = "string" 16 + minLength = 1 17 + maxLength = 512 18 + } 19 + ["streamUrl"] = new StringType { 20 + type = "string" 21 + description = "Direct, playable stream URL." 22 + format = "uri" 23 + } 24 + ["description"] = new StringType { 25 + type = "string" 26 + maxLength = 2000 27 + } 28 + ["genre"] = new StringType { 29 + type = "string" 30 + maxLength = 256 31 + } 32 + ["homepage"] = new StringType { 33 + type = "string" 34 + format = "uri" 35 + } 36 + ["logo"] = new StringType { 37 + type = "string" 38 + description = "Logo/artwork URL." 39 + format = "uri" 40 + } 41 + ["tags"] = new Array { 42 + type = "array" 43 + items = new StringType { 44 + type = "string" 45 + } 46 + } 47 + ["createdAt"] = new StringType { 48 + type = "string" 49 + format = "datetime" 50 + } 51 + } 52 + } 53 + } 54 + }
+96
packages/lexicons/pkl/schema/lexicon.pkl
··· 1 + open class BaseType { 2 + type: String 3 + description: String? 4 + } 5 + 6 + class StringType extends BaseType { 7 + type: "string" 8 + maxGraphemes: Int? 9 + minLength: Int? 10 + maxLength: Int? 11 + format: "uri" | "datetime" | "cid" | "at-uri" | "at-identifier" | "did" | Null = null 12 + } 13 + 14 + class IntegerType extends BaseType { 15 + type: "integer" 16 + maximum: Int? 17 + minimum: Int? 18 + default: Int? 19 + } 20 + 21 + class BooleanType extends BaseType { 22 + type: "boolean" 23 + } 24 + 25 + class Blob extends BaseType { 26 + type: "blob" 27 + accept: List<String> 28 + maxSize: Int 29 + } 30 + 31 + class Array extends BaseType { 32 + type: "array" 33 + items: StringType | IntegerType | Blob | Ref | Union 34 + } 35 + 36 + class Params { 37 + type: "params" 38 + required: List<String>? 39 + properties: Mapping<String, StringType | IntegerType | BooleanType | Blob | Ref | Array> 40 + } 41 + 42 + class ObjectType extends BaseType { 43 + type: "object" 44 + required: List<String>? 45 + properties: Mapping<String, StringType | IntegerType | BooleanType | Blob | Ref | Union | Array> 46 + } 47 + 48 + class Union extends BaseType { 49 + type: "union" 50 + refs: List<String> 51 + } 52 + 53 + class Ref extends BaseType { 54 + type: "ref" 55 + ref: String 56 + } 57 + 58 + class Input { 59 + encoding: "application/json" 60 + schema: ObjectType 61 + } 62 + 63 + class Output { 64 + encoding: "application/json" | "application/octet-stream" 65 + schema: ObjectType | Ref | Array | Null = null 66 + } 67 + 68 + class Query extends BaseType { 69 + type: "query" 70 + parameters: Params 71 + output: Output 72 + } 73 + 74 + class Procedure extends BaseType { 75 + type: "procedure" 76 + parameters: Params? 77 + input: Input? 78 + output: Output? 79 + } 80 + 81 + class Record extends BaseType { 82 + type: "record" 83 + key: String 84 + `record`: ObjectType 85 + } 86 + 87 + lexicon: Int = 1 88 + id: String 89 + description: String? 90 + defs: Mapping<String, Procedure> 91 + | Mapping<String, Record> 92 + | Mapping<String, ObjectType> 93 + | Mapping<String, Query> 94 + | Mapping<String, Array> 95 + | Mapping<String, ObjectType | Array> 96 + | Null = null
+33
packages/lexicons/scripts/pkl.ts
··· 1 + import chalk from "chalk"; 2 + import { readdirSync, statSync, mkdirSync } from "fs"; 3 + import { dirname, join } from "path"; 4 + import { $ } from "zx"; 5 + import { consola } from "consola"; 6 + 7 + function getPklFilesRecursive(dir: string): string[] { 8 + const files: string[] = []; 9 + for (const entry of readdirSync(dir)) { 10 + const fullPath = join(dir, entry); 11 + if (statSync(fullPath).isDirectory()) { 12 + files.push(...getPklFilesRecursive(fullPath)); 13 + continue; 14 + } 15 + if (entry.endsWith(".pkl")) files.push(fullPath); 16 + } 17 + return files; 18 + } 19 + 20 + const files = getPklFilesRecursive(join("pkl", "defs")); 21 + 22 + await Promise.all( 23 + files.map(async (fullPath) => { 24 + const outPath = fullPath 25 + .replace(/\.pkl$/, ".json") 26 + .replace(/pkl[\\/]defs/g, "lexicons"); 27 + mkdirSync(dirname(outPath), { recursive: true }); 28 + consola.info(`pkl eval ${chalk.cyan(fullPath)} -> ${chalk.green(outPath)}`); 29 + await $`pkl eval -f json ${fullPath} > ${outPath}`; 30 + }), 31 + ); 32 + 33 + consola.success("Lexicons generated.");
+3
packages/lexicons/src/index.ts
··· 1 + export * from "./types"; 2 + export * from "./schema"; 3 + export * from "./mappers";
+120
packages/lexicons/src/mappers.ts
··· 1 + import type { 2 + FavoriteRecord, 3 + Station, 4 + StationDraft, 5 + StationInfo, 6 + StationRecord, 7 + StrongRef, 8 + } from "./types"; 9 + import { NSID } from "./types"; 10 + 11 + /** Drop `undefined` and empty-string values so records stay minimal. */ 12 + function clean<T extends Record<string, unknown>>(obj: T): T { 13 + return Object.fromEntries( 14 + Object.entries(obj).filter(([, v]) => v !== undefined && v !== ""), 15 + ) as T; 16 + } 17 + 18 + /** Parse an at-uri into its parts. */ 19 + export function parseAtUri(uri: string): { 20 + did: string; 21 + collection: string; 22 + rkey: string; 23 + } { 24 + const m = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/); 25 + if (!m) throw new Error(`Invalid at-uri: ${uri}`); 26 + return { did: m[1], collection: m[2], rkey: m[3] }; 27 + } 28 + 29 + export const rkeyFromUri = (uri: string) => parseAtUri(uri).rkey; 30 + 31 + // ---- Station <-> stationInfo (embedded snapshot) ---- 32 + 33 + export function stationToInfo(s: Station): StationInfo { 34 + return clean({ 35 + stationId: s.id, 36 + name: s.name, 37 + streamUrl: s.streamUrl, 38 + source: s.source, 39 + description: s.description, 40 + genre: s.genre, 41 + homepage: s.homepage, 42 + logo: s.favicon, 43 + country: s.country, 44 + language: s.language, 45 + bitrate: s.bitrate, 46 + codec: s.codec, 47 + tags: s.tags, 48 + }) as StationInfo; 49 + } 50 + 51 + export function infoToStation(i: StationInfo): Station { 52 + return clean({ 53 + id: i.stationId, 54 + name: i.name, 55 + streamUrl: i.streamUrl, 56 + source: i.source, 57 + description: i.description, 58 + genre: i.genre, 59 + homepage: i.homepage, 60 + favicon: i.logo, 61 + country: i.country, 62 + language: i.language, 63 + bitrate: i.bitrate, 64 + codec: i.codec, 65 + tags: i.tags, 66 + }) as Station; 67 + } 68 + 69 + // ---- fm.atradio.favorite ---- 70 + 71 + export function buildFavoriteRecord( 72 + s: Station, 73 + opts: { subject?: StrongRef; createdAt?: string } = {}, 74 + ): FavoriteRecord { 75 + return clean({ 76 + $type: NSID.favorite, 77 + station: stationToInfo(s), 78 + subject: opts.subject, 79 + createdAt: opts.createdAt ?? new Date().toISOString(), 80 + }) as FavoriteRecord; 81 + } 82 + 83 + export const favoriteRecordToStation = (r: FavoriteRecord): Station => 84 + infoToStation(r.station); 85 + 86 + // ---- fm.atradio.station ---- 87 + 88 + export function buildStationRecord( 89 + draft: StationDraft, 90 + createdAt?: string, 91 + ): StationRecord { 92 + return clean({ 93 + $type: NSID.station, 94 + name: draft.name, 95 + streamUrl: draft.streamUrl, 96 + description: draft.description, 97 + genre: draft.genre, 98 + homepage: draft.homepage, 99 + logo: draft.logoUrl, 100 + createdAt: createdAt ?? new Date().toISOString(), 101 + }) as StationRecord; 102 + } 103 + 104 + /** Turn a stored station record + its rkey into an app `Station`. */ 105 + export function stationRecordToStation( 106 + r: StationRecord, 107 + rkey: string, 108 + ): Station { 109 + return clean({ 110 + id: `custom:${rkey}`, 111 + name: r.name, 112 + streamUrl: r.streamUrl, 113 + source: "custom", 114 + description: r.description, 115 + genre: r.genre, 116 + homepage: r.homepage, 117 + favicon: r.logo, 118 + tags: r.tags, 119 + }) as Station; 120 + }
+58
packages/lexicons/src/schema.ts
··· 1 + import { z } from "zod"; 2 + 3 + export const stationSourceSchema = z.enum([ 4 + "radio-browser", 5 + "tunein", 6 + "custom", 7 + ]); 8 + 9 + export const stationInfoSchema = z.object({ 10 + stationId: z.string(), 11 + name: z.string(), 12 + streamUrl: z.string(), 13 + source: stationSourceSchema, 14 + description: z.string().optional(), 15 + genre: z.string().optional(), 16 + homepage: z.string().optional(), 17 + logo: z.string().optional(), 18 + country: z.string().optional(), 19 + language: z.string().optional(), 20 + bitrate: z.number().optional(), 21 + codec: z.string().optional(), 22 + tags: z.array(z.string()).optional(), 23 + }); 24 + 25 + export const strongRefSchema = z.object({ 26 + uri: z.string(), 27 + cid: z.string(), 28 + }); 29 + 30 + export const favoriteRecordSchema = z.object({ 31 + $type: z.literal("fm.atradio.favorite").optional(), 32 + station: stationInfoSchema, 33 + subject: strongRefSchema.optional(), 34 + createdAt: z.string(), 35 + }); 36 + 37 + export const stationRecordSchema = z.object({ 38 + $type: z.literal("fm.atradio.station").optional(), 39 + name: z.string(), 40 + streamUrl: z.string(), 41 + description: z.string().optional(), 42 + genre: z.string().optional(), 43 + homepage: z.string().optional(), 44 + logo: z.string().optional(), 45 + tags: z.array(z.string()).optional(), 46 + createdAt: z.string(), 47 + }); 48 + 49 + export const stationViewSchema = z.object({ 50 + uri: z.string(), 51 + station: stationInfoSchema, 52 + createdAt: z.string(), 53 + }); 54 + 55 + export const stationListOutputSchema = z.object({ 56 + cursor: z.string().optional(), 57 + items: z.array(stationViewSchema), 58 + });
+94
packages/lexicons/src/types.ts
··· 1 + export const NSID = { 2 + station: "fm.atradio.station", 3 + favorite: "fm.atradio.favorite", 4 + getFavorites: "fm.atradio.getFavorites", 5 + getStations: "fm.atradio.getStations", 6 + } as const; 7 + 8 + export type StationSource = "radio-browser" | "tunein" | "custom"; 9 + 10 + export interface StrongRef { 11 + uri: string; 12 + cid: string; 13 + } 14 + 15 + /** `fm.atradio.defs#stationInfo` — a self-contained station snapshot. */ 16 + export interface StationInfo { 17 + stationId: string; 18 + name: string; 19 + streamUrl: string; 20 + source: StationSource; 21 + description?: string; 22 + genre?: string; 23 + homepage?: string; 24 + logo?: string; 25 + country?: string; 26 + language?: string; 27 + bitrate?: number; 28 + codec?: string; 29 + tags?: string[]; 30 + } 31 + 32 + /** `fm.atradio.favorite` record. */ 33 + export interface FavoriteRecord { 34 + $type?: typeof NSID.favorite; 35 + station: StationInfo; 36 + subject?: StrongRef; 37 + createdAt: string; 38 + } 39 + 40 + /** `fm.atradio.station` record. */ 41 + export interface StationRecord { 42 + $type?: typeof NSID.station; 43 + name: string; 44 + streamUrl: string; 45 + description?: string; 46 + genre?: string; 47 + homepage?: string; 48 + logo?: string; 49 + tags?: string[]; 50 + createdAt: string; 51 + } 52 + 53 + /** `fm.atradio.defs#stationView` — a query output item. */ 54 + export interface StationView { 55 + uri: string; 56 + station: StationInfo; 57 + createdAt: string; 58 + } 59 + 60 + /** Output of `fm.atradio.getFavorites` / `getStations`. */ 61 + export interface StationListOutput { 62 + cursor?: string; 63 + items: StationView[]; 64 + } 65 + 66 + /** 67 + * The app-level station shape (mirrors apps/web `Station`). Kept structurally 68 + * identical so the web app can pass its own `Station` to these mappers. 69 + */ 70 + export interface Station { 71 + id: string; 72 + name: string; 73 + description?: string; 74 + genre?: string; 75 + streamUrl: string; 76 + homepage?: string; 77 + favicon?: string; 78 + country?: string; 79 + language?: string; 80 + bitrate?: number; 81 + codec?: string; 82 + tags?: string[]; 83 + source: StationSource; 84 + } 85 + 86 + /** A user-entered station draft (mirrors apps/web `CustomStationInput`). */ 87 + export interface StationDraft { 88 + name: string; 89 + streamUrl: string; 90 + description?: string; 91 + genre?: string; 92 + homepage?: string; 93 + logoUrl?: string; 94 + }
+9
packages/lexicons/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.base.json", 3 + "compilerOptions": { 4 + "lib": ["ES2023"], 5 + "types": [], 6 + "rootDir": "src" 7 + }, 8 + "include": ["src"] 9 + }
+29
systemd/README.md
··· 1 + # systemd units 2 + 3 + Deployment units for the atradio.fm API. Two services split the combined 4 + `apps/api` entrypoint so they can be scaled/restarted independently: 5 + 6 + - **`atradio-api.service`** — the Express server (XRPC read API + TuneIn/ICY 7 + media proxies), `bun run start:server`. 8 + - **`atradio-jetstream.service`** — the Jetstream consumer that indexes 9 + `fm.atradio.*` records into Postgres, `bun run start:consumer`. 10 + 11 + Both read config from `apps/api/.env` (loaded via dotenv). 12 + 13 + ## Install 14 + 15 + Adjust `WorkingDirectory` / `User` / `HOME` to your host, then: 16 + 17 + ```bash 18 + sudo cp systemd/atradio-*.service /etc/systemd/system/ 19 + sudo systemctl daemon-reload 20 + sudo systemctl enable --now atradio-api atradio-jetstream 21 + ``` 22 + 23 + ## Operate 24 + 25 + ```bash 26 + systemctl status atradio-api atradio-jetstream 27 + journalctl -u atradio-jetstream -f # follow the firehose consumer 28 + sudo systemctl restart atradio-api 29 + ```
+22
systemd/atradio-api.service
··· 1 + [Unit] 2 + Description=atradio.fm API (Express XRPC + media proxies) 3 + After=network.target 4 + Wants=network-online.target 5 + 6 + [Service] 7 + Type=simple 8 + User=root 9 + WorkingDirectory=/root/github/atradio.fm/apps/api 10 + ExecStart=/bin/bash -ic 'exec bun run start:server' 11 + Restart=on-failure 12 + RestartSec=5 13 + StandardOutput=journal 14 + StandardError=journal 15 + SyslogIdentifier=atradio-api 16 + 17 + # Environment (reads apps/api/.env via dotenv) 18 + Environment=HOME=/root 19 + Environment=USER=root 20 + 21 + [Install] 22 + WantedBy=multi-user.target
+22
systemd/atradio-jetstream.service
··· 1 + [Unit] 2 + Description=atradio.fm Jetstream consumer (fm.atradio.* -> Postgres AppView) 3 + After=network.target 4 + Wants=network-online.target 5 + 6 + [Service] 7 + Type=simple 8 + User=root 9 + WorkingDirectory=/root/github/atradio.fm/apps/api 10 + ExecStart=/bin/bash -ic 'exec bun run start:consumer' 11 + Restart=on-failure 12 + RestartSec=5 13 + StandardOutput=journal 14 + StandardError=journal 15 + SyslogIdentifier=atradio-jetstream 16 + 17 + # Environment (reads apps/api/.env via dotenv) 18 + Environment=HOME=/root 19 + Environment=USER=root 20 + 21 + [Install] 22 + WantedBy=multi-user.target
+1
tools/console/.cpcache/2582488212.basis
··· 1 + {:paths ["src"], :deps {org.clojure/clojure {:mvn/version "1.12.4"}}, :aliases {:deps {:replace-paths [], :replace-deps {org.clojure/tools.deps.cli {:mvn/version "0.15.131"}}, :ns-default clojure.tools.deps.cli.api, :ns-aliases {help clojure.tools.deps.cli.help}}, :test {:extra-paths ["test"]}, :rebel {:extra-deps {com.bhauman/rebel-readline {:mvn/version "0.1.5"}}, :exec-fn rebel-readline.tool/repl, :exec-args {}, :main-opts ["-m" "rebel-readline.main"]}, :console {:extra-deps {com.bhauman/rebel-readline {:mvn/version "0.1.5"}}, :main-opts ["-m" "rebel-readline.main"]}}, :mvn/repos {"central" {:url "https://repo1.maven.org/maven2/"}, "clojars" {:url "https://repo.clojars.org/"}}, :libs {org.clojure/clojure {:mvn/version "1.12.4", :deps/manifest :mvn, :parents #{[]}, :paths ["/Users/tsirysandratraina/.m2/repository/org/clojure/clojure/1.12.4/clojure-1.12.4.jar"]}, org.clojure/spec.alpha {:mvn/version "0.5.238", :deps/manifest :mvn, :dependents [org.clojure/clojure], :parents #{[org.clojure/clojure]}, :paths ["/Users/tsirysandratraina/.m2/repository/org/clojure/spec.alpha/0.5.238/spec.alpha-0.5.238.jar"]}, org.clojure/core.specs.alpha {:mvn/version "0.4.74", :deps/manifest :mvn, :dependents [org.clojure/clojure], :parents #{[org.clojure/clojure]}, :paths ["/Users/tsirysandratraina/.m2/repository/org/clojure/core.specs.alpha/0.4.74/core.specs.alpha-0.4.74.jar"]}}, :classpath-roots ["src" "/Users/tsirysandratraina/.m2/repository/org/clojure/clojure/1.12.4/clojure-1.12.4.jar" "/Users/tsirysandratraina/.m2/repository/org/clojure/core.specs.alpha/0.4.74/core.specs.alpha-0.4.74.jar" "/Users/tsirysandratraina/.m2/repository/org/clojure/spec.alpha/0.5.238/spec.alpha-0.5.238.jar"], :classpath {"src" {:path-key :paths}, "/Users/tsirysandratraina/.m2/repository/org/clojure/clojure/1.12.4/clojure-1.12.4.jar" {:lib-name org.clojure/clojure}, "/Users/tsirysandratraina/.m2/repository/org/clojure/core.specs.alpha/0.4.74/core.specs.alpha-0.4.74.jar" {:lib-name org.clojure/core.specs.alpha}, "/Users/tsirysandratraina/.m2/repository/org/clojure/spec.alpha/0.5.238/spec.alpha-0.5.238.jar" {:lib-name org.clojure/spec.alpha}}, :basis-config {:project "deps.edn", :user "/Users/tsirysandratraina/.clojure/deps.edn", :extra nil, :args nil}}
+1
tools/console/.cpcache/2582488212.cp
··· 1 + src:/Users/tsirysandratraina/.m2/repository/org/clojure/clojure/1.12.4/clojure-1.12.4.jar:/Users/tsirysandratraina/.m2/repository/org/clojure/core.specs.alpha/0.4.74/core.specs.alpha-0.4.74.jar:/Users/tsirysandratraina/.m2/repository/org/clojure/spec.alpha/0.5.238/spec.alpha-0.5.238.jar
+50
tools/console/README.md
··· 1 + # tools/console 2 + 3 + A single place to run every command in the atradio.fm monorepo, two ways: 4 + 5 + - **Interactive REPL** — a Clojure + [rebel-readline](https://github.com/bhauman/rebel-readline) 6 + console where each command is a function. 7 + - **Babashka tasks** — the same commands as fast `bb` tasks for scripting/CI. 8 + 9 + ## Setup 10 + 11 + The toolchain (Java, Clojure, Babashka) is pinned in the root `mise.toml`: 12 + 13 + ```bash 14 + mise install # installs java, clojure, babashka (+ node, bun) 15 + ``` 16 + 17 + ## Interactive console (rebel REPL) 18 + 19 + From the repo root: 20 + 21 + ```bash 22 + ./console # launches the rebel REPL with all commands preloaded 23 + ``` 24 + 25 + Then call commands as functions: 26 + 27 + ```clojure 28 + (help) ; list everything 29 + (dev) ; run every app in dev 30 + (api-consumer) ; run only the Jetstream consumer 31 + (migrate) ; apply Drizzle migrations 32 + (gen-lexicons) ; regenerate lexicon JSON from Pkl 33 + (gen-icons) ; regenerate PWA icons 34 + ``` 35 + 36 + ## Babashka tasks 37 + 38 + From `tools/console/`: 39 + 40 + ```bash 41 + bb tasks # list tasks 42 + bb dev # turbo dev 43 + bb db:migrate # apply migrations 44 + bb lexicons:gen # pkl -> lexicon JSON 45 + bb repl # launch the rebel REPL 46 + ``` 47 + 48 + Both frontends shell out to the underlying `bun` / `turbo` / `drizzle-kit` / 49 + `pkl` commands from the repo root, so they stay in sync with the workspace 50 + scripts — nothing is duplicated.
+28
tools/console/bb.edn
··· 1 + ;; Babashka task hub for the atradio.fm monorepo. 2 + ;; Run from this directory: `bb tasks` to list, `bb dev`, `bb migrate`, … 3 + {:paths ["src"] 4 + :tasks 5 + {:init (def root "../..") 6 + 7 + install {:doc "Install all workspace deps" :task (shell {:dir root} "bun" "install")} 8 + dev {:doc "Run every app in dev (turbo)" :task (shell {:dir root} "bun" "run" "dev")} 9 + build {:doc "Build everything" :task (shell {:dir root} "bun" "run" "build")} 10 + test {:doc "Run all tests" :task (shell {:dir root} "bun" "run" "test")} 11 + typecheck {:doc "Typecheck all workspaces" :task (shell {:dir root} "bun" "run" "typecheck")} 12 + 13 + web:dev {:doc "Run the web app (Vite)" :task (shell {:dir (str root "/apps/web")} "bun" "run" "dev")} 14 + web:build {:doc "Build the web app" :task (shell {:dir (str root "/apps/web")} "bun" "run" "build")} 15 + web:preview {:doc "Preview the web build" :task (shell {:dir (str root "/apps/web")} "bun" "run" "preview")} 16 + icons {:doc "Regenerate PWA icons" :task (shell {:dir (str root "/apps/web")} "bun" "run" "generate-icons")} 17 + 18 + api:dev {:doc "Run API server + Jetstream" :task (shell {:dir (str root "/apps/api")} "bun" "run" "dev")} 19 + api:server {:doc "Run only the Express server" :task (shell {:dir (str root "/apps/api")} "bun" "run" "start:server")} 20 + api:consumer {:doc "Run only the Jetstream consumer" :task (shell {:dir (str root "/apps/api")} "bun" "run" "start:consumer")} 21 + db:generate {:doc "Generate a Drizzle migration" :task (shell {:dir (str root "/apps/api")} "bun" "run" "db:generate")} 22 + db:migrate {:doc "Apply Drizzle migrations" :task (shell {:dir (str root "/apps/api")} "bun" "run" "db:migrate")} 23 + db:push {:doc "Push the Drizzle schema" :task (shell {:dir (str root "/apps/api")} "bun" "run" "db:push")} 24 + 25 + lexicons:gen {:doc "Generate lexicon JSON from Pkl" :task (shell {:dir (str root "/packages/lexicons")} "bun" "run" "pkl:gen")} 26 + 27 + repl {:doc "Launch the interactive Clojure + rebel console" 28 + :task (clojure "-M:console")}}}
+6
tools/console/deps.edn
··· 1 + {:paths ["src"] 2 + :aliases 3 + {;; Interactive rebel-readline REPL with every command preloaded (via user.clj). 4 + :console 5 + {:extra-deps {com.bhauman/rebel-readline {:mvn/version "0.1.5"}} 6 + :main-opts ["-m" "rebel-readline.main"]}}}
+82
tools/console/src/atradio/console.clj
··· 1 + (ns atradio.console 2 + "Centralized command hub for the atradio.fm monorepo. 3 + 4 + Every function shells out to the underlying bun/turbo/drizzle/pkl command from 5 + the repo root, streaming output. Use `(help)` to list commands." 6 + (:require [clojure.string :as str])) 7 + 8 + (def ^:private root 9 + (or (System/getenv "ATRADIO_ROOT") 10 + (System/getProperty "user.dir"))) 11 + 12 + (defn- run* 13 + "Run a command in `dir` (relative to repo root), inheriting stdio." 14 + [dir args] 15 + (let [wd (if (str/blank? dir) root (str root "/" dir)) 16 + pb (doto (ProcessBuilder. ^java.util.List (vec (map str args))) 17 + (.directory (java.io.File. wd)) 18 + (.inheritIO)) 19 + code (.waitFor (.start pb))] 20 + (when-not (zero? code) 21 + (println (str "✖ exited " code))) 22 + code)) 23 + 24 + (defn- root! [& args] (run* "" args)) 25 + (defn- web! [& args] (run* "apps/web" args)) 26 + (defn- api! [& args] (run* "apps/api" args)) 27 + (defn- lex! [& args] (run* "packages/lexicons" args)) 28 + 29 + ;; ---- workspace-wide (turbo) ---- 30 + (defn install "Install all workspace deps" [] (root! "bun" "install")) 31 + (defn dev "Run every app in dev (turbo dev)" [] (root! "bun" "run" "dev")) 32 + (defn build "Build everything" [] (root! "bun" "run" "build")) 33 + (defn test* "Run all tests" [] (root! "bun" "run" "test")) 34 + (defn typecheck "Typecheck all workspaces" [] (root! "bun" "run" "typecheck")) 35 + 36 + ;; ---- web ---- 37 + (defn web-dev "Run the web app (Vite)" [] (web! "bun" "run" "dev")) 38 + (defn web-build "Build the web app" [] (web! "bun" "run" "build")) 39 + (defn web-preview "Preview the web build (PWA active)" [] (web! "bun" "run" "preview")) 40 + (defn gen-icons "Regenerate the PWA icon set" [] (web! "bun" "run" "generate-icons")) 41 + 42 + ;; ---- api ---- 43 + (defn api-dev "Run the API (server + Jetstream consumer)" [] (api! "bun" "run" "dev")) 44 + (defn api-server "Run only the Express server" [] (api! "bun" "run" "start:server")) 45 + (defn api-consumer "Run only the Jetstream consumer" [] (api! "bun" "run" "start:consumer")) 46 + (defn gen-migration "Generate a Drizzle migration from the schema" [] (api! "bun" "run" "db:generate")) 47 + (defn migrate "Apply Drizzle migrations" [] (api! "bun" "run" "db:migrate")) 48 + (defn db-push "Push the Drizzle schema (dev)" [] (api! "bun" "run" "db:push")) 49 + 50 + ;; ---- lexicons ---- 51 + (defn gen-lexicons "Generate lexicon JSON from Pkl" [] (lex! "bun" "run" "pkl:gen")) 52 + 53 + (def ^:private commands 54 + [["(install)" "Install all workspace deps"] 55 + ["(dev)" "Run every app in dev"] 56 + ["(build)" "Build everything"] 57 + ["(test*)" "Run all tests"] 58 + ["(typecheck)" "Typecheck all workspaces"] 59 + ["(web-dev)" "Run the web app (Vite)"] 60 + ["(web-build)" "Build the web app"] 61 + ["(web-preview)" "Preview the web build"] 62 + ["(gen-icons)" "Regenerate PWA icons"] 63 + ["(api-dev)" "Run API server + Jetstream"] 64 + ["(api-server)" "Run only the Express server"] 65 + ["(api-consumer)" "Run only the Jetstream consumer"] 66 + ["(gen-migration)" "Generate a Drizzle migration"] 67 + ["(migrate)" "Apply Drizzle migrations"] 68 + ["(db-push)" "Push the Drizzle schema"] 69 + ["(gen-lexicons)" "Generate lexicon JSON from Pkl"] 70 + ["(help)" "Show this list"]]) 71 + 72 + (defn help "List all console commands" [] 73 + (println "\natradio.fm console — commands:\n") 74 + (doseq [[cmd doc] commands] 75 + (println (format " %-16s %s" cmd doc))) 76 + (println) 77 + :ok) 78 + 79 + (defn banner [] 80 + (println "\n ▟ atradio.fm command console — Clojure + rebel REPL") 81 + (println (str " root: " root)) 82 + (help))
+5
tools/console/src/user.clj
··· 1 + (ns user 2 + "Auto-loaded at REPL startup: pulls every atradio console command into scope." 3 + (:require [atradio.console :refer :all])) 4 + 5 + (banner)