diff --git a/.env.example b/.env.example index 570c9e7..fe99724 100644 --- a/.env.example +++ b/.env.example @@ -2,10 +2,19 @@ PUBLIC_RELAY_URL=ws://localhost:3334 # required — NIP-29 relay hosting the g PUBLIC_MODE=simple # simple | full (single forum vs. many rooms) PUBLIC_GROUP_ID=mygrouprandomid # required in simple mode; the single forum's group id (ignored in full mode) PUBLIC_TITLE= # top-bar title; empty falls back to the group name -PUBLIC_JOINCODE=no # yes | no — show an invite-code field when a join is rejected +# Render on the server (Node deploy) instead of shipping a static SPA +PUBLIC_SSR=no +# With SSR on, warm the server cache from client-side navigations (yes/no) +PUBLIC_SSR_WARM=yes +# Snapshot cache windows in seconds: served as is / served stale while refreshing +PUBLIC_SSR_CACHE_FRESH=300 +PUBLIC_SSR_CACHE_STALE=21600 + PUBLIC_SEARCH=no # yes | no — enable the search box (requires a relay with NIP-50 support) PUBLIC_LABELS= # comma-separated discussion labels (e.g., bug,feature,question) +PUBLIC_NOSTRCONNECT_RELAYS= # comma-separated relays for nostrconnect:// (bunker) login; defaults to bucket.coracle.social,relay.primal.net PUBLIC_BLOSSOM_URL= # Blossom server URL for uploads (e.g., https://blossom.primal.net) # Theme color overrides — quote the value, a leading # is read as a comment otherwise PUBLIC_ACCENT_COLOR= # override the accent color (default #e32a6d), e.g. "#00ff00"; hover shade derived PUBLIC_SECONDARY_COLOR= # override the secondary color (default #ffaf25), e.g. "#0000ff"; hover shade derived +PUBLIC_FAVICON= # favicon URL (absolute, or /favicon.png for a file in static/); default is a filled circle in the accent color diff --git a/.gitignore b/.gitignore index 3b462cb..5f67bcb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules +.local # Output .output @@ -18,6 +19,10 @@ Thumbs.db !.env.example !.env.test +# Deploy: only the example unit is tracked +/deploy/* +!/deploy/production-example.service + # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2c37403 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 dtonon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index f49aba9..1bc50a0 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,10 @@ Squalk is configured entirely through environment variables (all prefixed `PUBLI | `PUBLIC_GROUP_ID` | in simple mode | — | The single forum's group id. Required when `PUBLIC_MODE=simple`; ignored in full mode, where rooms are selected at runtime. | | `PUBLIC_TITLE` | no | group name | Title shown in the top bar. When empty it falls back to the group's name. | | `PUBLIC_JOINCODE` | no | `no` | `yes` to show an invite-code field when a join request is rejected (for code-gated relays). | +| `PUBLIC_SSR` | no | `no` | `yes` to render pages on the server (crawlable HTML, real 404s). The build then targets Node (`node build`) instead of a static bundle; see [Deploying](#deploying). | +| `PUBLIC_SSR_WARM` | no | `yes` | With `PUBLIC_SSR=yes`, each client-side navigation also asks the server to fetch and cache that page, so a later refresh, shared link or crawler hit is served warm. Costs one extra relay query per navigation on the server; set to `no` to disable. | +| `PUBLIC_SSR_CACHE_FRESH` | no | `300` | Seconds a server-rendered snapshot is served as is. Also the edge cache's `s-maxage`. | +| `PUBLIC_SSR_CACHE_STALE` | no | `21600` | Seconds after which a snapshot is no longer served while being refreshed in the background (until then a stale page is answered instantly and updated for the next visitor). Also the edge cache's `stale-while-revalidate`. | | `PUBLIC_SEARCH` | no | `no` | `yes` to show a search box at the top of the homepage. Requires a relay with NIP-50 search support. | | `PUBLIC_LABELS` | no | — | Comma-separated discussion labels offered when composing, e.g. `bug,feature,question`. | | `PUBLIC_BLOSSOM_URL` | no | — | Blossom server URL used for media uploads, e.g. `https://blossom.primal.net`. Uploads are disabled when unset. | @@ -89,12 +93,26 @@ npm run dev -- --open ## Building -To create a production version of your app: +Two deployment targets share the same code base, selected by `PUBLIC_SSR`: -```sh -npm run build -``` +- **Static (default, `PUBLIC_SSR=no`)** — `npm run build` (or `just build`) writes a single-page bundle to `build/`; serve it from any web server with `index.html` as the fallback for unknown paths. Everything is fetched by the browser. +- **Server-rendered (`PUBLIC_SSR=yes`)** — `just build-ssr` writes a Node app to `build/`. Pages arrive as crawlable HTML (threads, rooms, resources, contacts, with description/Open Graph tags, JSON-LD, a live `robots.txt` and `sitemap.xml`, and real 404s), then the browser takes over exactly as in the static build. The server reads the relay anonymously, so it only ever renders public content; members see their private rooms once the client is running. -You can preview the production build with `npm run preview`. +Preview a build locally with `npm run preview` (static) or `node --env-file=.env.production build` (server). -> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. +## Deploying + +`just deploy ` rsyncs the static bundle to `~/squalk/` on the host and purges the Cloudflare cache. + +`just deploy-ssr ` builds with `--mode ` (so vite bakes `.env.` in), ships the Node build, `package.json`/`package-lock.json` and `.env.` (as `.env` in the app dir, since the server reads the `PUBLIC_*` values at runtime), runs `npm ci --omit=dev` and restarts the instance's systemd unit. Everything instance-specific lives in `.env..local` (gitignored, never shipped): `DEPLOY_HOST`, `DEPLOY_DIR` and `DEPLOY_SERVICE` (all required), plus the Cloudflare credentials (`CF_ZONE_ID`/`CF_API_TOKEN`) for the cache purge. Multiple instances coexist by giving each its own mode, directory, unit and port. On the host you need: + +- Node 22 or newer (the relay client uses the built-in `WebSocket`). +- The unit from [`deploy/production-example.service`](deploy/production-example.service), with `ORIGIN` set to the public URL — it feeds canonical links, `robots.txt` and the sitemap. +- A reverse proxy in front of the port in `PORT`, replacing whatever served the static files before. With Caddy: + + ``` + forum.example.com { + reverse_proxy 127.0.0.1:3000 + } + ``` +- If Cloudflare sits in front, a cache rule that caches HTML and respects origin headers: pages and snapshots are sent with `Cache-Control: public, max-age=0, s-maxage=, stale-while-revalidate=` (by default served for five minutes, then refreshed in the background for up to six hours), the same windows the server's own in-memory cache uses. `just deploy-ssr` purges the cache after each release. diff --git a/deploy/production-example.service b/deploy/production-example.service new file mode 100644 index 0000000..b62db51 --- /dev/null +++ b/deploy/production-example.service @@ -0,0 +1,25 @@ +# Example systemd unit for the server-rendered build (PUBLIC_SSR=yes). +# Copy to /etc/systemd/system/.service, adjust the paths, the port +# and ORIGIN, then: systemctl daemon-reload && systemctl enable --now +[Unit] +Description=Squalk forum +After=network.target + +[Service] +Type=simple +# Ephemeral unprivileged user, created by systemd for the service's lifetime +# (implies ProtectSystem=strict, ProtectHome=read-only, PrivateTmp, +# NoNewPrivileges). The app dir stays root-owned; the service only reads it. +DynamicUser=yes +WorkingDirectory=/srv/squalk +# PUBLIC_* values read at runtime by the server (deploy-ssr copies .env. here) +EnvironmentFile=/srv/squalk/.env +# ORIGIN is the public URL: it drives canonical links, robots.txt and the sitemap +Environment=NODE_ENV=production PORT=3000 ORIGIN=https://forum.example.com +ExecStart=/usr/bin/node build +Restart=on-failure +TimeoutStopSec=10 +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/homepages.sh b/homepages.sh deleted file mode 100644 index d06ee30..0000000 --- a/homepages.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/sh - -### -# HOME -### - -nak event --prompt-sec -k 30023 -c "https://basspistol.org/7c1a1054c24fdf0972fc2259c43cb516f42adf2d95a08e7ad50c8be0dfd5fdfd.png - -### This is a forum Managed by nostr:npub15a5jljcjaqgty64hn5v3993zx09yjz2gepwt0q8strvlegdy2q8q6xkqfd — the Outernational Music Syndicate. - -It is intended for all things music, including but not limited to technical questions, referals, self-promotion (it's a dirty job, but someone has to do it.). Read more in the [about page](/resource/about)" --tag t=squalk-partial --tag d=home --tag title=Homepage basspistol.org - -### -# ABOUT -### - -nak event --prompt-sec -k 30023 -c "https://basspistol.org/7c1a1054c24fdf0972fc2259c43cb516f42adf2d95a08e7ad50c8be0dfd5fdfd.png - -# About this forum - -This is a community forum managed by nostr:npub15a5jljcjaqgty64hn5v3993zx09yjz2gepwt0q8strvlegdy2q8q6xkqfd and facilitated for people who enjoy, create, promote and grow Music. It is intended for all things music, including but not limited to technical questions, referals, self-promotion (it's a dirty job, but someone has to do it.) and it runs on [Nostr](https://nostr.org). - -## What is Nostr? - -Nostr — Notes and Other Stuff Transmitted by Relays — is an open protocol for decentralized social communication. There is no central server and no company in control: you own a keypair, your posts are signed events, and relays pass them around. That simplicity makes Nostr a fertile ground for experimentation, and this forum is a place to do that thinking out loud, together. - -## What we talk about - -Common themes include: - -- Feedback — stuck in the creative process, uncertain where to take your tune, wondering if the high-hat cuts too deep? Just ask! -- Promotion and growth — how to launch a project, reach people, write good posts, and tell the story of what you are creating without spamming. -- Synths and software — proposing, refining, and discussing the must have in your studio. -- Showcase — release announcements, demos, and works in progress that deserve eyes and feedback. -- Help and onboarding — a friendly place to ask the obvious questions about how to leverage nostr for your music project, get unstuck, and help newcomers find their footing. - -## How it works - -Discussions are long-form threads you can follow and search, with a live chat sidebar for quick back-and-forth. Everything is built on open Nostr standards, so your identity and your posts are portable — they are not locked inside this site. Sign in with your Nostr key, join a room, and you are ready to take part. - -## A community, not a feed - -This forum favors depth over noise. We would rather have one thoughtful thread than ten throwaway hot takes. Be generous with your knowledge, credit the work of others, and keep the tone welcoming. - -Before posting, take a moment to read the [Netiquette guidelines](/resource/guidelines) linked in the Resources menu — they describe the simple etiquette that keeps this place worth coming back to. - -Welcome aboard, and happy musing." --tag t=squalk-resource --tag d=about --tag title=About basspistol.org - -### -# TERMS -### - -nak event --prompt-sec -k 30023 -c "https://basspistol.org/7c1a1054c24fdf0972fc2259c43cb516f42adf2d95a08e7ad50c8be0dfd5fdfd.png - -# Forum Netiquette - -Welcome! This forum works best when everyone treats it as a shared space. These guidelines describe the basic etiquette we expect from every member. They are not rigid rules so much as the habits that keep discussion useful, friendly, and worth coming back to. - -## Be respectful - -Treat others the way you would want to be treated in person. Disagree with ideas, not people. Personal attacks, insults, harassment, and discriminatory language have no place here. A heated topic is no excuse for contempt — if a thread makes you angry, step away before replying. - -## Stay on topic - -Post in the room that fits your subject, and keep replies relevant to the thread you are in. If a conversation drifts somewhere interesting, start a new thread for it rather than derailing the original one. Focused threads are easier for everyone to follow and to search later. - -## Write so others can read you - -Give your thread a descriptive title and take a moment to organize your thoughts. Break long posts into paragraphs, quote only the part you are replying to, and avoid writing in all capitals — it reads as shouting. Before asking a question, search to see whether it has already been answered. - -## Add value - -Aim to contribute something: an answer, a perspective, a useful link, or a thoughtful question. Avoid one-word replies, duplicate posts, and +1 comments that add noise. If you simply appreciate a post, a reaction is enough — there is no need to bump a thread without new content. - -## Respect privacy and ownership - -Do not share other people's private information, and do not repost private messages without permission. When you quote or build on someone else's work, credit them. Spam, advertising, and self-promotion that ignores the conversation are not welcome. - -## Assume good faith - -Tone is hard to read online. Assume the person you are replying to means well, ask for clarification before accusing, and be willing to admit when you were wrong. A short, honest my mistake defuses more arguments than any long rebuttal. - -## Welcome newcomers - -Everyone was new once. If a newcomer phrases a question awkwardly or misses an existing thread, point them in the right direction kindly instead of mocking them. The way regulars treat beginners sets the tone for the whole community, and patient answers are remembered long after the question is forgotten. - -## A note on language - -This is an international community, and many members are not writing in their first language. Be patient with grammar and spelling, and prefer plain wording over heavy slang, sarcasm, or inside jokes that are easy to misread. When tone matters, a short clarifying line or a reaction can prevent a misunderstanding before it starts. - -## When things go wrong - -If you see a post that breaks these guidelines, report it to the moderators instead of starting a public fight. Moderators are volunteers keeping the community healthy: follow their guidance, and reach out privately if you disagree with a decision. - -By taking part in this forum, you agree to follow this etiquette. Thanks for helping keep the community welcoming for everyone." --tag t=squalk-resource --tag d=guidelines --tag title=Guidelines basspistol.org diff --git a/justfile b/justfile index 2326605..9b886b6 100644 --- a/justfile +++ b/justfile @@ -1,8 +1,49 @@ +set dotenv-load + +# Cloudflare credentials: per deploy target in .env..local (gitignored, +# never shipped to the server), falling back to the environment / .env + dev: npm run dev +# Static single-page bundle (served by any web server) build: - npm run build + PUBLIC_SSR=no npm run build + +# Server-rendered bundle (needs Node 22+ on the host, see deploy/production-example.service) +build-ssr: + PUBLIC_SSR=yes npm run build deploy target: build rsync -av --delete --progress build/ {{target}}:~/squalk/ + @just purge-web-cache + +# Ships the Node build plus its runtime deps and env, then restarts the unit. +# `mode` picks the instance: vite bakes .env. into the build, and +# .env..local provides the deployment details (DEPLOY_HOST, DEPLOY_DIR, +# DEPLOY_SERVICE) plus the Cloudflare credentials. The remote step runs in a +# login shell so the user's PATH (npm, nvm…) applies. +deploy-ssr mode: + #!/usr/bin/env bash + set -euo pipefail + [ -f .env.{{mode}}.local ] || { echo "Missing .env.{{mode}}.local"; exit 1; } + set -a; source .env.{{mode}}.local; set +a + : "${DEPLOY_HOST:?DEPLOY_HOST missing in .env.{{mode}}.local}" + : "${DEPLOY_DIR:?DEPLOY_DIR missing in .env.{{mode}}.local}" + : "${DEPLOY_SERVICE:?DEPLOY_SERVICE missing in .env.{{mode}}.local}" + PUBLIC_SSR=yes npm run build -- --mode {{mode}} + rsync -av --delete --progress --exclude node_modules build/ "$DEPLOY_HOST:$DEPLOY_DIR/build/" + rsync -av package.json package-lock.json "$DEPLOY_HOST:$DEPLOY_DIR/" + rsync -av .env.{{mode}} "$DEPLOY_HOST:$DEPLOY_DIR/.env" + ssh "$DEPLOY_HOST" "\$SHELL -l -c 'cd $DEPLOY_DIR && npm ci --omit=dev && sudo systemctl restart $DEPLOY_SERVICE'" + just purge-web-cache {{mode}} + +purge-web-cache mode="production": + #!/usr/bin/env bash + if [ -f .env.{{mode}}.local ]; then set -a; source .env.{{mode}}.local; set +a; fi + echo -e "\nPurging Cloudflare cache... for zone ${CF_ZONE_ID:-}" + curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID:-}/purge_cache" \ + -H "Authorization: Bearer ${CF_API_TOKEN:-}" \ + -H "Content-Type: application/json" \ + --data '{"purge_everything": true}' \ + | jq -r 'if .success then "✅ Cache purged successfully" else "‼️ Error: " + (.errors[0].message // "Unknown error") end' diff --git a/package-lock.json b/package-lock.json index f30ebca..57c431e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,3538 +1,4218 @@ { - "name": "squalk", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "squalk", - "version": "0.0.1", - "dependencies": { - "@djot/djot": "^0.3.2", - "@nostr/gadgets": "npm:@jsr/nostr__gadgets@^0.1.8", - "@nostr/tools": "npm:@jsr/nostr__tools@^2.23.3" - }, - "devDependencies": { - "@eslint/compat": "^2.0.4", - "@eslint/js": "^10.0.1", - "@sveltejs/adapter-auto": "^7.0.1", - "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.57.0", - "@sveltejs/vite-plugin-svelte": "^7.0.0", - "@tailwindcss/forms": "^0.5.11", - "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.2.2", - "@types/node": "^22", - "eslint": "^10.2.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-svelte": "^3.17.0", - "globals": "^17.4.0", - "prettier": "^3.8.1", - "prettier-plugin-svelte": "^3.5.1", - "prettier-plugin-tailwindcss": "^0.7.2", - "svelte": "^5.55.2", - "svelte-check": "^4.4.6", - "tailwindcss": "^4.2.2", - "typescript": "^6.0.2", - "typescript-eslint": "^8.58.1", - "vite": "^8.0.7" - } - }, - "node_modules/@djot/djot": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@djot/djot/-/djot-0.3.2.tgz", - "integrity": "sha512-joMKR24B8rxueyFiJbpZAqEiypjvOyzTxzkhyr0q5mM/sUBaOD3unna/9IxtOotFugViyYlkIRaiXg3xM//zxg==", - "license": "MIT", - "bin": { - "djot": "lib/cli.js" - }, - "engines": { - "node": ">=17.0.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/compat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.0.5.tgz", - "integrity": "sha512-IbHDbHJfkVNv6xjlET8AIVo/K1NQt7YT4Rp6ok/clyBGcpRx1l6gv0Rq3vBvYfPJIZt6ODf66Zq08FJNDpnzgg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "peerDependencies": { - "eslint": "^8.40 || 9 || 10" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", - "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", - "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jsr/fiatjaf__lru-cache": { - "version": "0.0.1", - "resolved": "https://npm.jsr.io/~/11/@jsr/fiatjaf__lru-cache/0.0.1.tgz", - "integrity": "sha512-9MbsYMHWGe751L4E7XpxV3jWydd+N6RJv+c7tYtK03lST5xdcULn2QZorImZgCbP8qUu47n1RL7RrqvjZdmXPg==" - }, - "node_modules/@jsr/henrygd__semaphore": { - "version": "0.0.2", - "resolved": "https://npm.jsr.io/~/11/@jsr/henrygd__semaphore/0.0.2.tgz", - "integrity": "sha512-nrwZ8HaqU1Agb2ij8omIxaOCAsKkDHWcwS9hTRumPhZuptwh6/0BJExBd8+eClTYM7jBnZxK+cP4WJRTcHBvCA==" - }, - "node_modules/@jsr/nostr__tools": { - "version": "2.23.3", - "resolved": "https://npm.jsr.io/~/11/@jsr/nostr__tools/2.23.3.tgz", - "integrity": "sha512-jknaAXP0YMnibM7hd4K6+CpASOMHuP5b8fBV9i5OKv5KHvcjTZClW1epBbwdybFrsi01RcmWwiYdSwUGnfmGAA==", - "dependencies": { - "@noble/ciphers": "2.1.1", - "@noble/curves": "2.0.1", - "@noble/hashes": "2.0.1", - "@scure/base": "2.0.0", - "@scure/bip32": "2.0.1", - "@scure/bip39": "2.0.1", - "nostr-wasm": "0.1.0" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@noble/ciphers": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", - "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", - "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "2.0.1" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", - "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@nostr/gadgets": { - "name": "@jsr/nostr__gadgets", - "version": "0.1.8", - "resolved": "https://npm.jsr.io/~/11/@jsr/nostr__gadgets/0.1.8.tgz", - "integrity": "sha512-N6890VkkXqXxIVlp/fOIQRaHOmNrIZrdafua/0kSSsko8JImuT3hFYJxG9jt0jpx+/18QG0ogaFrXOZdWgS2Qg==", - "dependencies": { - "@jsr/fiatjaf__lru-cache": "^0.0.1", - "@jsr/henrygd__semaphore": "^0.0.2", - "@jsr/nostr__tools": "^2.23.0", - "@noble/hashes": "2.0.1", - "idb-keyval": "^6.2.1" - } - }, - "node_modules/@nostr/tools": { - "name": "@jsr/nostr__tools", - "version": "2.23.3", - "resolved": "https://npm.jsr.io/~/11/@jsr/nostr__tools/2.23.3.tgz", - "integrity": "sha512-jknaAXP0YMnibM7hd4K6+CpASOMHuP5b8fBV9i5OKv5KHvcjTZClW1epBbwdybFrsi01RcmWwiYdSwUGnfmGAA==", - "dependencies": { - "@noble/ciphers": "2.1.1", - "@noble/curves": "2.0.1", - "@noble/hashes": "2.0.1", - "@scure/base": "2.0.0", - "@scure/bip32": "2.0.1", - "@scure/bip39": "2.0.1", - "nostr-wasm": "0.1.0" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", - "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", - "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", - "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", - "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@scure/base": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", - "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.0.1.tgz", - "integrity": "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==", - "license": "MIT", - "dependencies": { - "@noble/curves": "2.0.1", - "@noble/hashes": "2.0.1", - "@scure/base": "2.0.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.0.1.tgz", - "integrity": "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "2.0.1", - "@scure/base": "2.0.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", - "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^8.9.0" - } - }, - "node_modules/@sveltejs/adapter-auto": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-7.0.1.tgz", - "integrity": "sha512-dvuPm1E7M9NI/+canIQ6KKQDU2AkEefEZ2Dp7cY6uKoPq9Z/PhOXABe526UdW2mN986gjVkuSLkOYIBnS/M2LQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@sveltejs/kit": "^2.0.0" - } - }, - "node_modules/@sveltejs/adapter-static": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", - "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@sveltejs/kit": "^2.0.0" - } - }, - "node_modules/@sveltejs/kit": { - "version": "2.58.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.58.0.tgz", - "integrity": "sha512-kT9GCN8yJTkCK1W+Gi/bvGooWAM7y7WXP+yd+rf6QOIjyoK1ERPrMwSufXJUNu2pMWIqruhFvmz+LbOqsEmKmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@sveltejs/acorn-typescript": "^1.0.5", - "@types/cookie": "^0.6.0", - "acorn": "^8.14.1", - "cookie": "^0.6.0", - "devalue": "^5.6.4", - "esm-env": "^1.2.2", - "kleur": "^4.1.5", - "magic-string": "^0.30.5", - "mrmime": "^2.0.0", - "set-cookie-parser": "^3.0.0", - "sirv": "^3.0.0" - }, - "bin": { - "svelte-kit": "svelte-kit.js" - }, - "engines": { - "node": ">=18.13" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": "^5.3.3 || ^6.0.0", - "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.0.0.tgz", - "integrity": "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deepmerge": "^4.3.1", - "magic-string": "^0.30.21", - "obug": "^2.1.0", - "vitefu": "^1.1.2" - }, - "engines": { - "node": "^20.19 || ^22.12 || >=24" - }, - "peerDependencies": { - "svelte": "^5.46.4", - "vite": "^8.0.0-beta.7 || ^8.0.0" - } - }, - "node_modules/@tailwindcss/forms": { - "version": "0.5.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz", - "integrity": "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mini-svg-data-uri": "^1.2.3" - }, - "peerDependencies": { - "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", - "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", - "jiti": "^2.6.1", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.2.4" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", - "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.4", - "@tailwindcss/oxide-darwin-arm64": "4.2.4", - "@tailwindcss/oxide-darwin-x64": "4.2.4", - "@tailwindcss/oxide-freebsd-x64": "4.2.4", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", - "@tailwindcss/oxide-linux-x64-musl": "4.2.4", - "@tailwindcss/oxide-wasm32-wasi": "4.2.4", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", - "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", - "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", - "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", - "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", - "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", - "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", - "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", - "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", - "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", - "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", - "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", - "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/typography": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", - "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "6.0.10" - }, - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz", - "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.2.4", - "@tailwindcss/oxide": "4.2.4", - "tailwindcss": "4.2.4" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7 || ^8" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", - "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz", - "integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.1", - "@typescript-eslint/type-utils": "8.59.1", - "@typescript-eslint/utils": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.59.1", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz", - "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.59.1", - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/typescript-estree": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz", - "integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.1", - "@typescript-eslint/types": "^8.59.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz", - "integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz", - "integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz", - "integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/typescript-estree": "8.59.1", - "@typescript-eslint/utils": "8.59.1", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz", - "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz", - "integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.59.1", - "@typescript-eslint/tsconfig-utils": "8.59.1", - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz", - "integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.1", - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/typescript-estree": "8.59.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz", - "integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.1", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/aria-query": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", - "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/devalue": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.7.1.tgz", - "integrity": "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==", - "dev": true, - "license": "MIT" - }, - "node_modules/enhanced-resolve": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", - "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", - "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.5.5", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-prettier": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", - "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "funding": { - "url": "https://opencollective.com/eslint-config-prettier" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-svelte": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.17.1.tgz", - "integrity": "sha512-NyiXHtS3Ni7e532RBwS9OXlMKDIrENg3gY+/+ODjZzQx2xhU3NlJ+nIl1a93iUUQeiJL3lS8KLmY+W8hklzweQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.6.1", - "@jridgewell/sourcemap-codec": "^1.5.0", - "esutils": "^2.0.3", - "globals": "^16.0.0", - "known-css-properties": "^0.37.0", - "postcss": "^8.4.49", - "postcss-load-config": "^3.1.4", - "postcss-safe-parser": "^7.0.0", - "semver": "^7.6.3", - "svelte-eslint-parser": "^1.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", - "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "svelte": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-svelte/node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esm-env": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrap": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.5.tgz", - "integrity": "sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "peerDependencies": { - "@typescript-eslint/types": "^8.2.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/types": { - "optional": true - } - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "17.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", - "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/idb-keyval": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", - "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", - "license": "Apache-2.0" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-reference": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.6" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/known-css-properties": { - "version": "0.37.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", - "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/mini-svg-data-uri": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", - "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", - "dev": true, - "license": "MIT", - "bin": { - "mini-svg-data-uri": "cli.js" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/nostr-wasm": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", - "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", - "license": "MIT" - }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-load-config/node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss-safe-parser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", - "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-scss": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", - "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss-scss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.4.29" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", - "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-plugin-svelte": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz", - "integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "prettier": "^3.0.0", - "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" - } - }, - "node_modules/prettier-plugin-tailwindcss": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.4.tgz", - "integrity": "sha512-UKii4RjY05SNt/WQi6/NcOn/LsT0/ILLXsxygjbRg5/YZelsSu5jTqorYHPDGq4nZy5q5hpCu+XdGZ1xaJEQgw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.19" - }, - "peerDependencies": { - "@ianvs/prettier-plugin-sort-imports": "*", - "@prettier/plugin-hermes": "*", - "@prettier/plugin-oxc": "*", - "@prettier/plugin-pug": "*", - "@shopify/prettier-plugin-liquid": "*", - "@trivago/prettier-plugin-sort-imports": "*", - "@zackad/prettier-plugin-twig": "*", - "prettier": "^3.0", - "prettier-plugin-astro": "*", - "prettier-plugin-css-order": "*", - "prettier-plugin-jsdoc": "*", - "prettier-plugin-marko": "*", - "prettier-plugin-multiline-arrays": "*", - "prettier-plugin-organize-attributes": "*", - "prettier-plugin-organize-imports": "*", - "prettier-plugin-sort-imports": "*", - "prettier-plugin-svelte": "*" - }, - "peerDependenciesMeta": { - "@ianvs/prettier-plugin-sort-imports": { - "optional": true - }, - "@prettier/plugin-hermes": { - "optional": true - }, - "@prettier/plugin-oxc": { - "optional": true - }, - "@prettier/plugin-pug": { - "optional": true - }, - "@shopify/prettier-plugin-liquid": { - "optional": true - }, - "@trivago/prettier-plugin-sort-imports": { - "optional": true - }, - "@zackad/prettier-plugin-twig": { - "optional": true - }, - "prettier-plugin-astro": { - "optional": true - }, - "prettier-plugin-css-order": { - "optional": true - }, - "prettier-plugin-jsdoc": { - "optional": true - }, - "prettier-plugin-marko": { - "optional": true - }, - "prettier-plugin-multiline-arrays": { - "optional": true - }, - "prettier-plugin-organize-attributes": { - "optional": true - }, - "prettier-plugin-organize-imports": { - "optional": true - }, - "prettier-plugin-sort-imports": { - "optional": true - }, - "prettier-plugin-svelte": { - "optional": true - } - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/rolldown": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", - "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.127.0", - "@rolldown/pluginutils": "1.0.0-rc.17" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.17", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", - "@rolldown/binding-darwin-x64": "1.0.0-rc.17", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" - } - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-cookie-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", - "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/sirv": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/svelte": { - "version": "5.55.5", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.5.tgz", - "integrity": "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.5", - "@types/estree": "^1.0.5", - "@types/trusted-types": "^2.0.7", - "acorn": "^8.12.1", - "aria-query": "5.3.1", - "axobject-query": "^4.1.0", - "clsx": "^2.1.1", - "devalue": "^5.6.4", - "esm-env": "^1.2.1", - "esrap": "^2.2.4", - "is-reference": "^3.0.3", - "locate-character": "^3.0.0", - "magic-string": "^0.30.11", - "zimmerframe": "^1.1.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/svelte-check": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.6.tgz", - "integrity": "sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "chokidar": "^4.0.1", - "fdir": "^6.2.0", - "picocolors": "^1.0.0", - "sade": "^1.7.4" - }, - "bin": { - "svelte-check": "bin/svelte-check" - }, - "engines": { - "node": ">= 18.0.0" - }, - "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": ">=5.0.0" - } - }, - "node_modules/svelte-eslint-parser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.6.0.tgz", - "integrity": "sha512-qoB1ehychT6OxEtQAqc/guSqLS20SlA53Uijl7x375s8nlUT0lb9ol/gzraEEatQwsyPTJo87s2CmKL9Xab+Uw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.0.0", - "postcss": "^8.4.49", - "postcss-scss": "^4.0.9", - "postcss-selector-parser": "^7.0.0", - "semver": "^7.7.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0", - "pnpm": "10.30.3" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "svelte": { - "optional": true - } - } - }, - "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-eslint-parser/node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-eslint-parser/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tailwindcss": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", - "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.1.tgz", - "integrity": "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.59.1", - "@typescript-eslint/parser": "8.59.1", - "@typescript-eslint/typescript-estree": "8.59.1", - "@typescript-eslint/utils": "8.59.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", - "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.10", - "rolldown": "1.0.0-rc.17", - "tinyglobby": "^0.2.16" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitefu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", - "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", - "dev": true, - "license": "MIT", - "workspaces": [ - "tests/deps/*", - "tests/projects/*", - "tests/projects/workspace/packages/*" - ], - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zimmerframe": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", - "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "dev": true, - "license": "MIT" - } - } + "name": "squalk", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "squalk", + "version": "0.0.1", + "dependencies": { + "@djot/djot": "^0.3.2", + "@nostr/gadgets": "npm:@jsr/nostr__gadgets@^0.1.8", + "@nostr/tools": "npm:@jsr/nostr__tools@^2.23.3", + "nstart-modal": "^2.1.0", + "uqr": "^0.1.3" + }, + "devDependencies": { + "@eslint/compat": "^2.0.4", + "@eslint/js": "^10.0.1", + "@sveltejs/adapter-node": "^5.5.7", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.57.0", + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@tailwindcss/forms": "^0.5.11", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.2.2", + "@types/node": "^22", + "eslint": "^10.2.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.17.0", + "globals": "^17.4.0", + "prettier": "^3.8.1", + "prettier-plugin-svelte": "^3.5.1", + "prettier-plugin-tailwindcss": "^0.7.2", + "svelte": "^5.55.2", + "svelte-check": "^4.4.6", + "tailwindcss": "^4.2.2", + "typescript": "^6.0.2", + "typescript-eslint": "^8.58.1", + "vite": "^8.0.7" + } + }, + "node_modules/@djot/djot": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@djot/djot/-/djot-0.3.2.tgz", + "integrity": "sha512-joMKR24B8rxueyFiJbpZAqEiypjvOyzTxzkhyr0q5mM/sUBaOD3unna/9IxtOotFugViyYlkIRaiXg3xM//zxg==", + "license": "MIT", + "bin": { + "djot": "lib/cli.js" + }, + "engines": { + "node": ">=17.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/compat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.0.5.tgz", + "integrity": "sha512-IbHDbHJfkVNv6xjlET8AIVo/K1NQt7YT4Rp6ok/clyBGcpRx1l6gv0Rq3vBvYfPJIZt6ODf66Zq08FJNDpnzgg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "peerDependencies": { + "eslint": "^8.40 || 9 || 10" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsr/fiatjaf__lru-cache": { + "version": "0.0.1", + "resolved": "https://npm.jsr.io/~/11/@jsr/fiatjaf__lru-cache/0.0.1.tgz", + "integrity": "sha512-9MbsYMHWGe751L4E7XpxV3jWydd+N6RJv+c7tYtK03lST5xdcULn2QZorImZgCbP8qUu47n1RL7RrqvjZdmXPg==" + }, + "node_modules/@jsr/henrygd__semaphore": { + "version": "0.0.2", + "resolved": "https://npm.jsr.io/~/11/@jsr/henrygd__semaphore/0.0.2.tgz", + "integrity": "sha512-nrwZ8HaqU1Agb2ij8omIxaOCAsKkDHWcwS9hTRumPhZuptwh6/0BJExBd8+eClTYM7jBnZxK+cP4WJRTcHBvCA==" + }, + "node_modules/@jsr/nostr__tools": { + "version": "2.23.3", + "resolved": "https://npm.jsr.io/~/11/@jsr/nostr__tools/2.23.3.tgz", + "integrity": "sha512-jknaAXP0YMnibM7hd4K6+CpASOMHuP5b8fBV9i5OKv5KHvcjTZClW1epBbwdybFrsi01RcmWwiYdSwUGnfmGAA==", + "dependencies": { + "@noble/ciphers": "2.1.1", + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0", + "@scure/bip32": "2.0.1", + "@scure/bip39": "2.0.1", + "nostr-wasm": "0.1.0" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@noble/ciphers": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", + "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nostr/gadgets": { + "name": "@jsr/nostr__gadgets", + "version": "0.1.8", + "resolved": "https://npm.jsr.io/~/11/@jsr/nostr__gadgets/0.1.8.tgz", + "integrity": "sha512-N6890VkkXqXxIVlp/fOIQRaHOmNrIZrdafua/0kSSsko8JImuT3hFYJxG9jt0jpx+/18QG0ogaFrXOZdWgS2Qg==", + "dependencies": { + "@jsr/fiatjaf__lru-cache": "^0.0.1", + "@jsr/henrygd__semaphore": "^0.0.2", + "@jsr/nostr__tools": "^2.23.0", + "@noble/hashes": "2.0.1", + "idb-keyval": "^6.2.1" + } + }, + "node_modules/@nostr/tools": { + "name": "@jsr/nostr__tools", + "version": "2.23.3", + "resolved": "https://npm.jsr.io/~/11/@jsr/nostr__tools/2.23.3.tgz", + "integrity": "sha512-jknaAXP0YMnibM7hd4K6+CpASOMHuP5b8fBV9i5OKv5KHvcjTZClW1epBbwdybFrsi01RcmWwiYdSwUGnfmGAA==", + "dependencies": { + "@noble/ciphers": "2.1.1", + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0", + "@scure/bip32": "2.0.1", + "@scure/bip39": "2.0.1", + "nostr-wasm": "0.1.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "29.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.3.tgz", + "integrity": "sha512-ZaOxZceP7SOUW7Lqw5IRVweSQYWaeIPnXIGLiB690EBA3FGJTO40EEr2L5yZplJWsgTCogILRSpcAe7+U0Otdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", + "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scure/base": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", + "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.0.1.tgz", + "integrity": "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.0.1.tgz", + "integrity": "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "5.5.7", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.5.7.tgz", + "integrity": "sha512-uOfc9eVlI3A37RRSaKcgrheBYPrfJwC9VMqDp8x/O6tlKdcLLvHThSWD0KNIbjQ/d+7bwLGx3vx6aowAcRfd2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-replace": "^6.0.3", + "rollup": "^4.59.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", + "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.58.0.tgz", + "integrity": "sha512-kT9GCN8yJTkCK1W+Gi/bvGooWAM7y7WXP+yd+rf6QOIjyoK1ERPrMwSufXJUNu2pMWIqruhFvmz+LbOqsEmKmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.6.4", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3 || ^6.0.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.0.0.tgz", + "integrity": "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" + } + }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz", + "integrity": "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", + "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.4" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", + "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-x64": "4.2.4", + "@tailwindcss/oxide-freebsd-x64": "4.2.4", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-x64-musl": "4.2.4", + "@tailwindcss/oxide-wasm32-wasi": "4.2.4", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", + "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", + "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", + "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", + "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", + "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", + "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", + "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", + "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", + "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", + "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", + "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", + "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz", + "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.4", + "@tailwindcss/oxide": "4.2.4", + "tailwindcss": "4.2.4" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz", + "integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.1", + "@typescript-eslint/type-utils": "8.59.1", + "@typescript-eslint/utils": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz", + "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.1", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz", + "integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.1", + "@typescript-eslint/types": "^8.59.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz", + "integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz", + "integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz", + "integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1", + "@typescript-eslint/utils": "8.59.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz", + "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz", + "integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.1", + "@typescript-eslint/tsconfig-utils": "8.59.1", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz", + "integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.1", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz", + "integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.7.1.tgz", + "integrity": "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", + "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", + "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.17.1.tgz", + "integrity": "sha512-NyiXHtS3Ni7e532RBwS9OXlMKDIrENg3gY+/+ODjZzQx2xhU3NlJ+nIl1a93iUUQeiJL3lS8KLmY+W8hklzweQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.6.1", + "@jridgewell/sourcemap-codec": "^1.5.0", + "esutils": "^2.0.3", + "globals": "^16.0.0", + "known-css-properties": "^0.37.0", + "postcss": "^8.4.49", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^7.0.0", + "semver": "^7.6.3", + "svelte-eslint-parser": "^1.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-svelte/node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.5.tgz", + "integrity": "sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", + "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/idb-keyval": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", + "license": "Apache-2.0" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nostr-wasm": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", + "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", + "license": "MIT" + }, + "node_modules/nstart-modal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nstart-modal/-/nstart-modal-2.1.0.tgz", + "integrity": "sha512-PolShYoWK07yJJbINtUn/IoOI5B0lmXRG9zOY9dirKKVjmMWFKjYLlafunNOl94EGcEndzAPWJAFDCt8flYRqg==" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz", + "integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.4.tgz", + "integrity": "sha512-UKii4RjY05SNt/WQi6/NcOn/LsT0/ILLXsxygjbRg5/YZelsSu5jTqorYHPDGq4nZy5q5hpCu+XdGZ1xaJEQgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-hermes": "*", + "@prettier/plugin-oxc": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-hermes": { + "optional": true + }, + "@prettier/plugin-oxc": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.55.5", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.5.tgz", + "integrity": "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.6.4", + "esm-env": "^1.2.1", + "esrap": "^2.2.4", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.6.tgz", + "integrity": "sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.6.0.tgz", + "integrity": "sha512-qoB1ehychT6OxEtQAqc/guSqLS20SlA53Uijl7x375s8nlUT0lb9ol/gzraEEatQwsyPTJo87s2CmKL9Xab+Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.0", + "postcss": "^8.4.49", + "postcss-scss": "^4.0.9", + "postcss-selector-parser": "^7.0.0", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0", + "pnpm": "10.30.3" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", + "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.1.tgz", + "integrity": "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.1", + "@typescript-eslint/parser": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1", + "@typescript-eslint/utils": "8.59.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uqr": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.3.tgz", + "integrity": "sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==", + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" + } + } } diff --git a/package.json b/package.json index 8327b54..064ee7f 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "squalk", + "license": "MIT", "private": true, "version": "0.0.1", "type": "module", @@ -16,7 +17,7 @@ "devDependencies": { "@eslint/compat": "^2.0.4", "@eslint/js": "^10.0.1", - "@sveltejs/adapter-auto": "^7.0.1", + "@sveltejs/adapter-node": "^5.5.7", "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.57.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", @@ -41,6 +42,11 @@ "dependencies": { "@djot/djot": "^0.3.2", "@nostr/gadgets": "npm:@jsr/nostr__gadgets@^0.1.8", - "@nostr/tools": "npm:@jsr/nostr__tools@^2.23.3" + "@nostr/tools": "npm:@jsr/nostr__tools@^2.23.3", + "nstart-modal": "^2.1.0", + "uqr": "^0.1.3" + }, + "overrides": { + "lightningcss": "^1.33.0" } } diff --git a/src/app.d.ts b/src/app.d.ts index 520c421..53a4264 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,10 +1,28 @@ // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces +import type { + ForumShell, + OverviewSnapshot, + Profiles, + ThreadsSnapshot, +} from "$lib/forum/snapshot"; +import type { ThreadDetail } from "$lib/forum/thread"; + declare global { + // Injected by vite.config.ts from PUBLIC_SSR + const __SQUALK_SSR__: boolean; + namespace App { // interface Error {} // interface Locals {} - // interface PageData {} + // Server-rendered snapshots; null in the browser and in client-only builds + interface PageData { + shell?: ForumShell | null; + profiles?: Profiles | null; + thread?: ThreadDetail | null; + threads?: ThreadsSnapshot | null; + overview?: OverviewSnapshot | null; + } // interface PageState {} // interface Platform {} } diff --git a/src/lib/access.svelte.ts b/src/lib/access.svelte.ts new file mode 100644 index 0000000..1710cd1 --- /dev/null +++ b/src/lib/access.svelte.ts @@ -0,0 +1,109 @@ +import type { AbstractRelay } from "@nostr/tools/abstract-relay"; +import { auth } from "$lib/auth.svelte"; +import { ensureForumRelay, forumRelayInfo } from "$lib/relay"; +import type { ForumRelayInfo } from "$lib/relay"; + +// Whether the forum relay will serve this visitor at all. A relay can refuse +// every read (auth-required for anonymous visitors, restricted for logged-in +// non-members, blocked for bans), in which case the app would otherwise just +// look empty; the layout shows a gate page instead until a probe comes back +// open. +export type RelayAccess = + | "checking" + | "open" + | "auth-required" + | "restricted" + | "blocked"; + +let state = $state("checking"); +let message = $state(null); +let info = $state(null); +// Bumped when a probe completes, so listeners can react to every result even +// when the state itself didn't change (open → open after a login). +let probes = $state(0); + +export const relayAccess = { + get state() { + return state; + }, + get message() { + return message; + }, + get info() { + return info; + }, + get probes() { + return probes; + }, +}; + +// Sends a throwaway REQ and reads the CLOSED reason: nostr-tools' query helpers +// discard it, and it is the only cross-relay signal for a read gate. No `#h` +// in the filter, so a private group's per-group auth-required (handled by the +// room itself) can't masquerade as a relay-wide one. +function probeRead(relay: AbstractRelay): Promise { + return new Promise((resolve) => { + let settled = false; + const finish = (reason: string | null) => { + if (settled) return; + settled = true; + try { + sub.close(); + } catch {} + resolve(reason); + }; + const sub = relay.subscribe([{ kinds: [39000], limit: 1 }], { + onevent() {}, + oneose() { + finish(null); + }, + onclose(reason) { + finish(reason); + }, + }); + setTimeout(() => finish(null), 4000); + }); +} + +function classify(reason: string | null): RelayAccess { + const r = (reason ?? "").toLowerCase(); + if (r.startsWith("auth-required:")) return "auth-required"; + if (r.startsWith("restricted:")) return "restricted"; + if (r.startsWith("blocked:")) return "blocked"; + return "open"; +} + +let inFlight: Promise | null = null; + +// Re-evaluates access for the current signer. Concurrent calls share one probe. +// An anonymous visitor on a relay whose NIP-11 declares auth_required is gated +// without waiting for the relay to refuse a request. +export function probeRelayAccess(): Promise { + if (inFlight) return inFlight; + inFlight = (async () => { + try { + const relayInfo = await forumRelayInfo(); + info = relayInfo; + let next: RelayAccess; + let reason: string | null = null; + if (!auth.signer && relayInfo.authRequired) { + next = "auth-required"; + } else { + try { + reason = await probeRead(await ensureForumRelay()); + } catch { + reason = null; // connection failure: the pages surface their own errors + } + next = classify(reason); + } + state = next; + message = + next === "open" ? null : (reason?.replace(/^[a-z-]+:\s*/i, "") ?? null); + probes++; + return next; + } finally { + inFlight = null; + } + })(); + return inFlight; +} diff --git a/src/lib/actions/autogrow.ts b/src/lib/actions/autogrow.ts index b3db3c8..68d0864 100644 --- a/src/lib/actions/autogrow.ts +++ b/src/lib/actions/autogrow.ts @@ -24,14 +24,20 @@ export const autogrow: Action< const vPad = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom); const vBorder = parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth); + const min = lh * Math.max(node.rows, 1) + vPad + vBorder; const max = lh * maxRows + vPad + vBorder; node.style.height = "auto"; - const next = Math.min(node.scrollHeight, max); + // scrollHeight is 0 while hidden (display:none); never go below `rows`. + const next = Math.min(Math.max(node.scrollHeight, min), max); node.style.height = `${next}px`; node.style.overflowY = node.scrollHeight > max ? "auto" : "hidden"; } node.addEventListener("input", resize); + // Re-measure when the textarea becomes visible or changes width. + const ro = + typeof ResizeObserver !== "undefined" ? new ResizeObserver(resize) : null; + ro?.observe(node); resize(); return { @@ -43,6 +49,7 @@ export const autogrow: Action< }, destroy() { node.removeEventListener("input", resize); + ro?.disconnect(); }, }; }; diff --git a/src/lib/admins.svelte.ts b/src/lib/admins.svelte.ts index 7345f65..19c208f 100644 --- a/src/lib/admins.svelte.ts +++ b/src/lib/admins.svelte.ts @@ -1,18 +1,21 @@ +import { page } from "$app/state"; import { MODE } from "$lib/config"; import { groupStore } from "$lib/group.svelte"; import { queryForum } from "$lib/relay"; +import { fetchRoomAdmins } from "$lib/forum/groups"; // room id -> admin pubkeys (NIP-29 kind 39001 `p` tags), across all rooms. let byRoom = $state>({}); let loaded = $state(false); let loadedKey = ""; +// Until the live fetch lands, the server snapshot (if any) stands in. export const roomAdminsStore = { get byRoom() { - return byRoom; + return loaded ? byRoom : (page.data.shell?.adminsByRoom ?? {}); }, get loaded() { - return loaded; + return loaded || !!page.data.shell; }, }; @@ -21,11 +24,11 @@ export const roomAdminsStore = { export const adminPubkeys = { get list(): string[] { return MODE === "full" - ? [...new Set(Object.values(byRoom).flat())] + ? [...new Set(Object.values(roomAdminsStore.byRoom).flat())] : (groupStore.data?.admins ?? []); }, get loaded(): boolean { - return MODE === "full" ? loaded : groupStore.loaded; + return MODE === "full" ? roomAdminsStore.loaded : groupStore.loaded; }, }; @@ -37,7 +40,7 @@ export function isGroupAdmin( ): boolean { if (!pubkey || !groupId) return false; return MODE === "full" - ? (byRoom[groupId]?.includes(pubkey) ?? false) + ? (roomAdminsStore.byRoom[groupId]?.includes(pubkey) ?? false) : (groupStore.data?.admins?.includes(pubkey) ?? false); } @@ -48,17 +51,7 @@ export async function loadRoomAdmins(roomIds: string[]) { loadedKey = key; try { - const events = await queryForum({ - kinds: [39001], - "#d": roomIds, - }); - const map: Record = {}; - for (const e of events) { - const d = e.tags.find((t) => t[0] === "d")?.[1]; - if (!d) continue; - map[d] = e.tags.filter((t) => t[0] === "p" && t[1]).map((t) => t[1]); - } - byRoom = map; + byRoom = await fetchRoomAdmins(queryForum, roomIds); } finally { loaded = true; } diff --git a/src/lib/assets/favicon.svg b/src/lib/assets/favicon.svg deleted file mode 100644 index b1aac38..0000000 --- a/src/lib/assets/favicon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - Basspistol Icon - - - - - Basspistol Icon - - - - diff --git a/src/lib/auth.svelte.ts b/src/lib/auth.svelte.ts index 2fc1a23..0b96b3a 100644 --- a/src/lib/auth.svelte.ts +++ b/src/lib/auth.svelte.ts @@ -4,6 +4,14 @@ import type { EventTemplate, VerifiedEvent } from "@nostr/tools/core"; import * as nip19 from "@nostr/tools/nip19"; import { finalizeEvent, getPublicKey } from "@nostr/tools/pure"; import { resetJoinState, initJoinForUser } from "$lib/join.svelte"; +import { + connectBunker, + restoreBunker, + startNostrConnect, + type BunkerSession, + type NostrConnect, +} from "$lib/bunker"; +import type { BunkerSigner } from "@nostr/tools/nip46"; declare global { interface Window { @@ -18,6 +26,8 @@ export type Signer = { let user = $state(null); let signer = $state(null); +// Kept outside $state (see makeExtensionSigner) so it can be closed on logout +let bunker: BunkerSigner | null = null; let loginModalOpen = $state(false); // Optional action to run once login succeeds, so an intent like "post" started // while logged out resumes (login => join => composer) instead of being dropped. @@ -45,6 +55,8 @@ export const auth = { const PUBKEY_KEY = "nostr_pubkey"; const METHOD_KEY = "nostr_login_method"; const NSEC_KEY = "nostr_nsec"; +const BUNKER_KEY = "nostr_bunker"; +const BUNKER_CLIENT_KEY = "nostr_bunker_client_key"; export function openLogin(after?: () => void) { afterLogin = after ?? null; @@ -94,6 +106,30 @@ function makeExtensionSigner(): Signer { }; } +function makeBunkerSigner(remote: BunkerSigner, pubkey: string): Signer { + return { + async getPublicKey() { + return pubkey; + }, + async signEvent(template) { + return remote.signEvent(template); + }, + }; +} + +function closeBunker() { + bunker?.close().catch(() => {}); + bunker = null; +} + +function clearStoredSession() { + localStorage.removeItem(PUBKEY_KEY); + localStorage.removeItem(METHOD_KEY); + localStorage.removeItem(NSEC_KEY); + localStorage.removeItem(BUNKER_KEY); + localStorage.removeItem(BUNKER_CLIENT_KEY); +} + async function setUser(pubkey: string) { const { loadNostrUser } = await import("@nostr/gadgets/metadata"); user = await loadNostrUser(pubkey); @@ -105,10 +141,11 @@ export async function loginWithExtension() { throw new Error("No Nostr extension found"); } const pubkey = await window.nostr.getPublicKey(); + closeBunker(); signer = makeExtensionSigner(); + clearStoredSession(); localStorage.setItem(PUBKEY_KEY, pubkey); localStorage.setItem(METHOD_KEY, "extension"); - localStorage.removeItem(NSEC_KEY); await setUser(pubkey); sessionEpoch++; runAfterLogin(); @@ -137,7 +174,9 @@ export async function loginWithNsec(input: string) { const sk = parseSecretKey(input); const nsec = nip19.nsecEncode(sk); const pubkey = getPublicKey(sk); + closeBunker(); signer = makeNsecSigner(sk); + clearStoredSession(); localStorage.setItem(PUBKEY_KEY, pubkey); localStorage.setItem(METHOD_KEY, "nsec"); localStorage.setItem(NSEC_KEY, nsec); @@ -146,12 +185,47 @@ export async function loginWithNsec(input: string) { runAfterLogin(); } +async function loginWithBunkerSession(session: BunkerSession) { + closeBunker(); + bunker = session.signer; + signer = makeBunkerSigner(session.signer, session.pubkey); + clearStoredSession(); + localStorage.setItem(PUBKEY_KEY, session.pubkey); + localStorage.setItem(METHOD_KEY, "bunker"); + localStorage.setItem(BUNKER_KEY, session.bunkerUrl); + localStorage.setItem(BUNKER_CLIENT_KEY, session.clientSecretKey); + await setUser(session.pubkey); + sessionEpoch++; + runAfterLogin(); +} + +// Pasted bunker:// URL or a NIP-05 that advertises one +export async function loginWithBunker(input: string) { + await loginWithBunkerSession(await connectBunker(input)); +} + +// Client-initiated flow: show `uri` (QR/link) and await `done`. `cancel` stops +// waiting when the user leaves the view. +// `onAck` reports the signer's first reply so the UI can show progress while +// the session is finalized. +export function loginWithNostrConnect(onAck?: () => void): { + uri: string; + done: Promise; + cancel: () => void; +} { + const nc: NostrConnect = startNostrConnect(onAck); + return { + uri: nc.uri, + done: nc.session.then(loginWithBunkerSession), + cancel: nc.cancel, + }; +} + export function logout() { user = null; signer = null; - localStorage.removeItem(PUBKEY_KEY); - localStorage.removeItem(METHOD_KEY); - localStorage.removeItem(NSEC_KEY); + closeBunker(); + clearStoredSession(); resetJoinState(); sessionEpoch++; } @@ -171,6 +245,16 @@ export async function restoreSession() { } catch { return; } + } else if (method === "bunker") { + const url = localStorage.getItem(BUNKER_KEY); + const clientKey = localStorage.getItem(BUNKER_CLIENT_KEY); + if (!url || !clientKey) return; + try { + bunker = await restoreBunker(url, clientKey); + } catch { + return; + } + signer = makeBunkerSigner(bunker, pubkey); } else { signer = makeExtensionSigner(); } diff --git a/src/lib/bunker.ts b/src/lib/bunker.ts new file mode 100644 index 0000000..817602d --- /dev/null +++ b/src/lib/bunker.ts @@ -0,0 +1,216 @@ +import { + BunkerSigner, + createNostrConnectURI, + parseBunkerInput, + toBunkerURL, + type BunkerPointer, +} from "@nostr/tools/nip46"; +import { SimplePool } from "@nostr/tools/pool"; +import { NostrConnect as NostrConnectKind } from "@nostr/tools/kinds"; +import { decrypt, getConversationKey } from "@nostr/tools/nip44"; +import { generateSecretKey, getPublicKey } from "@nostr/tools/pure"; +import { bytesToHex, hexToBytes } from "@nostr/tools/utils"; +import { NOSTRCONNECT_RELAYS } from "$lib/config"; + +// NIP-46 remote signing. Two ways in: the user pastes a bunker:// URL (or a +// NIP-05 that advertises one), or the app shows a nostrconnect:// URI and the +// signer app calls back. Either way the result is a live BunkerSigner plus the +// data needed to reconnect silently next time: the bunker URL (secret +// stripped) and the client key the conversation is bound to. + +export type BunkerSession = { + signer: BunkerSigner; + pubkey: string; + bunkerUrl: string; + clientSecretKey: string; +}; + +// The signer app usually lives on the phone, and on mobile the browser tab is +// backgrounded while the user approves there, which can drop the socket. A +// reconnecting pool keeps the subscriptions alive across that. +const pool = new SimplePool({ enableReconnect: true }); + +// The approval happens in the signer app, so the tab is backgrounded and its +// sockets can drop; nostr-tools only retries after a 10s backoff. Reconnect +// the relays this flow has used as soon as the tab or the network is back, +// and keep a shorter backoff for drops in between. +const FAST_BACKOFF = [1000, 2000, 5000, 10000]; +const usedRelays = new Set(); +function touchRelays(urls: string[] = []) { + for (const url of urls) usedRelays.add(url); + for (const url of usedRelays) { + pool + .ensureRelay(url) + .then((relay) => { + relay.resubscribeBackoff = FAST_BACKOFF; + }) + .catch(() => {}); + } +} +if (typeof document !== "undefined") { + document.addEventListener("visibilitychange", () => { + if (!document.hidden) touchRelays(); + }); + window.addEventListener("online", () => touchRelays()); +} + +const signerParams = { + pool, + onauth: (url: string) => { + window.open(url, "_blank"); + }, +}; + +function stripSecret(url: string): string { + const u = new URL(url); + u.searchParams.delete("secret"); + return u.toString(); +} + +function describe(e: unknown): string { + return e instanceof Error ? e.message : String(e); +} + +export async function connectBunker(input: string): Promise { + const bp = await parseBunkerInput(input.trim()); + if (!bp) throw new Error("Invalid bunker"); + if (bp.relays.length === 0) throw new Error("Bunker URL has no relay"); + const sk = generateSecretKey(); + touchRelays(bp.relays); + const signer = BunkerSigner.fromBunker(sk, bp, signerParams); + try { + await signer.connect(); + const pubkey = await signer.getPublicKey(); + return { + signer, + pubkey, + bunkerUrl: stripSecret(toBunkerURL(bp)), + clientSecretKey: bytesToHex(sk), + }; + } catch (e) { + signer.close().catch(() => {}); + throw new Error(`Bunker error: ${describe(e)}`); + } +} + +export type NostrConnect = { + uri: string; + // Resolves once a signer answers the URI; rejects on abort or relay close + session: Promise; + cancel: () => void; +}; + +// Waits for the signer's reply to a nostrconnect:// URI. Unlike the library's +// helper, the filter has no `limit: 0`, so a reply published while the tab was +// offline is replayed once the relay reconnects. +function waitForConnect( + sk: Uint8Array, + secret: string, + signal: AbortSignal, +): Promise { + const clientPubkey = getPublicKey(sk); + touchRelays(NOSTRCONNECT_RELAYS); + return new Promise((resolve, reject) => { + let settled = false; + const finish = (fn: () => void) => { + if (settled) return; + settled = true; + sub.close(); + fn(); + }; + const sub = pool.subscribe( + NOSTRCONNECT_RELAYS, + { + kinds: [NostrConnectKind], + "#p": [clientPubkey], + since: Math.floor(Date.now() / 1000) - 60, + }, + { + onevent: (event) => { + let reply: { result?: string; error?: string }; + try { + const key = getConversationKey(sk, event.pubkey); + reply = JSON.parse(decrypt(event.content, key)); + } catch (e) { + console.warn("nostrconnect: undecryptable reply", e); + return; + } + if (reply.result === secret) { + finish(() => + resolve({ + pubkey: event.pubkey, + relays: NOSTRCONNECT_RELAYS, + secret, + }), + ); + } else if (reply.error) { + finish(() => reject(new Error(`Signer refused: ${reply.error}`))); + } else { + finish(() => + reject( + new Error( + `Signer replied "${reply.result}" instead of the secret`, + ), + ), + ); + } + }, + onclose: (reasons) => + finish(() => + reject(new Error(`Relay closed: ${reasons.join("; ")}`)), + ), + }, + ); + // Closes the subscription on every relay; the pool's own abort handling + // only reaches the last one + signal.addEventListener("abort", () => + finish(() => reject(new Error("Cancelled"))), + ); + }); +} + +// `onAck` fires the moment the signer answers the URI, before the follow-up +// round-trip that completes the session — the point where "waiting for the +// signer" becomes "finishing up". +export function startNostrConnect(onAck?: () => void): NostrConnect { + const sk = generateSecretKey(); + const secret = Math.random().toString(36).substring(2, 10); + const uri = createNostrConnectURI({ + clientPubkey: getPublicKey(sk), + relays: NOSTRCONNECT_RELAYS, + secret, + name: location.host, + url: location.origin, + }); + const abort = new AbortController(); + const session = waitForConnect(sk, secret, abort.signal).then(async (bp) => { + onAck?.(); + // The signer already accepted this client key; no connect round-trip + const signer = BunkerSigner.fromBunker(sk, bp, signerParams); + try { + const pubkey = await signer.getPublicKey(); + return { + signer, + pubkey, + bunkerUrl: stripSecret(toBunkerURL(bp)), + clientSecretKey: bytesToHex(sk), + }; + } catch (e) { + signer.close().catch(() => {}); + throw new Error(`Bunker error: ${describe(e)}`); + } + }); + return { uri, session, cancel: () => abort.abort() }; +} + +// Rebuilds the signer from stored data without a connect round-trip: the +// bunker already knows this client key, so the first sign request is enough. +export async function restoreBunker( + bunkerUrl: string, + clientSecretKey: string, +): Promise { + const bp = await parseBunkerInput(bunkerUrl); + if (!bp) throw new Error("Invalid bunker"); + touchRelays(bp.relays); + return BunkerSigner.fromBunker(hexToBytes(clientSecretKey), bp, signerParams); +} diff --git a/src/lib/chat.svelte.ts b/src/lib/chat.svelte.ts index 1517961..4056656 100644 --- a/src/lib/chat.svelte.ts +++ b/src/lib/chat.svelte.ts @@ -1,5 +1,5 @@ import type { Event } from "@nostr/tools"; -import { loadNostrUser, type NostrUser } from "@nostr/gadgets/metadata"; +import { loadNostrUser, type NostrUser } from "$lib/gadgets"; import { RELAY_URL } from "$lib/config"; import { auth } from "$lib/auth.svelte"; import { queryForum, publishForum, subscribeForum } from "$lib/relay"; diff --git a/src/lib/components/ChatContent.svelte b/src/lib/components/ChatContent.svelte index ee828ad..50c286b 100644 --- a/src/lib/components/ChatContent.svelte +++ b/src/lib/components/ChatContent.svelte @@ -1,6 +1,7 @@ - - {title} - +

{title}

- {#if !showPrivateGate && !checkingAccess} + {#if !showPrivateGate && !checkingAccess && !empty}
@@ -165,9 +185,27 @@

+
+ {:else if empty} +
+

+ This room is still empty +

+
{:else} @@ -194,6 +232,19 @@ {threadStore.loadingMore ? "Loading…" : "Show more"}
+ {:else if fairlyNew} +
+

+ This room is fairly new +

+ +
{:else if rows.length > 0}

- import { joinState, closeJoinModal, submitJoin } from "$lib/join.svelte"; + import { joinState, closeJoinModal, submitJoinCode } from "$lib/join.svelte"; import { tick } from "svelte"; let code = $state(""); let codeInput = $state(null); + // Pending approval hides the code field behind a discreet link, so a group + // that doesn't hand out codes doesn't send people hunting for one. + let codeRevealed = $state(false); + + const step = $derived(joinState.modalStep); + const showCode = $derived( + step === "relay-code" || + (step === "group-code" && joinState.modalCodeHinted) || + codeRevealed, + ); + const canOfferCode = $derived( + step === "pending" || (step === "group-code" && !joinState.modalCodeHinted), + ); $effect(() => { - if (joinState.modalOpen && joinState.needsCode) { + if (!joinState.modalOpen) { + code = ""; + codeRevealed = false; + } + }); + + $effect(() => { + if (joinState.modalOpen && showCode) { tick().then(() => codeInput?.focus()); } - if (!joinState.modalOpen) code = ""; }); async function onSubmit() { - await submitJoin(code.trim() || undefined); + await submitJoinCode(code.trim() || undefined); } function onClose() { @@ -56,15 +75,28 @@ id="join-title" class="mb-2 text-lg font-semibold text-neutral-900 dark:text-neutral-100" > - Join this group + {#if step === "relay-code"} + Invite code required + {:else if step === "pending"} + Request sent + {:else if step === "error"} + Could not join + {:else} + Join this group + {/if}

- {#if joinState.needsCode} - Join this group to participate. If you have an invite code, enter it - below — otherwise just send the request. + {#if step === "relay-code"} + This relay only accepts members. Enter an invite code to join. + {:else if step === "pending"} + Your request to join has been sent. An admin needs to approve it + before you can participate. + {:else if step === "error"} + The relay refused the request. + {:else if joinState.modalCodeHinted} + This group requires an invite code to join. {:else} - Join this group to participate. Your request is sent to the relay and, - where needed, approved by an admin. + The group refused the join request. {/if}

@@ -77,13 +109,17 @@ {/if} - {#if joinState.needsCode} - + {#if showCode} + e.key === "Enter" && onSubmit()} class="focus:ring-accent mb-3 w-full rounded border border-neutral-200 px-3 py-2 text-sm focus:ring-1 focus:outline-none disabled:opacity-50 dark:border-neutral-700" /> + {:else if canOfferCode} + {/if}
@@ -103,18 +147,16 @@ > Close - + {#if showCode} + + {/if}
diff --git a/src/lib/components/LoginModal.svelte b/src/lib/components/LoginModal.svelte index 01941a4..5547e21 100644 --- a/src/lib/components/LoginModal.svelte +++ b/src/lib/components/LoginModal.svelte @@ -2,25 +2,78 @@ import { auth, closeLogin, + loginWithBunker, loginWithExtension, + loginWithNostrConnect, loginWithNsec, } from "$lib/auth.svelte"; - import { tick } from "svelte"; + import { TITLE, MODE } from "$lib/config"; + import { groupStore } from "$lib/group.svelte"; + import { themeState } from "$lib/theme.svelte"; + import type { NstartModal } from "nstart-modal"; + import { onDestroy, tick } from "svelte"; + import { renderSVG } from "uqr"; - let view = $state<"extension" | "nsec">("extension"); + type View = "extension" | "bunker" | "nsec"; + + let view = $state("extension"); let nsec = $state(""); + let bunkerUrl = $state(""); let error = $state(null); let busy = $state(false); + let copied = $state(false); let nsecInput = $state(null); + let bunkerInput = $state(null); + + // Nstart sign-up wizard (an iframe overlay). Built lazily and reused; it is + // rebuilt only when the theme it was built for changes. + let wizard: NstartModal | null = null; + let wizardTheme: string | null = null; + // The login dialog hides while the wizard is up and comes back on cancel + let signingUp = $state(false); + + const appName = $derived( + TITLE || + (MODE === "simple" ? groupStore.data?.name : "") || + (typeof location !== "undefined" ? location.host : ""), + ); + + // Client-initiated NIP-46 flow, alive only while the bunker view is shown. + // `finalizing` flips when the signer first answers, so the QR gives way to + // a progress state while the session completes. + // $state.raw so identity checks against the raw object (connect === nc) + // hold; the object is replaced wholesale, never mutated. + let connect = $state.raw | null>( + null, + ); + let finalizing = $state(false); + const qrSvg = $derived( + connect ? renderSVG(connect.uri, { pixelSize: 4, border: 2 }) : "", + ); const hasExtension = $derived( typeof window !== "undefined" && !!window.nostr, ); + const titles: Record = { + extension: "Log in", + bunker: "Log in with a bunker", + nsec: "Log in with nsec", + }; + + function stopConnect() { + connect?.cancel(); + connect = null; + finalizing = false; + } + function reset() { + stopConnect(); view = "extension"; nsec = ""; + bunkerUrl = ""; error = null; + copied = false; } async function handleExtension() { @@ -53,33 +106,151 @@ } } + async function handleBunker() { + if (busy || !bunkerUrl.trim()) return; + error = null; + busy = true; + try { + await loginWithBunker(bunkerUrl); + closeLogin(); + reset(); + } catch (e) { + error = + e instanceof Error ? e.message : "Failed to connect to the bunker"; + } finally { + busy = false; + } + } + + function startConnect() { + stopConnect(); + const nc = loginWithNostrConnect(() => { + if (connect === nc) finalizing = true; + }); + connect = nc; + nc.done + .then(() => { + if (connect !== nc) return; + closeLogin(); + reset(); + }) + .catch((e) => { + if (connect !== nc) return; // cancelled by leaving the view + connect = null; + finalizing = false; + error = `${e instanceof Error ? e.message : "Connection failed"}. Go back and retry.`; + }); + } + + async function copyUri() { + if (!connect) return; + try { + await navigator.clipboard.writeText(connect.uri); + copied = true; + setTimeout(() => (copied = false), 2000); + } catch { + error = "Could not copy to the clipboard"; + } + } + function onClose() { if (busy) return; closeLogin(); reset(); } + // The accent color as Nstart wants it (hex, no #), so the wizard matches the + // forum; env overrides are already applied to the CSS variable. + function accentHex(): string | undefined { + const v = getComputedStyle(document.documentElement) + .getPropertyValue("--color-accent") + .trim(); + return /^#[0-9a-fA-F]{6}$/.test(v) ? v.slice(1) : undefined; + } + + async function handleSignup() { + if (busy) return; + error = null; + stopConnect(); + const { NstartModal } = await import("nstart-modal"); + if (!wizard || wizardTheme !== themeState.theme) { + wizard?.destroy(); + wizardTheme = themeState.theme; + wizard = new NstartModal({ + baseUrl: "https://nstart.me", + an: appName, + aa: accentHex(), + am: themeState.theme, + // Only bunker:// or nsec come back; ncryptsec has no login here + aac: true, + onComplete: ({ nostrLogin }) => void completeSignup(nostrLogin), + onCancel: () => { + signingUp = false; + }, + }); + } + signingUp = true; + wizard.open(); + } + + async function completeSignup(nostrLogin: string | null) { + signingUp = false; + if (!nostrLogin) { + error = "Sign-up finished without a login credential"; + return; + } + busy = true; + try { + if (nostrLogin.startsWith("bunker://")) { + await loginWithBunker(nostrLogin); + } else { + await loginWithNsec(nostrLogin); + } + closeLogin(); + reset(); + } catch (e) { + error = e instanceof Error ? e.message : "Failed to login"; + } finally { + busy = false; + } + } + async function showNsecView() { + stopConnect(); view = "nsec"; error = null; await tick(); nsecInput?.focus(); } + async function showBunkerView() { + view = "bunker"; + error = null; + startConnect(); + await tick(); + bunkerInput?.focus(); + } + function showExtensionView() { + stopConnect(); view = "extension"; error = null; } function onKeydown(e: KeyboardEvent) { - if (!auth.loginModalOpen) return; + if (!auth.loginModalOpen || signingUp) return; if (e.key === "Escape") onClose(); } + + onDestroy(() => { + stopConnect(); + wizard?.destroy(); + }); -{#if auth.loginModalOpen} +{#if auth.loginModalOpen && !signingUp}
+ +
diff --git a/src/lib/components/SearchInline.svelte b/src/lib/components/SearchInline.svelte index 40e0155..0f4635e 100644 --- a/src/lib/components/SearchInline.svelte +++ b/src/lib/components/SearchInline.svelte @@ -15,7 +15,7 @@ function select(r: SearchResult) { open = false; - goto(`/thread/${r.threadId}`); + goto(search.threadUrl(r.threadId)); } function onKeydown(e: KeyboardEvent) { diff --git a/src/lib/components/SearchModal.svelte b/src/lib/components/SearchModal.svelte index 9d07dbf..923d26b 100644 --- a/src/lib/components/SearchModal.svelte +++ b/src/lib/components/SearchModal.svelte @@ -21,13 +21,26 @@ function select(r: SearchResult) { closeSearch(); - goto(`/thread/${r.threadId}`); + goto(search.threadUrl(r.threadId)); } // Global "/" opens the search from any page, unless typing somewhere else function onWindowKeydown(e: KeyboardEvent) { if (searchModal.open) { - if (e.key === "Escape") closeSearch(); + if (e.key === "Escape") { + closeSearch(); + return; + } + // Typing fast right after "/" can outrun the async focus; route the + // keystroke into the input so no leading characters get lost + if ( + e.key.length === 1 && + !e.metaKey && + !e.ctrlKey && + !e.altKey && + document.activeElement !== inputEl + ) + inputEl?.focus(); return; } if (e.key !== "/" || e.metaKey || e.ctrlKey || e.altKey) return; diff --git a/src/lib/components/SearchResults.svelte b/src/lib/components/SearchResults.svelte index a8585c1..7361927 100644 --- a/src/lib/components/SearchResults.svelte +++ b/src/lib/components/SearchResults.svelte @@ -41,7 +41,7 @@ {#each search.results as r, i (r.threadId)} r.trim()) + .filter(Boolean); +// Server-side rendering: opt-in, decided at build time (vite.config.ts bakes +// PUBLIC_SSR in; svelte.config.js picks the matching adapter). +export const SSR_ENABLED = __SQUALK_SSR__; +// With SSR on, a client-side navigation also asks the server for the same +// snapshot (without waiting for it) so the page is already cached for the +// next refresh, shared link or crawler. Costs one server→relay query per +// navigation; opt out with PUBLIC_SSR_WARM=no. +export const SSR_WARM = SSR_ENABLED && env.PUBLIC_SSR_WARM !== "no"; +// Lifetime of server-rendered snapshots, in seconds. Within FRESH a cached +// page is served as is; up to STALE it is still served at once but refreshed +// in the background; beyond that it is fetched again before answering. Only +// crawlers and cold refreshes see the snapshot — the browser always refetches +// live data after hydration — so these trade first-paint staleness for speed. +function seconds(raw: string | undefined, fallback: number): number { + const n = Number(raw); + return Number.isFinite(n) && n >= 0 ? n : fallback; +} +export const SSR_CACHE_FRESH = seconds(env.PUBLIC_SSR_CACHE_FRESH, 300); +export const SSR_CACHE_STALE = seconds(env.PUBLIC_SSR_CACHE_STALE, 6 * 3600); +// The same policy for a shared HTTP cache in front (Cloudflare honours it once +// HTML caching is enabled). Browsers always revalidate (max-age=0) so a login +// shows its content at once. +export const CACHE_CONTROL = `public, max-age=0, s-maxage=${SSR_CACHE_FRESH}, stale-while-revalidate=${SSR_CACHE_STALE}`; // Requires a relay with NIP-50 support. export const SEARCH_ENABLED = env.PUBLIC_SEARCH === "yes"; export const LABELS = (env.PUBLIC_LABELS ?? "") @@ -31,3 +62,14 @@ export const BLOSSOM_URL = (env.PUBLIC_BLOSSOM_URL ?? "").replace(/\/$/, ""); // CSS variables defined in layout.css. Hover shades are derived in the layout. export const ACCENT_COLOR = env.PUBLIC_ACCENT_COLOR ?? ""; export const SECONDARY_COLOR = env.PUBLIC_SECONDARY_COLOR ?? ""; + +// Favicon: any URL (absolute, or root-relative for a file in static/). The +// default is a ring in the accent color (a circle with a transparent hole of +// half its radius), inlined as an SVG data URI. +const DEFAULT_ACCENT = "#e32a6d"; +export const FAVICON = + env.PUBLIC_FAVICON || + "data:image/svg+xml," + + encodeURIComponent( + ``, + ); diff --git a/src/lib/forum/groups.ts b/src/lib/forum/groups.ts new file mode 100644 index 0000000..846f50c --- /dev/null +++ b/src/lib/forum/groups.ts @@ -0,0 +1,81 @@ +import type { Event } from "@nostr/tools/core"; +import { tag, tags, type Query } from "./query"; + +export type GroupSummary = { + id: string; // NIP-29 group id (the `d` tag) — also the room URL slug + name: string; + picture?: string; + about?: string; + createdAt: number; + flags: string[]; // special NIP-29 markers present (private, hidden, closed, restricted) +}; + +export type GroupMetadata = { + name: string; + picture?: string; + about?: string; + isPrivate: boolean; + isClosed: boolean; + isRestricted: boolean; + isHidden: boolean; + admins: string[]; +}; + +const SPECIAL_FLAGS = ["private", "hidden", "closed", "restricted"]; + +function parseSummary(e: Event): GroupSummary { + const id = tag(e, "d") ?? ""; + return { + id, + name: tag(e, "name") ?? id, + picture: tag(e, "picture"), + about: tag(e, "about"), + createdAt: e.created_at, + flags: SPECIAL_FLAGS.filter((f) => e.tags.some((t) => t[0] === f)), + }; +} + +// Every group the relay serves this visitor (kind 39000), oldest first. +export async function fetchGroups(q: Query): Promise { + const events = await q({ kinds: [39000] }); + return events + .map(parseSummary) + .filter((g) => g.id) + .sort((a, b) => a.createdAt - b.createdAt); +} + +// One group's metadata plus its admins (kinds 39000 + 39001). +export async function fetchGroup( + q: Query, + id: string, +): Promise { + const events = await q({ kinds: [39000, 39001], "#d": [id] }); + const event = events.find((e) => e.kind === 39000); + if (!event) return null; + const adminsEvent = events.find((e) => e.kind === 39001); + return { + name: tag(event, "name") ?? id, + picture: tag(event, "picture"), + about: tag(event, "about"), + isPrivate: event.tags.some((t) => t[0] === "private"), + isClosed: event.tags.some((t) => t[0] === "closed"), + isRestricted: event.tags.some((t) => t[0] === "restricted"), + isHidden: event.tags.some((t) => t[0] === "hidden"), + admins: adminsEvent ? tags(adminsEvent, "p") : [], + }; +} + +// room id -> admin pubkeys (kind 39001 `p` tags) for the given rooms. +export async function fetchRoomAdmins( + q: Query, + roomIds: string[], +): Promise> { + if (roomIds.length === 0) return {}; + const events = await q({ kinds: [39001], "#d": roomIds }); + const map: Record = {}; + for (const e of events) { + const d = tag(e, "d"); + if (d) map[d] = tags(e, "p"); + } + return map; +} diff --git a/src/lib/forum/overview.ts b/src/lib/forum/overview.ts new file mode 100644 index 0000000..ad750b0 --- /dev/null +++ b/src/lib/forum/overview.ts @@ -0,0 +1,73 @@ +import { tag, type Query } from "./query"; + +export type RoomActivity = { + latestAt: number; + latestPubkey: string; +}; + +export type RecentThread = { + id: string; + title: string; + groupId: string; + authorPubkey: string; + createdAt: number; +}; + +export type Overview = { + activity: Record; + admins: Record; // room id -> first admin pubkey + recent: RecentThread[]; +}; + +const RECENT_LIMIT = 20; + +// Landing data across rooms: each room's last activity and first admin (for +// the room cards) and the most recent threads overall (for the side panel). +// The group relay truncates multi-value "#h" filters, so rooms are queried +// one by one and merged. +export async function fetchOverview( + q: Query, + roomIds: string[], +): Promise { + const latest = await Promise.all( + roomIds.map((id) => q({ kinds: [11, 1111], "#h": [id], limit: 1 })), + ); + const activity: Record = {}; + roomIds.forEach((id, i) => { + const e = latest[i][0]; + if (e) activity[id] = { latestAt: e.created_at, latestPubkey: e.pubkey }; + }); + + const adminEvents = await Promise.all( + roomIds.map((id) => q({ kinds: [39001], "#d": [id] })), + ); + const admins: Record = {}; + roomIds.forEach((id, i) => { + const pk = adminEvents[i][0] ? tag(adminEvents[i][0], "p") : undefined; + if (pk) admins[id] = pk; + }); + + const perRoom = await Promise.all( + roomIds.map((id) => q({ kinds: [11], "#h": [id], limit: RECENT_LIMIT })), + ); + const threads = perRoom.flat().sort((a, b) => b.created_at - a.created_at); + const recent = threads.slice(0, RECENT_LIMIT).map((e) => ({ + id: e.id, + title: tag(e, "title") ?? "(untitled)", + groupId: tag(e, "h") ?? "", + authorPubkey: e.pubkey, + createdAt: e.created_at, + })); + + return { activity, admins, recent }; +} + +export function overviewPubkeys(o: Overview): string[] { + return [ + ...new Set([ + ...Object.values(o.activity).map((a) => a.latestPubkey), + ...Object.values(o.admins), + ...o.recent.map((t) => t.authorPubkey), + ]), + ]; +} diff --git a/src/lib/forum/partials.ts b/src/lib/forum/partials.ts new file mode 100644 index 0000000..60c37ab --- /dev/null +++ b/src/lib/forum/partials.ts @@ -0,0 +1,48 @@ +import { tag, type Query } from "./query"; + +export type PartialSlot = "home" | "contacts"; +const SLOTS = new Set(["home", "contacts"]); + +export type Partial = { + id: string; + slot: PartialSlot; + title: string; + content: string; // markdown body + pubkey: string; + createdAt: number; +}; + +// Every `squalk-partial` event for a known slot. Not admin-filtered nor +// reduced to the newest per slot: both depend on the admin set. +export async function fetchPartials(q: Query): Promise { + const events = await q({ kinds: [30023], "#t": ["squalk-partial"] }); + const out: Partial[] = []; + for (const e of events) { + const slot = tag(e, "d"); + if (!slot || !SLOTS.has(slot)) continue; + out.push({ + id: e.id, + slot: slot as PartialSlot, + title: tag(e, "title") ?? slot, + content: e.content, + pubkey: e.pubkey, + createdAt: e.created_at, + }); + } + return out; +} + +// The newest admin-authored partial for a slot. +export function pickPartial( + all: Partial[], + slot: PartialSlot, + admins: string[], +): Partial | undefined { + const set = new Set(admins); + let best: Partial | undefined; + for (const p of all) { + if (p.slot !== slot || !set.has(p.pubkey)) continue; + if (!best || p.createdAt > best.createdAt) best = p; + } + return best; +} diff --git a/src/lib/forum/profiles.ts b/src/lib/forum/profiles.ts new file mode 100644 index 0000000..a2edab5 --- /dev/null +++ b/src/lib/forum/profiles.ts @@ -0,0 +1,75 @@ +import type { Event } from "@nostr/tools/core"; +import * as nip19 from "@nostr/tools/nip19"; +import type { NostrUser } from "$lib/gadgets"; +import type { Query } from "./query"; + +// Public relays with broad kind-0 coverage. +export const PROFILE_RELAYS = [ + "wss://purplepag.es", + "wss://relay.nostr.band", + "wss://relay.damus.io", + "wss://nos.lol", +]; + +// Same shape @nostr/gadgets produces, built here from raw kind-0 events so the +// server can resolve authors without that (browser-only) library. +export function bareUser(pubkey: string): NostrUser { + const npub = nip19.npubEncode(pubkey); + return { + pubkey, + npub, + shortName: npub.substring(0, 8) + "…" + npub.substring(59), + metadata: {}, + lastUpdated: 0, + }; +} + +export function userFromEvent(e: Event): NostrUser { + const u = bareUser(e.pubkey); + let md: NostrUser["metadata"] = {}; + try { + md = JSON.parse(e.content) ?? {}; + } catch { + // Malformed metadata: keep the bare user + } + u.metadata = md; + u.shortName = + md.name || md.display_name || md.nip05?.split("@")?.[0] || u.shortName; + u.lastUpdated = e.created_at; + if (md.picture) u.image = md.picture; + return u; +} + +// Newest kind 0 per pubkey; pubkeys with no metadata are left out. +export async function fetchProfiles( + q: Query, + pubkeys: string[], +): Promise> { + const out: Record = {}; + if (pubkeys.length === 0) return out; + const events = await q({ kinds: [0], authors: pubkeys }); + for (const e of events) { + const prev = out[e.pubkey]; + if (prev && prev.lastUpdated >= e.created_at) continue; + out[e.pubkey] = userFromEvent(e); + } + return out; +} + +// In-app profile page, always addressed by npub. +export function profilePath(pubkey: string): string { + return `/profile/${nip19.npubEncode(pubkey)}`; +} + +// The pubkey behind a profile URL segment: npub, nprofile or bare hex. +export function decodeProfileId(id: string): string | null { + if (/^[0-9a-f]{64}$/.test(id)) return id; + try { + const decoded = nip19.decode(id); + if (decoded.type === "npub") return decoded.data; + if (decoded.type === "nprofile") return decoded.data.pubkey; + } catch { + // Not a bech32 entity + } + return null; +} diff --git a/src/lib/forum/query.ts b/src/lib/forum/query.ts new file mode 100644 index 0000000..911715e --- /dev/null +++ b/src/lib/forum/query.ts @@ -0,0 +1,36 @@ +import type { AbstractRelay } from "@nostr/tools/abstract-relay"; +import type { Event } from "@nostr/tools/core"; +import type { Filter } from "@nostr/tools/filter"; + +// A relay read. The fetchers in this folder only depend on this signature, so +// the same code runs behind the browser's authenticated connection and the +// server's anonymous one. +export type Query = (filter: Filter) => Promise; + +// Drive a subscription on an already-open relay and resolve at EOSE. +export function relayQuery(relay: AbstractRelay): Query { + return (filter) => + new Promise((resolve) => { + const events: Event[] = []; + const sub = relay.subscribe([filter], { + onevent(e) { + events.push(e); + }, + oneose() { + sub.close(); + resolve(events); + }, + onclose() { + resolve(events); + }, + }); + }); +} + +export function tag(e: Event, name: string): string | undefined { + return e.tags.find((t) => t[0] === name)?.[1]; +} + +export function tags(e: Event, name: string): string[] { + return e.tags.filter((t) => t[0] === name && t[1]).map((t) => t[1]); +} diff --git a/src/lib/forum/resources.ts b/src/lib/forum/resources.ts new file mode 100644 index 0000000..e6c0181 --- /dev/null +++ b/src/lib/forum/resources.ts @@ -0,0 +1,47 @@ +import { tag, type Query } from "./query"; + +export type Resource = { + id: string; + slug: string; // NIP-23 `d` tag — also the resource URL slug + title: string; + content: string; // markdown body + position?: number; + pubkey: string; + createdAt: number; +}; + +// Positioned resources first (ascending), then alphabetical by title. +function compare(a: Resource, b: Resource): number { + const ap = a.position; + const bp = b.position; + if (ap !== undefined && bp !== undefined) + return ap !== bp ? ap - bp : a.title.localeCompare(b.title); + if (ap !== undefined) return -1; + if (bp !== undefined) return 1; + return a.title.localeCompare(b.title); +} + +// Every `squalk-resource` article, newest per slug. Not admin-filtered: the +// caller applies the admin set, which may resolve later than this fetch. +export async function fetchResources(q: Query): Promise { + const events = await q({ kinds: [30023], "#t": ["squalk-resource"] }); + const bySlug = new Map(); + for (const e of events) { + const slug = tag(e, "d"); + if (!slug) continue; + const existing = bySlug.get(slug); + if (existing && existing.createdAt >= e.created_at) continue; + const posTag = tag(e, "position"); + const pos = posTag !== undefined ? Number(posTag) : NaN; + bySlug.set(slug, { + id: e.id, + slug, + title: tag(e, "title") ?? slug, + content: e.content, + position: Number.isFinite(pos) ? pos : undefined, + pubkey: e.pubkey, + createdAt: e.created_at, + }); + } + return [...bySlug.values()].sort(compare); +} diff --git a/src/lib/forum/snapshot.ts b/src/lib/forum/snapshot.ts new file mode 100644 index 0000000..46f7584 --- /dev/null +++ b/src/lib/forum/snapshot.ts @@ -0,0 +1,74 @@ +import { browser } from "$app/environment"; +import { error } from "@sveltejs/kit"; +import { SSR_WARM } from "$lib/config"; +import type { NostrUser } from "$lib/gadgets"; +import type { GroupMetadata, GroupSummary } from "./groups"; +import type { Resource } from "./resources"; +import type { Partial } from "./partials"; +import type { SortMode, ThreadPage } from "./threads"; +import type { Overview } from "./overview"; + +// What the server rendered a page with. Stores treat these as the baseline +// (see the `page.data` fallbacks) until their own live fetch lands. +export type ForumShell = { + group: GroupMetadata | null; // simple mode + groups: GroupSummary[]; // full mode + adminsByRoom: Record; // full mode + admins: string[]; // the trust gate for resources and partials + resources: Resource[]; + partials: Partial[]; +}; + +export type Profiles = Record; + +export type ThreadsSnapshot = ThreadPage & { + groupId: string; + sort: SortMode; + snapshotAt: number; + profiles: Profiles; +}; + +export type OverviewSnapshot = Overview & { + roomIds: string[]; + profiles: Profiles; +}; + +type Fetch = typeof fetch; + +// Read a snapshot endpoint. On the server the response is inlined into the +// HTML; in the browser it is consumed only while hydrating that HTML. Client +// navigations get null, leaving the stores to fetch live (and authenticated) +// data as they always did — the same path a client-only build takes — but +// may still poke the endpoint so the server has the page cached. +// A 404 from the endpoint becomes a real 404 page on the server, so crawlers +// never index an empty shell; the client keeps its own not-found handling. +export async function snapshot( + fetch: Fetch, + url: string, +): Promise { + if (browser && !hasInlinedResponse(url)) { + if (SSR_WARM) fetch(url).catch(() => {}); + return null; + } + let res: Response; + try { + res = await fetch(url); + } catch { + return null; + } + if (res.status === 404 && !browser) error(404, "Not found"); + if (!res.ok) return null; + try { + return (await res.json()) as T; + } catch { + return null; + } +} + +// SvelteKit inlines each SSR fetch as a script tag keyed by URL and removes +// it once replayed, so its presence marks the hydration pass. +function hasInlinedResponse(url: string): boolean { + return !!document.querySelector( + `script[data-sveltekit-fetched][data-url=${JSON.stringify(url)}]`, + ); +} diff --git a/src/lib/forum/thread.ts b/src/lib/forum/thread.ts new file mode 100644 index 0000000..3ffd263 --- /dev/null +++ b/src/lib/forum/thread.ts @@ -0,0 +1,56 @@ +import { tag, tags, type Query } from "./query"; + +export type PostData = { + id: string; + pubkey: string; + createdAt: number; + content: string; +}; + +export type ThreadDetail = { + id: string; + title: string; + labels: string[]; + groupId: string; // the thread's NIP-29 group (its `h` tag) + op: PostData; + replies: PostData[]; +}; + +// A thread (kind 11) with its flat replies (kind 1111), oldest first. Null +// when the relay returns nothing: no such thread, or one the visitor can't +// read. `fallbackGroup` covers OPs missing an `h` tag. +export async function fetchThread( + q: Query, + id: string, + fallbackGroup = "", +): Promise { + const [threadEvents, replyEvents] = await Promise.all([ + q({ kinds: [11], ids: [id] }), + q({ kinds: [1111], "#E": [id] }), + ]); + const event = threadEvents[0]; + if (!event) return null; + const replies = replyEvents.sort((a, b) => a.created_at - b.created_at); + return { + id: event.id, + title: tag(event, "title") ?? "(untitled)", + labels: tags(event, "t"), + groupId: tag(event, "h") ?? fallbackGroup, + op: { + id: event.id, + pubkey: event.pubkey, + createdAt: event.created_at, + content: event.content, + }, + replies: replies.map((r) => ({ + id: r.id, + pubkey: r.pubkey, + createdAt: r.created_at, + content: r.content, + })), + }; +} + +export function threadAuthors(t: ThreadDetail): string[] { + return [...new Set([t.op.pubkey, ...t.replies.map((r) => r.pubkey)])]; +} diff --git a/src/lib/forum/threads.ts b/src/lib/forum/threads.ts new file mode 100644 index 0000000..c80bb86 --- /dev/null +++ b/src/lib/forum/threads.ts @@ -0,0 +1,238 @@ +import type { Event } from "@nostr/tools/core"; +import { tag, tags, type Query } from "./query"; + +export const PAGE_SIZE = 30; +const WALK_LIMIT = 150; // Events per walk query (~5x PAGE_SIZE) +const WALK_MAX_ITERS = 12; + +export type SortMode = "active" | "new"; + +export type ThreadData = { + id: string; + title: string; + labels: string[]; + authorPubkey: string; + createdAt: number; + replyCount: number; + latestAt: number; + latestPubkey: string; + replierPubkeys: string[]; // unique reply authors, excl. OP, max 4 +}; + +export type ThreadPage = { + threads: ThreadData[]; + nextCursor: number | null; // sort-key value of the last thread + done: boolean; +}; + +export type PageRequest = { + sort: SortMode; + until: number; // upper bound on the sort key (inclusive) + snapshotAt: number; // upper time bound for reply counts, frozen per listing + exclude?: Set; // thread ids already shown + n?: number; +}; + +type SliceItem = { + id: string; + latestAt: number; + latestPubkey: string; + op?: Event; +}; + +type Slice = { items: SliceItem[]; nextCursor: number | null; done: boolean }; + +function threadIdOf(e: Event): string | undefined { + return e.kind === 11 ? e.id : tag(e, "E"); +} + +// Walk the combined OP + reply stream newest-first, collecting up to `n` unique +// threads not already shown. The first event seen for a thread defines its +// activity timestamp. +async function fetchActivitySlice( + q: Query, + groupId: string, + until: number, + exclude: Set, + n: number, +): Promise { + const collected = new Map(); + let cur = until; + let done = false; + + for (let i = 0; i < WALK_MAX_ITERS && collected.size < n; i++) { + const events = await q({ + kinds: [11, 1111], + "#h": [groupId], + until: cur, + limit: WALK_LIMIT, + }); + if (events.length === 0) { + done = true; + break; + } + + events.sort((a, b) => b.created_at - a.created_at); + let oldest = cur; + for (const e of events) { + oldest = Math.min(oldest, e.created_at); + const id = threadIdOf(e); + if (!id || exclude.has(id)) continue; + const seen = collected.get(id); + if (seen) { + // Grab the OP from the stream when a thread was first seen via a reply, + // so we avoid the by-id backfill the group relay truncates + if (!seen.op && e.kind === 11) seen.op = e; + continue; + } + if (collected.size >= n) continue; // Page full; keep scanning for OPs + collected.set(id, { + id, + latestAt: e.created_at, + latestPubkey: e.pubkey, + op: e.kind === 11 ? e : undefined, + }); + } + + if (events.length < WALK_LIMIT) { + done = true; + break; + } + if (oldest >= cur) break; // No progress (single timestamp floods the window) + cur = oldest; // Inclusive; thread-level dedupe absorbs re-reads + } + + const items = [...collected.values()].sort((a, b) => b.latestAt - a.latestAt); + const nextCursor = items.length > 0 ? items[items.length - 1].latestAt : null; + return { items, nextCursor, done }; +} + +// Chronological-by-creation slice: just OPs ordered by created_at. No reply +// data is needed to order them (stable cursor), keeping the path cheap; reply +// counts are still attached later via enrichment. +async function fetchNewSlice( + q: Query, + groupId: string, + until: number, + exclude: Set, + n: number, +): Promise { + const ops = await q({ + kinds: [11], + "#h": [groupId], + until, + limit: n + 10, // headroom for boundary OPs re-read at the inclusive cursor + }); + ops.sort((a, b) => b.created_at - a.created_at); + const fresh = ops.filter((e) => !exclude.has(e.id)); + const slice = fresh.slice(0, n); + const items: SliceItem[] = slice.map((op) => ({ + id: op.id, + latestAt: op.created_at, + latestPubkey: op.pubkey, + op, + })); + const done = ops.length < n + 10; + const last = slice[slice.length - 1] ?? ops[ops.length - 1]; + const nextCursor = last ? last.created_at : null; + return { items, nextCursor, done }; +} + +async function enrichWithReplies( + q: Query, + groupId: string, + items: SliceItem[], + snapshotAt: number, +): Promise> { + const result = new Map(); + if (items.length === 0) return result; + const replies = await q({ + kinds: [1111], + "#h": [groupId], + "#E": items.map((it) => it.id), + until: snapshotAt, + limit: 5000, + }); + const acc = new Map }>(); + for (const r of replies) { + const root = tag(r, "E"); + if (!root) continue; + let a = acc.get(root); + if (!a) { + a = { count: 0, pubkeys: new Set() }; + acc.set(root, a); + } + a.count++; + a.pubkeys.add(r.pubkey); + } + for (const it of items) { + const a = acc.get(it.id); + const repliers = a + ? [...a.pubkeys].filter((p) => p !== it.op?.pubkey).slice(0, 4) + : []; + result.set(it.id, { count: a?.count ?? 0, repliers }); + } + return result; +} + +async function buildThreads( + q: Query, + groupId: string, + items: SliceItem[], + snapshotAt: number, +): Promise { + const missing = items.filter((it) => !it.op); + if (missing.length > 0) { + const fetched = await Promise.all( + missing.map((it) => q({ kinds: [11], "#h": [groupId], ids: [it.id] })), + ); + const byId = new Map(); + for (const evs of fetched) for (const e of evs) byId.set(e.id, e); + for (const it of items) if (!it.op) it.op = byId.get(it.id); + } + const enriched = await enrichWithReplies(q, groupId, items, snapshotAt); + const out: ThreadData[] = []; + for (const it of items) { + const op = it.op; + if (!op) continue; // OP missing (deleted/unavailable) — drop the row + const e = enriched.get(it.id); + out.push({ + id: it.id, + title: tag(op, "title") ?? "(untitled)", + labels: tags(op, "t"), + authorPubkey: op.pubkey, + createdAt: op.created_at, + replyCount: e?.count ?? 0, + latestAt: it.latestAt, + latestPubkey: it.latestPubkey, + replierPubkeys: e?.repliers ?? [], + }); + } + return out; +} + +// One page of a group's thread listing, in the given sort order. +export async function fetchThreadPage( + q: Query, + groupId: string, + req: PageRequest, +): Promise { + const exclude = req.exclude ?? new Set(); + const n = req.n ?? PAGE_SIZE; + const slice = + req.sort === "new" + ? await fetchNewSlice(q, groupId, req.until, exclude, n) + : await fetchActivitySlice(q, groupId, req.until, exclude, n); + const threads = await buildThreads(q, groupId, slice.items, req.snapshotAt); + return { threads, nextCursor: slice.nextCursor, done: slice.done }; +} + +export function threadPagePubkeys(threads: ThreadData[]): string[] { + const set = new Set(); + for (const t of threads) { + set.add(t.authorPubkey); + set.add(t.latestPubkey); + for (const p of t.replierPubkeys) set.add(p); + } + return [...set]; +} diff --git a/src/lib/forum/userPosts.ts b/src/lib/forum/userPosts.ts new file mode 100644 index 0000000..bce4392 --- /dev/null +++ b/src/lib/forum/userPosts.ts @@ -0,0 +1,106 @@ +import type { Event } from "@nostr/tools/core"; +import { tag, type Query } from "./query"; + +export type UserPost = { + id: string; + threadId: string; // the root thread; equals `id` for a root post + title: string; // thread title, empty when the root is unreachable + groupId: string; + createdAt: number; + content: string; +}; + +export type UserPostKind = "roots" | "replies"; + +export type UserPostPage = { + items: UserPost[]; + done: boolean; // no older posts left +}; + +export type UserPosts = Record; + +// Posts per "Show more" step +export const USER_POSTS_PAGE = 20; +const CURSOR_HEADROOM = 10; // boundary events re-read at the inclusive cursor + +export type UserPostPageRequest = { + groupId?: string; // scope to one group (simple mode) + until?: number; // oldest loaded timestamp, inclusive + exclude?: Set; // ids already shown + n?: number; +}; + +function newestFirst(events: Event[]): Event[] { + return [...events].sort((a, b) => b.created_at - a.created_at); +} + +// One page of a user's root posts (kind 11) or replies (kind 1111), newest +// first, older than the cursor. Without `groupId` every group the relay +// serves the caller counts. +export async function fetchUserPostPage( + q: Query, + pubkey: string, + kind: UserPostKind, + req: UserPostPageRequest = {}, +): Promise { + const n = req.n ?? USER_POSTS_PAGE; + const exclude = req.exclude ?? new Set(); + const events = await q({ + kinds: [kind === "roots" ? 11 : 1111], + authors: [pubkey], + ...(req.groupId ? { "#h": [req.groupId] } : {}), + ...(req.until ? { until: req.until } : {}), + limit: n + CURSOR_HEADROOM, + }); + const fresh = newestFirst(events).filter( + (e) => !exclude.has(e.id) && (kind === "roots" || tag(e, "E")), + ); + const slice = fresh.slice(0, n); + // Finished only when everything fresh fits the page and the relay had + // nothing beyond the requested window + const done = fresh.length <= n && events.length < n + CURSOR_HEADROOM; + + // Replies take their title from the root thread + const titles = new Map(); + if (kind === "replies" && slice.length > 0) { + const rootIds = [...new Set(slice.map((e) => tag(e, "E")!))]; + const roots = await q({ kinds: [11], ids: rootIds }); + for (const e of roots) titles.set(e.id, tag(e, "title") ?? "(untitled)"); + } + + const items = slice.map((e) => { + const threadId = kind === "roots" ? e.id : tag(e, "E")!; + return { + id: e.id, + threadId, + title: + kind === "roots" + ? (tag(e, "title") ?? "(untitled)") + : (titles.get(threadId) ?? ""), + groupId: tag(e, "h") ?? req.groupId ?? "", + createdAt: e.created_at, + content: e.content, + }; + }); + return { items, done }; +} + +// The first page of both columns. +export async function fetchUserPosts( + q: Query, + pubkey: string, + groupId?: string, +): Promise { + const [roots, replies] = await Promise.all([ + fetchUserPostPage(q, pubkey, "roots", { groupId }), + fetchUserPostPage(q, pubkey, "replies", { groupId }), + ]); + return { roots, replies }; +} + +// Cursor for the page after `page`: the oldest timestamp shown, inclusive +// (the ids already shown are excluded on the next read). +export function nextCursor(page: UserPostPage): number | undefined { + const last = page.items[page.items.length - 1]; + return last?.createdAt; +} diff --git a/src/lib/gadgets.ts b/src/lib/gadgets.ts new file mode 100644 index 0000000..a80165c --- /dev/null +++ b/src/lib/gadgets.ts @@ -0,0 +1,17 @@ +import type { NostrUser } from "@nostr/gadgets/metadata"; +import type { ListFetcher, RelayItem } from "@nostr/gadgets/lists"; + +export type { NostrUser, RelayItem }; + +// @nostr/gadgets reads localStorage when imported, so it can't be loaded on +// the server. Every consumer goes through these lazy wrappers; the library is +// pulled in on first use, in the browser only. +export async function loadNostrUser(pubkey: string): Promise { + const m = await import("@nostr/gadgets/metadata"); + return m.loadNostrUser(pubkey); +} + +export const loadRelayList: ListFetcher = async (...args) => { + const m = await import("@nostr/gadgets/lists"); + return m.loadRelayList(...args); +}; diff --git a/src/lib/group.svelte.ts b/src/lib/group.svelte.ts index 87b77c0..4ac11dd 100644 --- a/src/lib/group.svelte.ts +++ b/src/lib/group.svelte.ts @@ -1,20 +1,11 @@ +import { page } from "$app/state"; import { GROUP_ID, MODE } from "$lib/config"; import { queryForum } from "$lib/relay"; import { groupsStore } from "$lib/groups.svelte"; +import { fetchGroup, type GroupMetadata } from "$lib/forum/groups"; -export type GroupMetadata = { - name: string; - picture?: string; - about?: string; - isPrivate: boolean; - isClosed: boolean; - isRestricted: boolean; - isHidden: boolean; - admins: string[]; -}; +export type { GroupMetadata }; -// NIP-29 access flags: private = members-only read, restricted = members-only -// write, closed = join requests ignored, hidden = metadata hidden from non-members. export type GroupFlags = { isPrivate: boolean; isClosed: boolean; @@ -25,44 +16,24 @@ export type GroupFlags = { let group = $state(null); let loaded = $state(false); +// Until the live fetch lands, the server snapshot (if any) stands in. export const groupStore = { get data() { - return group; + return group ?? page.data.shell?.group ?? null; }, get loaded() { - return loaded; + return loaded || !!page.data.shell; }, }; export async function loadGroup() { try { - const events = await queryForum({ - kinds: [39000, 39001], - "#d": [GROUP_ID], - }); - const event = events.find((e) => e.kind === 39000); - if (!event) return; - // Admins live in the NIP-29 kind 39001 event as `p` tags. - const adminsEvent = events.find((e) => e.kind === 39001); - const admins = - adminsEvent?.tags.filter((t) => t[0] === "p").map((t) => t[1]) ?? []; - group = { - name: event.tags.find((t) => t[0] === "name")?.[1] ?? GROUP_ID, - picture: event.tags.find((t) => t[0] === "picture")?.[1], - about: event.tags.find((t) => t[0] === "about")?.[1], - isPrivate: event.tags.some((t) => t[0] === "private"), - isClosed: event.tags.some((t) => t[0] === "closed"), - isRestricted: event.tags.some((t) => t[0] === "restricted"), - isHidden: event.tags.some((t) => t[0] === "hidden"), - admins, - }; + group = (await fetchGroup(queryForum, GROUP_ID)) ?? group; } finally { loaded = true; } } -// A group's access flags from whichever store holds them: full mode keeps every -// room in the groups list, simple mode has the single active group's metadata. export function getGroupFlags(groupId: string): GroupFlags | null { if (MODE === "full") { const g = groupsStore.list.find((x) => x.id === groupId); @@ -82,3 +53,12 @@ export function getGroupFlags(groupId: string): GroupFlags | null { isHidden: group.isHidden, }; } + +// Display name for a group from whichever store holds it, falling back to the +// id when metadata hasn't loaded. +export function getGroupName(groupId: string): string { + if (MODE === "full") { + return groupsStore.list.find((x) => x.id === groupId)?.name ?? groupId; + } + return group?.name ?? groupId; +} diff --git a/src/lib/groups.svelte.ts b/src/lib/groups.svelte.ts index c881b24..976259b 100644 --- a/src/lib/groups.svelte.ts +++ b/src/lib/groups.svelte.ts @@ -1,49 +1,25 @@ +import { page } from "$app/state"; import { queryForum } from "$lib/relay"; +import { fetchGroups, type GroupSummary } from "$lib/forum/groups"; -export type GroupSummary = { - id: string; // NIP-29 group id (the `d` tag) — also the room URL slug - name: string; - picture?: string; - about?: string; - createdAt: number; - flags: string[]; // special NIP-29 markers present (private, hidden, closed, restricted) -}; - -// NIP-29 metadata markers we surface as room tags, in display order. -const SPECIAL_FLAGS = ["private", "hidden", "closed", "restricted"]; +export type { GroupSummary }; let list = $state([]); let loaded = $state(false); +// Until the live fetch lands, the server snapshot (if any) stands in. export const groupsStore = { get list() { - return list; + return loaded ? list : (page.data.shell?.groups ?? []); }, get loaded() { - return loaded; + return loaded || !!page.data.shell; }, }; -// Fetch every group the relay hosts. NIP-29 publishes one kind 39000 metadata -// event per group, so an unfiltered query enumerates them all. export async function loadGroups() { try { - const events = await queryForum({ kinds: [39000] }); - list = events - .map((e) => { - const id = e.tags.find((t) => t[0] === "d")?.[1] ?? ""; - return { - id, - name: e.tags.find((t) => t[0] === "name")?.[1] ?? id, - picture: e.tags.find((t) => t[0] === "picture")?.[1], - about: e.tags.find((t) => t[0] === "about")?.[1], - createdAt: e.created_at, - flags: SPECIAL_FLAGS.filter((f) => e.tags.some((t) => t[0] === f)), - }; - }) - .filter((g) => g.id) - // Oldest first. - .sort((a, b) => a.createdAt - b.createdAt); + list = await fetchGroups(queryForum); } finally { loaded = true; } diff --git a/src/lib/join.svelte.ts b/src/lib/join.svelte.ts index f527e06..f8dab25 100644 --- a/src/lib/join.svelte.ts +++ b/src/lib/join.svelte.ts @@ -1,24 +1,42 @@ import type { AbstractRelay } from "@nostr/tools/abstract-relay"; +import type { Event } from "@nostr/tools/core"; import { auth } from "$lib/auth.svelte"; -import { GROUP_ID, MODE, JOINCODE_REQUIRED } from "$lib/config"; -import { ensureForumRelay, publishForum } from "$lib/relay"; -import { getGroupFlags } from "$lib/group.svelte"; +import { GROUP_ID, MODE } from "$lib/config"; +import { ensureForumRelay, publishForum, nip43Self } from "$lib/relay"; +import { getGroupName } from "$lib/group.svelte"; +import { showToast } from "$lib/toast.svelte"; type Membership = "member" | "guest"; +// "none": the relay doesn't support NIP-43, so no relay-level join exists. +type RelayMembership = Membership | "none"; -// Per-group membership, resolved lazily and cached reactively so compose -// actions can be gated before the user writes anything. Keyed by pubkey so a +// Writing to a group can be gated at two levels: the relay itself (NIP-43) and +// the group (NIP-29). Neither gate is advertised up front, so membership is +// probed by reading the relay's membership events and, failing that, by trying +// to join and reacting to the rejection. Both caches are keyed by pubkey so a // silent session restore (anon → pubkey) re-evaluates instead of reusing the -// "guest" cached before auth.user was populated; the read stays a pure getter -// (no state writes inside a derivation). +// "guest" cached before auth.user was populated. let membership = $state>({}); +let relayMembership = $state>({}); const checking = new Map>(); +// The modal only appears when a relay refused a step and needs user input. +// - relay-code: the relay requires an invite code (NIP-43 claim) +// - group-code: the group refused the join; a code may be needed +// - pending: the group accepted the request but membership isn't granted yet +// (admin approval); an invite code, if any, can still be tried +// - error: a terminal refusal (e.g. banned); nothing the user can enter helps +export type JoinStep = "relay-code" | "group-code" | "pending" | "error"; + let modalOpen = $state(false); +let modalStep = $state("group-code"); let modalGroup = $state(null); let modalError = $state(null); +// A rejection message that mentions a code makes the code field the primary +// affordance rather than a discreet fallback. +let modalCodeHinted = $state(false); let busy = $state(false); -// Continuation to run once the join succeeds (open the composer, focus the +// Continuation to run once access is granted (open the composer, focus the // reply box, reload a now-visible feed, or retry the original post). let onJoined: (() => void | Promise) | null = null; @@ -29,26 +47,27 @@ export const joinState = { get modalOpen() { return modalOpen; }, + get modalStep() { + return modalStep; + }, get modalError() { return modalError; }, + get modalCodeHinted() { + return modalCodeHinted; + }, get busy() { return busy; }, - // Closed groups (and a globally configured invite gate) can't be self-joined, - // so the modal collects an invite code; open groups just confirm. - get needsCode() { - if (JOINCODE_REQUIRED) return true; - const f = modalGroup ? getGroupFlags(modalGroup) : null; - return !!f?.isClosed; - }, }; export function resetJoinState() { membership = {}; + relayMembership = {}; modalOpen = false; modalGroup = null; modalError = null; + modalCodeHinted = false; busy = false; onJoined = null; } @@ -85,11 +104,41 @@ export function ensureMembershipChecked(groupId: string): Promise { return p; } +type SubFilter = Parameters[0][number]; + +// Collects every event matching the filter until EOSE/close/timeout. +function queryEvents( + relay: AbstractRelay, + filter: SubFilter, + timeoutMs = 3000, +): Promise { + return new Promise((resolve) => { + let settled = false; + const events: Event[] = []; + const finish = () => { + if (settled) return; + settled = true; + try { + sub.close(); + } catch {} + resolve(events); + }; + const sub = relay.subscribe([filter], { + onevent(e) { + events.push(e); + }, + oneose: finish, + onclose: finish, + }); + setTimeout(finish, timeoutMs); + }); +} + // Existence check for a single matching event (resolves true on first event, -// false on EOSE/close/timeout). Used for the kind:39002 members-list fallback. +// false on EOSE/close/timeout). function queryHasMatch( relay: AbstractRelay, - filter: Parameters[0][number], + filter: SubFilter, timeoutMs = 3000, ): Promise { return new Promise((resolve) => { @@ -120,39 +169,34 @@ function queryHasMatch( // Newest `created_at` among events matching the filter, or null if none. We // don't trust the relay to honour limit ordering, so we keep the max across all // returned events (the #p filter keeps the set tiny). -function latestCreatedAt( +async function latestCreatedAt( relay: AbstractRelay, - filter: Parameters[0][number], - timeoutMs = 3000, + filter: SubFilter, ): Promise { - return new Promise((resolve) => { - let settled = false; - let latest: number | null = null; - const finish = () => { - if (settled) return; - settled = true; - try { - sub.close(); - } catch {} - resolve(latest); - }; - const sub = relay.subscribe([filter], { - onevent(e) { - if (latest === null || e.created_at > latest) latest = e.created_at; - }, - oneose: finish, - onclose: finish, - }); - setTimeout(finish, timeoutMs); - }); + const events = await queryEvents(relay, filter); + return events.reduce( + (max, e) => (max === null || e.created_at > max ? e.created_at : max), + null, + ); } -// Membership signal for one group — i.e. "is this user allowed to write here". -// Admins (kind:39001) can post but need not appear in the members list, so they -// count as members. Otherwise NIP-29 records membership as moderation events: -// kind:9000 adds a user, kind:9001 removes one, so the newest of the two for -// this user decides current membership (a later 9001 unjoins them). When the -// relay keeps neither, fall back to the kind:39002 members list. +// Add/remove events are the freshest signal: the newest of the two decides +// (a later removal unjoins). When the relay keeps neither, the list decides. +function decideMembership( + added: number | null, + removed: number | null, + listed: boolean, +): boolean { + if (added !== null || removed !== null) { + return added !== null && added >= (removed ?? -Infinity); + } + return listed; +} + +// Group membership — i.e. "is this user allowed to write here". Admins +// (kind:39001) can post but need not appear in the members list, so they count +// as members. Otherwise NIP-29 records membership as kind:9000 (add) and +// kind:9001 (remove) moderation events, with the kind:39002 list as fallback. async function checkMembership( pubkey: string, groupId: string, @@ -163,7 +207,7 @@ async function checkMembership( } catch { return false; } - const [isAdmin, added, removed] = await Promise.all([ + const [isAdmin, added, removed, listed] = await Promise.all([ queryHasMatch(relay, { kinds: [39001], "#d": [groupId], @@ -172,25 +216,240 @@ async function checkMembership( }), latestCreatedAt(relay, { kinds: [9000], "#h": [groupId], "#p": [pubkey] }), latestCreatedAt(relay, { kinds: [9001], "#h": [groupId], "#p": [pubkey] }), + queryHasMatch(relay, { + kinds: [39002], + "#d": [groupId], + "#p": [pubkey], + limit: 1, + }), ]); if (isAdmin) return true; - if (added !== null || removed !== null) { - return added !== null && added >= (removed ?? -Infinity); - } - return queryHasMatch(relay, { - kinds: [39002], - "#d": [groupId], - "#p": [pubkey], - limit: 1, - }); + return decideMembership(added, removed, listed); } -// Simple mode pre-checks the single configured group at login so the first post -// skips the 9021. Full mode checks lazily per room when the user first acts. +// Relay membership (NIP-43), mirroring the group check: kind:8000 (add) and +// kind:8001 (remove) signed by the relay, with the kind:13534 list as fallback. +// The `member` tag isn't filterable, so the list is fetched and scanned. +async function checkRelayMembership( + pubkey: string, + self: string, +): Promise { + let relay: AbstractRelay; + try { + relay = await ensureForumRelay(); + } catch { + return false; + } + const [added, removed, lists] = await Promise.all([ + latestCreatedAt(relay, { kinds: [8000], authors: [self], "#p": [pubkey] }), + latestCreatedAt(relay, { kinds: [8001], authors: [self], "#p": [pubkey] }), + queryEvents(relay, { kinds: [13534], authors: [self], limit: 1 }), + ]); + const listed = lists.some((e) => + e.tags.some((t) => t[0] === "member" && t[1] === pubkey), + ); + return decideMembership(added, removed, listed); +} + +function ensureRelayMembershipChecked(pubkey: string): Promise { + if (relayMembership[pubkey]) return Promise.resolve(); + const k = `relay:${pubkey}`; + const existing = checking.get(k); + if (existing) return existing; + const p = (async () => { + try { + const self = await nip43Self(); + if (!self) { + relayMembership[pubkey] = "none"; + return; + } + const isMember = await checkRelayMembership(pubkey, self); + relayMembership[pubkey] = isMember ? "member" : "guest"; + } finally { + checking.delete(k); + } + })(); + checking.set(k, p); + return p; +} + +// Probes both membership levels at login so a returning member's first post +// needs no join at all. Full mode checks groups lazily per room instead. export async function initJoinForUser(pubkey: string) { - if (MODE !== "simple") return; - if (await checkMembership(pubkey, GROUP_ID)) { - membership[memberKey(GROUP_ID)] = "member"; + await Promise.all([ + ensureRelayMembershipChecked(pubkey), + MODE === "simple" ? ensureMembershipChecked(GROUP_ID) : undefined, + ]); +} + +// Publishing rejections carry the relay's NIP-01 OK message; the prefix is the +// only cross-relay contract for what went wrong. +function reason(e: unknown): string { + return e instanceof Error ? e.message : String(e); +} +const prefixed = (msg: string, prefix: string) => + msg.toLowerCase().startsWith(prefix); +const stripPrefix = (msg: string) => msg.replace(/^[a-z-]+:\s*/i, ""); +const mentionsCode = (msg: string) => /\b(code|invite|claim)\b/i.test(msg); + +async function publishSigned(kind: number, tags: string[][]): Promise { + if (!auth.signer) throw new Error("Not logged in"); + const event = await auth.signer.signEvent({ + kind, + created_at: Math.floor(Date.now() / 1000), + tags, + content: "", + }); + const timeout = new Promise((_, reject) => + setTimeout(() => reject(new Error("Relay did not respond in time")), 8000), + ); + await Promise.race([Promise.all(publishForum(event)), timeout]); +} + +type Codes = { relay?: string; group?: string }; +type Refusal = { step: JoinStep; message: string | null; codeHinted: boolean }; +type Outcome = { ok: true } | ({ ok: false } & Refusal); + +const fail = ( + step: JoinStep, + message: string | null, + codeHinted = false, +): Outcome => ({ ok: false, step, message, codeHinted }); + +// Bare relay joins the relay declined, per pubkey. A refusal is only a hint: +// some relays (pyramid) let non-members into open groups, so the group join +// decides whether relay membership was needed. Remembering it spares one +// doomed signature per room. +const relayRefusals: Record = {}; + +// Relay-level join (NIP-43). Returns the refusal, if any, without acting on it. +async function tryRelayJoin(pubkey: string, codes: Codes): Promise { + await ensureRelayMembershipChecked(pubkey); + if (relayMembership[pubkey] !== "guest") return { ok: true }; + if (!codes.relay && relayRefusals[pubkey]) return relayRefusals[pubkey]; + const tags: string[][] = [["-"]]; + if (codes.relay) tags.push(["claim", codes.relay]); + let outcome: Outcome = { ok: true }; + let policy = false; + try { + await publishSigned(28934, tags); + relayMembership[pubkey] = "member"; + } catch (e) { + const msg = reason(e); + policy = prefixed(msg, "restricted:") || prefixed(msg, "blocked:"); + if (prefixed(msg, "duplicate:")) { + relayMembership[pubkey] = "member"; + } else if (prefixed(msg, "blocked:")) { + outcome = fail("error", stripPrefix(msg)); + } else { + // A bare request refused is the expected probe, not a user error + outcome = fail("relay-code", codes.relay ? stripPrefix(msg) : null, true); + } + } + if (outcome.ok) delete relayRefusals[pubkey]; + else if (!codes.relay && policy) relayRefusals[pubkey] = outcome; + return outcome; +} + +// Runs the two-level join: relay first (a group join may need relay +// membership), then group. Everything free happens silently; the first +// refusal that actually blocks is reported as the step needing user input. +async function ensureAccess(groupId: string, codes: Codes): Promise { + const pubkey = auth.user?.pubkey; + if (!pubkey) return fail("error", "Not logged in"); + + await ensureMembershipChecked(groupId); + const k = memberKey(groupId); + if (membership[k] === "member") return { ok: true }; + + const relay = await tryRelayJoin(pubkey, codes); + // A code the user typed is judged at once; a bare refusal only counts if + // the group join fails too + if (!relay.ok && codes.relay) return relay; + + const tags: string[][] = [["h", groupId]]; + if (codes.group) tags.push(["code", codes.group]); + try { + await publishSigned(9021, tags); + } catch (e) { + const msg = reason(e); + if (!prefixed(msg, "duplicate:")) { + if (prefixed(msg, "restricted:") && !relay.ok) return relay; + if (prefixed(msg, "restricted:")) { + const hinted = mentionsCode(msg); + return fail( + "group-code", + codes.group || !hinted ? stripPrefix(msg) : null, + hinted, + ); + } + return fail("error", stripPrefix(msg)); + } + } + // An accepted request doesn't imply membership: closed groups may hold it + // for admin approval, so re-read the relay's records before trusting it. + if (await checkMembership(pubkey, groupId)) { + membership[k] = "member"; + showToast(`You joined ${getGroupName(groupId)}`); + return { ok: true }; + } + return fail("pending", codes.group ? "The code was not accepted" : null); +} + +function openModal( + groupId: string | null, + refusal: Refusal, + cb: (() => void | Promise) | null, +) { + modalGroup = groupId; + onJoined = cb; + modalStep = refusal.step; + modalCodeHinted = refusal.codeHinted; + modalError = refusal.message; + modalOpen = true; +} + +// Wraps an action that posts to `groupId`, joining relay and group first when +// needed. A write refused for membership reasons (e.g. a stale cache after the +// relay changed policy) invalidates both caches and retries once from the top. +// Returns false when the modal took over; the action then runs on its success. +export async function withJoin( + groupId: string, + action: () => void | Promise, +): Promise { + if (busy) return false; + busy = true; + try { + return await runWithAccess(groupId, action, {}, true); + } finally { + busy = false; + } +} + +async function runWithAccess( + groupId: string, + action: () => void | Promise, + codes: Codes, + retry: boolean, +): Promise { + const outcome = await ensureAccess(groupId, codes); + if (!outcome.ok) { + openModal(groupId, outcome, action); + return false; + } + try { + await action(); + return true; + } catch (e) { + const pubkey = auth.user?.pubkey; + if (retry && pubkey && prefixed(reason(e), "restricted:")) { + delete membership[memberKey(groupId)]; + delete relayRefusals[pubkey]; + if (relayMembership[pubkey] === "member") + relayMembership[pubkey] = "guest"; + return runWithAccess(groupId, action, codes, false); + } + throw e; } } @@ -202,86 +461,51 @@ export function closeJoinModal() { onJoined = null; } -// Opens the join modal for a group, with an optional continuation to run after -// a successful join (open the composer, focus the reply box, reload the feed). -export function openJoinModal( - groupId: string, - cb?: () => void | Promise, -) { - modalGroup = groupId; - onJoined = cb ?? null; - modalError = null; - modalOpen = true; -} - -async function publishJoinRequest(groupId: string, code?: string) { - if (!auth.signer) throw new Error("Not logged in"); - const tags: string[][] = [["h", groupId]]; - if (code) tags.push(["code", code]); - const event = await auth.signer.signEvent({ - kind: 9021, - created_at: Math.floor(Date.now() / 1000), - tags, - content: "", - }); - const timeout = new Promise((_, reject) => - setTimeout(() => reject(new Error("Relay did not respond in time")), 8000), - ); - await Promise.race([Promise.all(publishForum(event)), timeout]); -} - -// Sends the kind:9021 for the modal's target group, marks the user joined, and -// runs the pending continuation. Errors keep the modal open for a retry. -export async function submitJoin(code?: string) { - if (!modalGroup || busy) return; +// Re-runs the join with the code entered for the current step. Errors keep the +// modal open for a retry; a further gate switches the modal to that step. +export async function submitJoinCode(code?: string) { + if (!modalOpen || busy) return; busy = true; modalError = null; const groupId = modalGroup; const cb = onJoined; + const codes: Codes = + modalStep === "relay-code" ? { relay: code } : { group: code }; try { - await publishJoinRequest(groupId, code); - membership[memberKey(groupId)] = "member"; + const pubkey = auth.user?.pubkey; + const outcome = groupId + ? await ensureAccess(groupId, codes) + : pubkey + ? await tryRelayJoin(pubkey, codes) + : fail("error", "Not logged in"); + if (!outcome.ok) { + openModal(groupId, outcome, cb); + return; + } modalOpen = false; modalGroup = null; onJoined = null; if (cb) await cb(); } catch (e) { - modalError = e instanceof Error ? e.message : "Could not join the group"; + modalError = reason(e); } finally { busy = false; } } -// Wraps an action that posts to `groupId`. The relay requires membership to -// write to any group, so a non-member is joined first. Compose entry points -// gate membership up front, making this mostly a safety net: it joins -// transparently if possible, else opens the modal to retry. -export async function withJoin( - groupId: string, - action: () => Promise, -): Promise { - if (membership[memberKey(groupId)] === "member") { - await action(); - return true; - } +// Relay-only join, for a relay that refuses reads from non-members: no group +// is involved, so the modal (if any) targets the relay alone. +export async function joinRelay(cb: () => void | Promise): Promise { + const pubkey = auth.user?.pubkey; + if (!pubkey || busy) return; busy = true; try { - const pubkey = auth.user?.pubkey; - if (pubkey && (await checkMembership(pubkey, groupId))) { - membership[memberKey(groupId)] = "member"; - await action(); - return true; + const outcome = await tryRelayJoin(pubkey, {}); + if (!outcome.ok) { + openModal(null, outcome, cb); + return; } - await publishJoinRequest(groupId); - await action(); - membership[memberKey(groupId)] = "member"; - return true; - } catch (e) { - modalGroup = groupId; - onJoined = action; - modalError = e instanceof Error ? e.message : "Could not join the group"; - modalOpen = true; - return false; + await cb(); } finally { busy = false; } diff --git a/src/lib/linkify.ts b/src/lib/linkify.ts index abc3117..5cef2a0 100644 --- a/src/lib/linkify.ts +++ b/src/lib/linkify.ts @@ -157,7 +157,7 @@ function decodeEntity(entity: string): ChatToken | null { return { type: "entity", entity, - href: `https://njump.me/${entity}`, + href: `https://nostr.basspistol.org/${entity}`, label: shortNostrEntity(entity), id: decoded.type === "note" ? decoded.data : decoded.data.id, }; @@ -165,7 +165,7 @@ function decodeEntity(entity: string): ChatToken | null { return { type: "entity", entity, - href: `https://njump.me/${entity}`, + href: `https://nostr.basspistol.org/${entity}`, label: shortNostrEntity(entity), }; } catch { diff --git a/src/lib/mentions.ts b/src/lib/mentions.ts index 11d7bb5..b40bb08 100644 --- a/src/lib/mentions.ts +++ b/src/lib/mentions.ts @@ -1,5 +1,5 @@ import * as nip19 from "@nostr/tools/nip19"; -import { loadRelayList } from "@nostr/gadgets/lists"; +import { loadRelayList } from "$lib/gadgets"; const RELAY_HINT_TIMEOUT_MS = 1500; diff --git a/src/lib/overview.svelte.ts b/src/lib/overview.svelte.ts index 4ef9abb..e1e75fc 100644 --- a/src/lib/overview.svelte.ts +++ b/src/lib/overview.svelte.ts @@ -1,22 +1,16 @@ -import type { AbstractRelay } from "@nostr/tools/abstract-relay"; -import type { Event } from "@nostr/tools/core"; -import type { Filter } from "@nostr/tools/filter"; -import { loadNostrUser, type NostrUser } from "@nostr/gadgets/metadata"; +import { page } from "$app/state"; +import { loadNostrUser, type NostrUser } from "$lib/gadgets"; import { ensureForumRelay } from "$lib/relay"; import { ingestNostrUser } from "$lib/profiles.svelte"; +import { relayQuery } from "$lib/forum/query"; +import { + fetchOverview, + overviewPubkeys, + type RecentThread, + type RoomActivity, +} from "$lib/forum/overview"; -export type RoomActivity = { - latestAt: number; - latestPubkey: string; -}; - -export type RecentThread = { - id: string; - title: string; - groupId: string; - authorPubkey: string; - createdAt: number; -}; +export type { RecentThread, RoomActivity }; let activity = $state>({}); let admins = $state>({}); // room id -> first admin pubkey @@ -24,43 +18,32 @@ let recent = $state([]); let profiles = $state>({}); let loading = $state(false); let loadedKey = ""; // room-id set last loaded for, to avoid redundant refetches +let settled = $state(false); // first live load finished + +// Until the live fetch lands, the server snapshot (if any) stands in. +function fallback() { + return settled ? null : (page.data.overview ?? null); +} export const overviewStore = { get activity() { - return activity; + return fallback()?.activity ?? activity; }, get admins() { - return admins; + return fallback()?.admins ?? admins; }, get recent() { - return recent; + return fallback()?.recent ?? recent; }, get profiles() { - return profiles; + const base = page.data.overview?.profiles; + return base ? { ...base, ...profiles } : profiles; }, get loading() { return loading; }, }; -function querySync(relay: AbstractRelay, filter: Filter): Promise { - return new Promise((resolve) => { - const events: Event[] = []; - const sub = relay.subscribe([filter], { - onevent(e) { - events.push(e); - }, - oneose() { - sub.close(); - resolve(events); - }, - onclose() { - resolve(events); - }, - }); - }); -} - async function loadProfile(pubkey: string) { if (profiles[pubkey]) return; const user = await loadNostrUser(pubkey); @@ -77,54 +60,15 @@ export async function loadOverview(roomIds: string[]) { loadedKey = key; loading = true; - const relay = await ensureForumRelay(); try { - // One tiny query per room for its newest event (thread or reply). - const latest = await Promise.all( - roomIds.map((id) => - querySync(relay, { kinds: [11, 1111], "#h": [id], limit: 1 }), - ), - ); - const act: Record = {}; - roomIds.forEach((id, i) => { - const e = latest[i][0]; - if (e) act[id] = { latestAt: e.created_at, latestPubkey: e.pubkey }; - }); - activity = act; - - // Each room's admin (NIP-29 kind 39001, first `p` tag) for the card byline. - const adminEvents = await Promise.all( - roomIds.map((id) => querySync(relay, { kinds: [39001], "#d": [id] })), - ); - const adm: Record = {}; - roomIds.forEach((id, i) => { - const pk = adminEvents[i][0]?.tags.find((t) => t[0] === "p")?.[1]; - if (pk) adm[id] = pk; - }); - admins = adm; - - // Most recent discussions (thread OPs) across all rooms. The group relay - // truncates multi-value "#h" filters, so query each room and merge. - const perRoom = await Promise.all( - roomIds.map((id) => - querySync(relay, { kinds: [11], "#h": [id], limit: 20 }), - ), - ); - const threads = perRoom.flat(); - threads.sort((a, b) => b.created_at - a.created_at); - recent = threads.slice(0, 20).map((e) => ({ - id: e.id, - title: e.tags.find((t) => t[0] === "title")?.[1] ?? "(untitled)", - groupId: e.tags.find((t) => t[0] === "h")?.[1] ?? "", - authorPubkey: e.pubkey, - createdAt: e.created_at, - })); - - for (const a of Object.values(act)) loadProfile(a.latestPubkey); - for (const pk of Object.values(adm)) loadProfile(pk); - for (const t of recent) loadProfile(t.authorPubkey); + const q = relayQuery(await ensureForumRelay()); + const o = await fetchOverview(q, roomIds); + activity = o.activity; + admins = o.admins; + recent = o.recent; + settled = true; + overviewPubkeys(o).forEach(loadProfile); } finally { - // shared forum connection is long-lived — don't close it here loading = false; } } diff --git a/src/lib/pageHighlight.ts b/src/lib/pageHighlight.ts new file mode 100644 index 0000000..48824c8 --- /dev/null +++ b/src/lib/pageHighlight.ts @@ -0,0 +1,61 @@ +import { + findPhraseSpans, + findMatchSpans, + tokenize, + type Span, +} from "$lib/textMatch"; + +const HIGHLIGHT_NAME = "search-terms"; + +function supported(): boolean { + return typeof CSS !== "undefined" && "highlights" in CSS; +} + +// Highlight the query inside the given roots using the CSS Custom Highlight +// API, which paints ranges without touching the DOM, so it never conflicts +// with Svelte re-renders. When the whole query appears contiguously anywhere +// on the page, only those phrase occurrences are painted; otherwise the +// textMatch fallback (contiguous runs as blocks, then single words) is used. +// Returns the first match in document order (for scrolling), or null. No-op +// on unsupported browsers. +export function applyHighlights( + roots: Iterable, + query: string, +): Range | null { + if (!supported()) return null; + CSS.highlights.delete(HIGHLIGHT_NAME); + if (tokenize(query).length === 0) return null; + + const nodes: Node[] = []; + for (const root of roots) { + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); + let node: Node | null; + while ((node = walker.nextNode())) nodes.push(node); + } + + let perNode: Span[][] = nodes.map((n) => + findPhraseSpans(n.textContent ?? "", query), + ); + if (perNode.every((spans) => spans.length === 0)) { + perNode = nodes.map((n) => findMatchSpans(n.textContent ?? "", query)); + } + + const ranges: Range[] = []; + nodes.forEach((node, i) => { + for (const span of perNode[i]) { + const range = new Range(); + range.setStart(node, span.start); + range.setEnd(node, span.end); + ranges.push(range); + } + }); + if (ranges.length === 0) return null; + + CSS.highlights.set(HIGHLIGHT_NAME, new Highlight(...ranges)); + ranges.sort((a, b) => a.compareBoundaryPoints(Range.START_TO_START, b)); + return ranges[0]; +} + +export function clearHighlights() { + if (supported()) CSS.highlights.delete(HIGHLIGHT_NAME); +} diff --git a/src/lib/partials.svelte.ts b/src/lib/partials.svelte.ts index ba45c49..79eda7a 100644 --- a/src/lib/partials.svelte.ts +++ b/src/lib/partials.svelte.ts @@ -1,63 +1,32 @@ +import { page } from "$app/state"; import { adminPubkeys } from "$lib/admins.svelte"; import { queryForum } from "$lib/relay"; +import { + fetchPartials, + pickPartial, + type Partial, + type PartialSlot, +} from "$lib/forum/partials"; -// Named slots a partial can fill. The NIP-23 `d` tag carries the slot name. -export type PartialSlot = "home" | "contacts"; -const SLOTS = new Set(["home", "contacts"]); - -export type Partial = { - id: string; - slot: PartialSlot; - title: string; - content: string; // markdown body - pubkey: string; - createdAt: number; -}; +export type { Partial, PartialSlot }; let all = $state([]); let loaded = $state(false); +// Until the live fetch lands, the server snapshot (if any) stands in. export const partialsStore = { - // The newest admin-authored partial for a slot, or undefined. The relay query - // is open, so the trusted admin set is the gate; resolving the winner here - // (rather than at load time) keeps a non-admin from shadowing it with a newer - // event, and picks up the admin set once it finishes loading in full mode. get(slot: PartialSlot): Partial | undefined { - const admins = new Set(adminPubkeys.list); - let best: Partial | undefined; - for (const p of all) { - if (p.slot !== slot || !admins.has(p.pubkey)) continue; - if (!best || p.createdAt > best.createdAt) best = p; - } - return best; + const source = loaded ? all : (page.data.shell?.partials ?? []); + return pickPartial(source, slot, adminPubkeys.list); }, get loaded() { - return loaded; + return loaded || !!page.data.shell; }, }; -// Partials are kind 30023 (NIP-23 long-form) tagged ["t", "squalk-partial"]; -// the `d` tag names the slot the article fills. export async function loadPartials() { try { - const events = await queryForum({ - kinds: [30023], - "#t": ["squalk-partial"], - }); - const next: Partial[] = []; - for (const e of events) { - const slot = e.tags.find((t) => t[0] === "d")?.[1]; - if (!slot || !SLOTS.has(slot)) continue; - next.push({ - id: e.id, - slot: slot as PartialSlot, - title: e.tags.find((t) => t[0] === "title")?.[1] ?? slot, - content: e.content, - pubkey: e.pubkey, - createdAt: e.created_at, - }); - } - all = next; + all = await fetchPartials(queryForum); } finally { loaded = true; } diff --git a/src/lib/profiles.svelte.ts b/src/lib/profiles.svelte.ts index c56428a..6765804 100644 --- a/src/lib/profiles.svelte.ts +++ b/src/lib/profiles.svelte.ts @@ -4,6 +4,7 @@ import { SvelteMap, SvelteSet } from "svelte/reactivity"; import type { NostrUser } from "@nostr/gadgets/metadata"; import { GROUP_ID } from "$lib/config"; import { queryForum } from "$lib/relay"; +import { PROFILE_RELAYS } from "$lib/forum/profiles"; export type ProfileEntry = { pubkey: string; @@ -27,12 +28,6 @@ const SEARCH_RELAYS = [ // Indexer relays used for kind:0 / kind:3 lookups. The forum relay almost // never has these — they live on each author's outbox relays — so we query // a small set of well-known aggregators. -const PROFILE_RELAYS = [ - "wss://purplepag.es", - "wss://relay.nostr.band", - "wss://relay.damus.io", - "wss://nos.lol", -]; const FETCH_BATCH_SIZE = 500; const profiles = new SvelteMap(); @@ -310,10 +305,9 @@ export function ingestProfileEvent(evt: Event) { if (entry) upsertProfile(entry); } -export function ingestNostrUser(user: NostrUser) { - if (!user.pubkey) return; +export function entryFromUser(user: NostrUser): ProfileEntry { const md = user.metadata ?? {}; - const entry: ProfileEntry = { + return { pubkey: user.pubkey, npub: user.npub, name: md.name, @@ -325,5 +319,9 @@ export function ingestNostrUser(user: NostrUser) { lud16: md.lud16, fetchedAt: user.lastUpdated || 0, }; - upsertProfile(entry); +} + +export function ingestNostrUser(user: NostrUser) { + if (!user.pubkey) return; + upsertProfile(entryFromUser(user)); } diff --git a/src/lib/relay.ts b/src/lib/relay.ts index e850417..22a4663 100644 --- a/src/lib/relay.ts +++ b/src/lib/relay.ts @@ -26,7 +26,9 @@ export const forumPool = pool; function authParam() { const signer = auth.signer; - return signer ? { onauth: (evt: EventTemplate) => signer.signEvent(evt) } : {}; + return signer + ? { onauth: (evt: EventTemplate) => signer.signEvent(evt) } + : {}; } // Resolves once the forum connection is open and, when logged in, NIP-42 @@ -104,3 +106,53 @@ export async function ensureForumRelay(): Promise { export function resetForumConnection(): void { pool.close([RELAY_URL]); } + +// NIP-11 document of the forum relay, fetched once per session. Used for NIP-43 +// discovery (clients must only send relay join requests to relays advertising +// it; `self` signs the membership events we read back) and for the access gate, +// which can show the relay's name before anything else is readable. +export type ForumRelayInfo = { + self: string | null; + nips: string[]; + name: string; + description: string; + icon: string; + authRequired: boolean; +}; +let relayInfo: Promise | null = null; + +export function forumRelayInfo(): Promise { + if (!relayInfo) { + relayInfo = import("@nostr/tools/nip11") + .then(({ fetchRelayInformation }) => fetchRelayInformation(RELAY_URL)) + .then((info) => { + // `self` is a NIP-43 addition the library type doesn't know yet + const self = (info as { self?: unknown }).self; + return { + self: typeof self === "string" ? self : null, + // Relays mix numbers and strings in this list + nips: (info.supported_nips ?? []).map(String), + name: info.name ?? "", + description: info.description ?? "", + icon: info.icon ?? "", + authRequired: info.limitation?.auth_required === true, + }; + }) + .catch(() => ({ + self: null, + nips: [], + name: "", + description: "", + icon: "", + authRequired: false, + })); + } + return relayInfo; +} + +// The relay pubkey when the forum relay supports NIP-43 relay membership, +// otherwise null (no relay-level join is needed or allowed). +export async function nip43Self(): Promise { + const info = await forumRelayInfo(); + return info.nips.includes("43") ? info.self : null; +} diff --git a/src/lib/resources.svelte.ts b/src/lib/resources.svelte.ts index 7d4b321..aa21b65 100644 --- a/src/lib/resources.svelte.ts +++ b/src/lib/resources.svelte.ts @@ -1,70 +1,30 @@ +import { page } from "$app/state"; import { adminPubkeys } from "$lib/admins.svelte"; import { queryForum } from "$lib/relay"; +import { fetchResources, type Resource } from "$lib/forum/resources"; -export type Resource = { - id: string; - slug: string; // NIP-23 `d` tag — also the resource URL slug - title: string; - content: string; // markdown body - position?: number; - pubkey: string; - createdAt: number; -}; +export type { Resource }; let all = $state([]); let loaded = $state(false); +// Only admin-authored resources are surfaced: the relay query is open, so the +// admin set is the trust gate. Until the live fetch lands, the server +// snapshot (if any) stands in. export const resourcesStore = { - // Only resources authored by an admin are surfaced; the relay query is - // open, so the trusted admin set is the gate. get list() { const admins = new Set(adminPubkeys.list); - return all.filter((r) => admins.has(r.pubkey)); + const source = loaded ? all : (page.data.shell?.resources ?? []); + return source.filter((r) => admins.has(r.pubkey)); }, get loaded() { - return loaded; + return loaded || !!page.data.shell; }, }; -// Positioned resources come first by ascending `position`; the rest follow -// alphabetically by title. Equal positions fall back to title order. -function compare(a: Resource, b: Resource): number { - const ap = a.position; - const bp = b.position; - if (ap !== undefined && bp !== undefined) - return ap !== bp ? ap - bp : a.title.localeCompare(b.title); - if (ap !== undefined) return -1; - if (bp !== undefined) return 1; - return a.title.localeCompare(b.title); -} - -// Resources are kind 30023 (NIP-23 long-form) tagged ["t", "squalk-resource"]. export async function loadResources() { try { - const events = await queryForum({ - kinds: [30023], - "#t": ["squalk-resource"], - }); - // Addressable events: keep the newest per `d` slug. - const bySlug = new Map(); - for (const e of events) { - const slug = e.tags.find((t) => t[0] === "d")?.[1]; - if (!slug) continue; - const existing = bySlug.get(slug); - if (existing && existing.createdAt >= e.created_at) continue; - const posTag = e.tags.find((t) => t[0] === "position")?.[1]; - const pos = posTag !== undefined ? Number(posTag) : NaN; - bySlug.set(slug, { - id: e.id, - slug, - title: e.tags.find((t) => t[0] === "title")?.[1] ?? slug, - content: e.content, - position: Number.isFinite(pos) ? pos : undefined, - pubkey: e.pubkey, - createdAt: e.created_at, - }); - } - all = [...bySlug.values()].sort(compare); + all = await fetchResources(queryForum); } finally { loaded = true; } diff --git a/src/lib/search.ts b/src/lib/search.ts index af0967a..c779206 100644 --- a/src/lib/search.ts +++ b/src/lib/search.ts @@ -1,6 +1,7 @@ import type { Event } from "@nostr/tools/core"; import type { Filter } from "@nostr/tools/filter"; import { queryForum } from "$lib/relay"; +import { bestMatch } from "$lib/textMatch"; import { GROUP_ID, MODE } from "$lib/config"; import { groupsStore } from "$lib/groups.svelte"; @@ -12,22 +13,26 @@ export type SearchResult = { createdAt: number; }; -// Window the snippet around the first term match so the highlight is visible -// even when the match sits deep in a long post. -function snippetOf(content: string, query: string): string { +// Window the snippet around the best match (full phrase > longest +// contiguous run > densest single-term cluster, per textMatch rules). The +// score ranks how well this content matched, so dedupe can keep the best +// snippet per thread and the result list can order its tiers. +function snippetOf( + content: string, + query: string, +): { text: string; score: number } { const flat = content.replace(/\s+/g, " ").trim(); const MAX = 140; - if (flat.length <= MAX) return flat; - const lower = flat.toLowerCase(); - let idx = -1; - for (const t of query.toLowerCase().split(/\s+/).filter(Boolean)) { - const i = lower.indexOf(t); - if (i !== -1 && (idx === -1 || i < idx)) idx = i; - } - if (idx <= 40) return flat.slice(0, MAX) + "…"; - const start = idx - 40; + const { anchor, score } = bestMatch(flat, query, MAX - 40); + + if (flat.length <= MAX) return { text: flat, score }; + if (anchor <= 40) return { text: flat.slice(0, MAX) + "…", score }; + const start = anchor - 40; const end = Math.min(flat.length, start + MAX); - return "…" + flat.slice(start, end) + (end < flat.length ? "…" : ""); + return { + text: "…" + flat.slice(start, end) + (end < flat.length ? "…" : ""), + score, + }; } // NIP-50 search over the forum relay: thread OPs (kind 11) and replies @@ -48,46 +53,58 @@ export async function searchThreads(query: string): Promise { return h !== undefined && (groups.size === 0 || groups.has(h)); }); - const byThread = new Map(); - const opless: SearchResult[] = []; + type Candidate = SearchResult & { score: number }; + const byThread = new Map(); + const needTitle: Candidate[] = []; for (const e of events) { - if (e.kind === 11) { - const existing = byThread.get(e.id); - // An OP match wins over a reply match on the same thread - if (existing && existing.matchKind === "thread") continue; - byThread.set(e.id, { - threadId: e.id, - title: e.tags.find((t) => t[0] === "title")?.[1] ?? "(untitled)", - snippet: snippetOf(e.content, query), - matchKind: "thread", - createdAt: e.created_at, - }); - } else { - const root = e.tags.find((t) => t[0] === "E")?.[1]; - if (!root || byThread.has(root)) continue; - const r: SearchResult = { - threadId: root, - title: "", - snippet: snippetOf(e.content, query), - matchKind: "reply", + const isOp = e.kind === 11; + const threadId = isOp ? e.id : e.tags.find((t) => t[0] === "E")?.[1]; + if (!threadId) continue; + const title = isOp + ? (e.tags.find((t) => t[0] === "title")?.[1] ?? "(untitled)") + : ""; + const { text, score } = snippetOf(e.content, query); + const existing = byThread.get(threadId); + if (!existing) { + const r: Candidate = { + threadId, + title, + snippet: text, + matchKind: isOp ? "thread" : "reply", createdAt: e.created_at, + score, }; - byThread.set(root, r); - opless.push(r); + byThread.set(threadId, r); + if (!isOp) needTitle.push(r); + } else { + // Keep the snippet of whichever event matched the query best + if (score > existing.score) { + existing.snippet = text; + existing.matchKind = isOp ? "thread" : "reply"; + existing.score = score; + } + if (isOp && !existing.title) existing.title = title; } } // Backfill titles for reply-only matches; the group relay truncates // multi-id queries, so fetch each OP on its own await Promise.all( - opless.map(async (r) => { + needTitle.map(async (r) => { + if (r.title) return; // The OP appeared in the same result set const ops = await queryForum({ kinds: [11], ids: [r.threadId] }); const op: Event | undefined = ops[0]; r.title = op?.tags.find((t) => t[0] === "title")?.[1] ?? "(untitled)"; }), ); - // Keep the relay's relevance order (arrival order); a thread keeps the - // position of its best-ranked match - return [...byThread.values()]; + // Tiered ordering: full-phrase matches, then contiguous multi-word runs + // (longest first), then single-word matches capped to keep noise down. + // Within a tier the relay's relevance (arrival) order is preserved — + // Array.prototype.sort is stable. + const SINGLES_LIMIT = 5; + let singles = 0; + return [...byThread.values()] + .sort((a, b) => b.score - a.score) + .filter((r) => r.score >= 100 || ++singles <= SINGLES_LIMIT); } diff --git a/src/lib/searchState.svelte.ts b/src/lib/searchState.svelte.ts index beb9870..61899e0 100644 --- a/src/lib/searchState.svelte.ts +++ b/src/lib/searchState.svelte.ts @@ -1,4 +1,5 @@ import { searchThreads, type SearchResult } from "$lib/search"; +import { findPhraseSpans, findMatchSpans } from "$lib/textMatch"; // Debounced-search state machine shared by the inline (homepage) and modal // search shells, so behavior lives in one place and the shells only differ @@ -12,10 +13,6 @@ export function createSearchState() { let timer: ReturnType | null = null; let seq = 0; - const terms = $derived( - resultsQuery.split(/\s+/).filter((t) => t.length >= 2), - ); - function schedule() { const q = query.trim(); if (timer) clearTimeout(timer); @@ -41,19 +38,26 @@ export function createSearchState() { }, 300); } - function escapeRe(s: string): string { - return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - } - - // Split into alternating plain/matched segments for rendering + // Split into alternating plain/matched segments for rendering. + // When the text contains the whole query as a contiguous phrase, only the + // phrase is marked; otherwise contiguous runs (as single blocks) and + // single words are marked as a fallback, to show why the text matched. function highlight(text: string): { text: string; hit: boolean }[] { - if (!text || terms.length === 0) return [{ text, hit: false }]; - const alts = terms.map(escapeRe).join("|"); - const exact = new RegExp(`^(${alts})$`, "i"); - return text - .split(new RegExp(`(${alts})`, "gi")) - .filter((s) => s !== "") - .map((s) => ({ text: s, hit: exact.test(s) })); + if (!text) return [{ text, hit: false }]; + const phrase = findPhraseSpans(text, resultsQuery); + const spans = + phrase.length > 0 ? phrase : findMatchSpans(text, resultsQuery); + if (spans.length === 0) return [{ text, hit: false }]; + const out: { text: string; hit: boolean }[] = []; + let pos = 0; + for (const s of spans) { + if (s.start > pos) + out.push({ text: text.slice(pos, s.start), hit: false }); + out.push({ text: text.slice(s.start, s.end), hit: true }); + pos = s.end; + } + if (pos < text.length) out.push({ text: text.slice(pos), hit: false }); + return out; } // Arrow/Enter handling; returns true when the event was consumed @@ -98,6 +102,13 @@ export function createSearchState() { get active() { return resultsQuery.trim().length >= 2; }, + get resultsQuery() { + return resultsQuery; + }, + // Thread link carrying the query, so the page can highlight the terms + threadUrl(threadId: string) { + return `/thread/${threadId}?q=${encodeURIComponent(resultsQuery.trim())}`; + }, schedule, highlight, navigate, diff --git a/src/lib/seo.ts b/src/lib/seo.ts new file mode 100644 index 0000000..7f81358 --- /dev/null +++ b/src/lib/seo.ts @@ -0,0 +1,47 @@ +// Markdown inline syntax reduced to plain text. +function stripInline(text: string): string { + return text + .replace(/!\[[^\]]*\]\([^)]*\)/g, "") // images + .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // links → label + .replace(/[*_`~]+/g, "") + .replace(/nostr:([a-z0-9]{12})[a-z0-9]*/g, "@$1…") // keep a short handle + .replace(/\s+/g, " ") + .trim(); +} + +function truncate(text: string, max: number): string { + return text.length > max ? text.slice(0, max - 1).trimEnd() + "…" : text; +} + +function isProse(line: string): boolean { + return !!line && !line.startsWith("#") && !/^https?:\/\/\S+$/.test(line); +} + +// Plain-text excerpt of a markdown body for description meta tags: the first +// paragraph that isn't a heading or a bare media URL, trimmed to `max` chars. +export function excerpt(markdown: string, max = 160): string { + const line = markdown + .split("\n") + .map((l) => l.trim()) + .find(isProse); + return line ? truncate(stripInline(line), max) : ""; +} + +// The whole body flattened to one plain-text line, trimmed to `max` chars. +// Headings, bare media URLs, code blocks, quoted text and list markers are +// dropped, so a reply reads as its own words. +export function summarize(markdown: string, max = 200): string { + let inCode = false; + const lines: string[] = []; + for (const raw of markdown.split("\n")) { + const l = raw.trim(); + if (l.startsWith("```")) { + inCode = !inCode; + continue; + } + if (inCode || l.startsWith(">")) continue; + const line = l.replace(/^(?:[-*+]\s+|\d+[.)]\s+)+/, ""); + if (isProse(line)) lines.push(line); + } + return truncate(stripInline(lines.join(" ")), max); +} diff --git a/src/lib/ssr/endpoint.ts b/src/lib/ssr/endpoint.ts new file mode 100644 index 0000000..62c83e3 --- /dev/null +++ b/src/lib/ssr/endpoint.ts @@ -0,0 +1,26 @@ +import { error, type RequestEvent } from "@sveltejs/kit"; +import { CACHE_CONTROL } from "$lib/config"; +import { RelayUnavailable } from "./relay"; + +// Wraps a snapshot endpoint: answers carry the shared cache policy, while a +// relay that could not be asked becomes an uncached 503, which snapshot() +// treats as "no snapshot" so the page hydrates empty and the client takes the +// same path as a client-only build (probe, gate or live fetch). +export function snapshotHandler( + handler: (event: E) => Promise, +): (event: E) => Promise { + return async (event) => { + try { + const res = await handler(event); + event.setHeaders({ "cache-control": CACHE_CONTROL }); + return res; + } catch (e) { + const unavailable = e instanceof RelayUnavailable; + event.setHeaders({ + "cache-control": unavailable ? "no-store" : CACHE_CONTROL, + }); + if (unavailable) error(503, "Relay unavailable"); + throw e; + } + }; +} diff --git a/src/lib/ssr/relay.ts b/src/lib/ssr/relay.ts new file mode 100644 index 0000000..de9af08 --- /dev/null +++ b/src/lib/ssr/relay.ts @@ -0,0 +1,171 @@ +import { SimplePool } from "@nostr/tools"; +import type { Event } from "@nostr/tools/core"; +import type { Filter } from "@nostr/tools/filter"; +import { RELAY_URL, SSR_CACHE_FRESH, SSR_CACHE_STALE } from "$lib/config"; +import { PROFILE_RELAYS } from "$lib/forum/profiles"; +import type { Query } from "$lib/forum/query"; + +// Anonymous relay access for server rendering. The server has no signer, so +// it only ever sees what the relay serves the public; members get their +// private rooms once the client takes over. Every query is bounded so a slow +// relay degrades to an empty page, never a hung request. +// +// A relay that is down or refuses anonymous reads (NIP-42 auth-required) is +// not the same as an empty result: the query rejects with RelayUnavailable so +// endpoints answer 503 rather than 404, and the client renders as it would +// without SSR (probing the relay and showing the gate when it must). +// +// Results are cached in memory with the fresh/stale windows from config: +// fresh entries are returned as they are, stale ones are returned at once and +// refreshed in the background, expired ones are fetched again. Profiles +// rarely change, so their fresh window is at least an hour. +const QUERY_TIMEOUT = 2500; +const PROFILE_TIMEOUT = 1500; +const CONNECT_TIMEOUT = 1500; +const FORUM_FRESH = SSR_CACHE_FRESH * 1000; +const PROFILE_FRESH = Math.max(FORUM_FRESH, 60 * 60_000); +const STALE = Math.max(SSR_CACHE_STALE * 1000, FORUM_FRESH); +const DEAD_RELAY_TTL = 5 * 60_000; +const CACHE_MAX = 2000; + +export class RelayUnavailable extends Error { + constructor() { + super("No relay answered"); + this.name = "RelayUnavailable"; + } +} + +const pool = new SimplePool(); +// Open relay sockets keep the process alive after SIGTERM until systemd kills it +process.on("sveltekit:shutdown", () => pool.destroy()); +type Entry = { at: number; result: Promise; refreshing: boolean }; +const cache = new Map(); +// Relays that failed to connect are skipped for a while, so a dead profile +// relay doesn't add its connect timeout to every cold page. +const deadUntil = new Map(); + +// Ask every reachable relay and resolve as soon as each has answered (EOSE or +// closed), or at the deadline, whichever comes first — always with whatever +// arrived, so one stalled relay never costs the events the others delivered. +// Rejects when no relay reached EOSE: a CLOSED (auth-required), a failed +// connection or the deadline are not answers. +function boundedQuery( + relays: string[], + filter: Filter, + maxWait: number, +): Promise { + return new Promise((resolve, reject) => { + const events: Event[] = []; + const subs: { close(): void }[] = []; + let done = false; + let answered = false; + let pending = 0; + const finish = () => { + if (done) return; + done = true; + clearTimeout(timer); + for (const s of subs) { + try { + s.close(); + } catch { + // Already closed + } + } + if (answered) resolve(events); + else reject(new RelayUnavailable()); + }; + const timer = setTimeout(finish, maxWait); + + const now = Date.now(); + const live = relays.filter((url) => (deadUntil.get(url) ?? 0) <= now); + pending = live.length; + if (pending === 0) return finish(); + + for (const url of live) { + let settled = false; + const settle = () => { + if (settled) return; + settled = true; + if (--pending <= 0) finish(); + }; + pool.ensureRelay(url, { connectionTimeout: CONNECT_TIMEOUT }).then( + (relay) => { + if (done) return settle(); + const sub = relay.subscribe([filter], { + onevent: (e) => events.push(e), + oneose: () => { + answered = true; + sub.close(); + settle(); + }, + onclose: settle, + // The library fakes an EOSE after its own timeout; the deadline + // above must win so a silent relay does not count as an answer + eoseTimeout: maxWait * 2, + }); + subs.push(sub); + }, + () => { + deadUntil.set(url, Date.now() + DEAD_RELAY_TTL); + settle(); + }, + ); + } + }); +} + +function cachedQuery( + relays: string[], + filter: Filter, + maxWait: number, + fresh: number, +): Promise { + const key = relays.join(",") + "|" + JSON.stringify(filter); + const now = Date.now(); + const hit = cache.get(key); + if (hit) { + const age = now - hit.at; + if (age < fresh) return hit.result; + if (age < STALE) { + if (!hit.refreshing) { + hit.refreshing = true; + boundedQuery(relays, filter, maxWait).then( + (events) => { + cache.set(key, { + at: Date.now(), + result: Promise.resolve(events), + refreshing: false, + }); + }, + // Keep serving the stale entry, try again next time + () => { + hit.refreshing = false; + }, + ); + } + return hit.result; + } + } + if (cache.size >= CACHE_MAX) cache.clear(); + const result = boundedQuery(relays, filter, maxWait); + const entry: Entry = { at: now, result, refreshing: false }; + cache.set(key, entry); + // A failure is never cached, so the next request asks the relay again + result.catch(() => { + if (cache.get(key) === entry) cache.delete(key); + }); + return result; +} + +export const forumQuery: Query = (filter) => + cachedQuery([RELAY_URL], filter, QUERY_TIMEOUT, FORUM_FRESH); + +// Profiles mostly live off the forum relay; ask it too for members who only +// published there. +export const profileQuery: Query = (filter) => + cachedQuery( + [RELAY_URL, ...PROFILE_RELAYS], + filter, + PROFILE_TIMEOUT, + PROFILE_FRESH, + ); diff --git a/src/lib/ssr/shell.ts b/src/lib/ssr/shell.ts new file mode 100644 index 0000000..4b522d9 --- /dev/null +++ b/src/lib/ssr/shell.ts @@ -0,0 +1,29 @@ +import { MODE, GROUP_ID } from "$lib/config"; +import { fetchGroup, fetchGroups, fetchRoomAdmins } from "$lib/forum/groups"; +import { fetchResources } from "$lib/forum/resources"; +import { fetchPartials } from "$lib/forum/partials"; +import type { ForumShell } from "$lib/forum/snapshot"; +import { forumQuery } from "./relay"; + +// Everything the layout needs on every page: the group(s), the admin set, +// sidebar resources and partials. +export async function loadShell(): Promise { + const [group, groups, resources, partials] = await Promise.all([ + MODE === "simple" ? fetchGroup(forumQuery, GROUP_ID) : null, + MODE === "full" ? fetchGroups(forumQuery) : [], + fetchResources(forumQuery), + fetchPartials(forumQuery), + ]); + const adminsByRoom = + MODE === "full" + ? await fetchRoomAdmins( + forumQuery, + groups.map((g) => g.id), + ) + : {}; + const admins = + MODE === "full" + ? [...new Set(Object.values(adminsByRoom).flat())] + : (group?.admins ?? []); + return { group, groups, adminsByRoom, admins, resources, partials }; +} diff --git a/src/lib/textMatch.ts b/src/lib/textMatch.ts new file mode 100644 index 0000000..33962aa --- /dev/null +++ b/src/lib/textMatch.ts @@ -0,0 +1,121 @@ +// Shared matching rules for search highlighting and ranking. The query is +// tokenized on any non-alphanumeric character (so "-_|!./" all act as word +// separators, in the query and in the text), and a token matches where a +// word starts or ends with it — "id" marks "id" or "identify" but never the +// middle of "gravida", while still allowing prefix searches like the first +// characters of an npub. + +export type Span = { start: number; end: number }; + +// Separator between adjacent tokens of a run: one or more non-word chars +const SEP = "[^\\p{L}\\p{N}]+"; + +export function tokenize(query: string): string[] { + return query + .toLowerCase() + .split(/[^\p{L}\p{N}]+/u) + .filter((t) => t.length >= 2); +} + +function isWordChar(ch: string | undefined): boolean { + return ch !== undefined && /[\p{L}\p{N}]/u.test(ch); +} + +// Start-with or end-with: the match must begin at a word start or finish at +// a word end. +function validBoundaries(text: string, span: Span): boolean { + return !isWordChar(text[span.start - 1]) || !isWordChar(text[span.end]); +} + +function overlaps(taken: Span[], s: Span): boolean { + return taken.some((t) => s.start < t.end && s.end > t.start); +} + +// All spans where the given tokens appear consecutively (any separators +// between them). Tokens are alphanumeric-only, so no regex escaping needed. +function findRuns(text: string, tokens: string[]): Span[] { + const re = new RegExp(tokens.join(SEP), "giu"); + const out: Span[] = []; + let m: RegExpExecArray | null; + while ((m = re.exec(text))) { + const span = { start: m.index, end: m.index + m[0].length }; + if (validBoundaries(text, span)) out.push(span); + if (m[0].length === 0) re.lastIndex++; + } + return out; +} + +// Occurrences of the whole query as one contiguous phrase +export function findPhraseSpans(text: string, query: string): Span[] { + const tokens = tokenize(query); + if (tokens.length < 2) return []; + return findRuns(text, tokens); +} + +// Fallback matches: longest contiguous multi-token runs claim their spans +// first (each painted as one block), then single tokens outside them. +export function findMatchSpans(text: string, query: string): Span[] { + const tokens = tokenize(query); + if (tokens.length === 0) return []; + const spans: Span[] = []; + for (let len = tokens.length; len >= 2; len--) { + for (let s = 0; s + len <= tokens.length; s++) { + for (const span of findRuns(text, tokens.slice(s, s + len))) { + if (!overlaps(spans, span)) spans.push(span); + } + } + } + for (const t of new Set(tokens)) { + for (const span of findRuns(text, [t])) { + if (!overlaps(spans, span)) spans.push(span); + } + } + return spans.sort((a, b) => a.start - b.start); +} + +// Rank how well a text matches the query, and where to anchor an excerpt: +// the full query (1000) beats any shorter contiguous run (100 + length), +// which beats scattered singles (count of distinct tokens inside a window of +// `windowSpan` chars, always < 100). +export function bestMatch( + text: string, + query: string, + windowSpan: number, +): { anchor: number; score: number } { + const tokens = tokenize(query); + if (tokens.length === 0) return { anchor: -1, score: 0 }; + + for (let len = tokens.length; len >= 2; len--) { + for (let s = 0; s + len <= tokens.length; s++) { + const runs = findRuns(text, tokens.slice(s, s + len)); + if (runs.length > 0) + return { + anchor: runs[0].start, + score: len === tokens.length ? 1000 : 100 + len, + }; + } + } + + const singles: { start: number; token: string }[] = []; + for (const t of new Set(tokens)) { + for (const span of findRuns(text, [t])) + singles.push({ start: span.start, token: t }); + } + if (singles.length === 0) return { anchor: -1, score: 0 }; + singles.sort((a, b) => a.start - b.start); + + let anchor = singles[0].start; + let score = 0; + for (const o of singles) { + const seen = new Set(); + for (const p of singles) { + if (p.start >= o.start && p.start <= o.start + windowSpan) + seen.add(p.token); + } + if (seen.size > score) { + score = seen.size; + anchor = o.start; + } + } + return { anchor, score }; +} diff --git a/src/lib/thread.svelte.ts b/src/lib/thread.svelte.ts index 0a194a5..829ab9b 100644 --- a/src/lib/thread.svelte.ts +++ b/src/lib/thread.svelte.ts @@ -1,4 +1,5 @@ -import { loadNostrUser, type NostrUser } from "@nostr/gadgets/metadata"; +import { loadNostrUser, type NostrUser } from "$lib/gadgets"; +import { page } from "$app/state"; import { RELAY_URL, GROUP_ID } from "$lib/config"; import { queryForum, publishForum } from "$lib/relay"; import { threads as mockThreads } from "$lib/mock"; @@ -10,40 +11,34 @@ import { buildPTagHints, } from "$lib/mentions"; import { convertForumUrls } from "$lib/linkify"; +import { + fetchThread, + type PostData, + type ThreadDetail, +} from "$lib/forum/thread"; + +export type { PostData }; const isNostrId = (id: string) => /^[0-9a-f]{64}$/.test(id); -export type PostData = { - id: string; - pubkey: string; - createdAt: number; - content: string; -}; - -type ThreadDetail = { - id: string; - title: string; - labels: string[]; - groupId: string; // the thread's NIP-29 group (its `h` tag) - op: PostData; - replies: PostData[]; -}; - let detail = $state(null); let profiles = $state>({}); // "notfound" means the relay returned nothing — either no such thread or it sits // in a private group the current (non-member) user can't read. let status = $state<"loading" | "ready" | "notfound">("loading"); +// Until the live fetch lands, the server snapshot (if any) stands in. Its +// profiles stay as a base layer: the live ones arrive one by one. export const threadDetailStore = { get detail() { - return detail; + return detail ?? page.data.thread ?? null; }, get profiles() { - return profiles; + const base = page.data.profiles; + return base ? { ...base, ...profiles } : profiles; }, get status() { - return status; + return status === "loading" && page.data.thread ? "ready" : status; }, }; @@ -104,39 +99,13 @@ export async function loadThread(id: string) { return; } - const [threadEvents, replyEvents] = await Promise.all([ - queryForum({ kinds: [11], ids: [id] }), - queryForum({ kinds: [1111], "#E": [id] }), - ]); - - const event = threadEvents[0]; - if (!event) { + const next = await fetchThread(queryForum, id, GROUP_ID); + if (!next) { status = "notfound"; return; } - - const replies = replyEvents.sort((a, b) => a.created_at - b.created_at); - - detail = { - id: event.id, - title: event.tags.find((t) => t[0] === "title")?.[1] ?? "(untitled)", - labels: event.tags.filter((t) => t[0] === "t" && t[1]).map((t) => t[1]), - groupId: event.tags.find((t) => t[0] === "h")?.[1] ?? GROUP_ID, - op: { - id: event.id, - pubkey: event.pubkey, - createdAt: event.created_at, - content: event.content, - }, - replies: replies.map((r) => ({ - id: r.id, - pubkey: r.pubkey, - createdAt: r.created_at, - content: r.content, - })), - }; - - [event.pubkey, ...replies.map((r) => r.pubkey)].forEach(loadProfile); + detail = next; + [next.op.pubkey, ...next.replies.map((r) => r.pubkey)].forEach(loadProfile); status = "ready"; } diff --git a/src/lib/threads.svelte.ts b/src/lib/threads.svelte.ts index 9a63419..af32a97 100644 --- a/src/lib/threads.svelte.ts +++ b/src/lib/threads.svelte.ts @@ -1,27 +1,15 @@ -import type { AbstractRelay } from "@nostr/tools/abstract-relay"; -import type { Event } from "@nostr/tools/core"; -import type { Filter } from "@nostr/tools/filter"; -import { loadNostrUser, type NostrUser } from "@nostr/gadgets/metadata"; +import { page } from "$app/state"; +import { loadNostrUser, type NostrUser } from "$lib/gadgets"; import { ensureForumRelay } from "$lib/relay"; import { ingestNostrUser } from "$lib/profiles.svelte"; +import { relayQuery } from "$lib/forum/query"; +import { + fetchThreadPage, + type SortMode, + type ThreadData, +} from "$lib/forum/threads"; -const PAGE_SIZE = 30; -const WALK_LIMIT = 150; // Events per walk query (~5x PAGE_SIZE) -const WALK_MAX_ITERS = 12; - -export type SortMode = "active" | "new"; - -export type ThreadData = { - id: string; - title: string; - labels: string[]; - authorPubkey: string; - createdAt: number; - replyCount: number; - latestAt: number; - latestPubkey: string; - replierPubkeys: string[]; // unique reply authors, excl. OP, max 4 -}; +export type { SortMode, ThreadData }; let threads = $state([]); let profiles = $state>({}); @@ -34,15 +22,34 @@ let cursor: number | null = null; // sort-key value of the last loaded thread let snapshotAt = 0; // upper time bound, frozen at initial load let reqId = 0; // supersedes in-flight loads when the sort/group changes +// Which listing the client asked for, and whether its first page is in. +let requested = $state<{ groupId: string; sort: SortMode } | null>(null); +let settled = $state(false); + +// The server snapshot stands in until the live listing lands, but only for +// the listing that was requested: a different room or sort shows "Loading…". +function fallback() { + const s = page.data.threads; + if (!s) return null; + if ( + requested && + (requested.groupId !== s.groupId || requested.sort !== s.sort) + ) + return null; + return s; +} + export const threadStore = { get threads() { - return threads; + if (settled || threads.length > 0) return threads; + return fallback()?.threads ?? threads; }, get profiles() { - return profiles; + const base = fallback()?.profiles; + return base ? { ...base, ...profiles } : profiles; }, get exhausted() { - return exhausted; + return settled ? exhausted : (fallback()?.done ?? exhausted); }, get loading() { return loading; @@ -59,238 +66,30 @@ async function loadProfile(pubkey: string) { ingestNostrUser(user); } -function querySync(relay: AbstractRelay, filter: Filter): Promise { - return new Promise((resolve) => { - const events: Event[] = []; - const sub = relay.subscribe([filter], { - onevent(e) { - events.push(e); - }, - oneose() { - sub.close(); - resolve(events); - }, - onclose() { - resolve(events); - }, - }); - }); -} - -function threadIdOf(e: Event): string | undefined { - return e.kind === 11 ? e.id : e.tags.find((t) => t[0] === "E")?.[1]; -} - -type SliceItem = { - id: string; - latestAt: number; - latestPubkey: string; - op?: Event; -}; - -// Walk the combined OP + reply stream newest-first, collecting up to `n` unique -// threads not already shown. The first event seen for a thread defines its -// activity timestamp. Returns the slice plus the cursor for the next call. -async function fetchActivitySlice( - relay: AbstractRelay, - groupId: string, - until: number, - exclude: Set, - n: number, -): Promise<{ items: SliceItem[]; nextCursor: number | null; done: boolean }> { - const collected = new Map(); - let cur = until; - let done = false; - - for (let i = 0; i < WALK_MAX_ITERS && collected.size < n; i++) { - const events = await querySync(relay, { - kinds: [11, 1111], - "#h": [groupId], - until: cur, - limit: WALK_LIMIT, - }); - if (events.length === 0) { - done = true; - break; - } - - events.sort((a, b) => b.created_at - a.created_at); - let oldest = cur; - for (const e of events) { - oldest = Math.min(oldest, e.created_at); - const id = threadIdOf(e); - if (!id || exclude.has(id)) continue; - const seen = collected.get(id); - if (seen) { - // Grab the OP from the stream when a thread was first seen via a reply, - // so we avoid the by-id backfill the group relay truncates - if (!seen.op && e.kind === 11) seen.op = e; - continue; - } - if (collected.size >= n) continue; // Page full; keep scanning for OPs - collected.set(id, { - id, - latestAt: e.created_at, - latestPubkey: e.pubkey, - op: e.kind === 11 ? e : undefined, - }); - } - - if (events.length < WALK_LIMIT) { - done = true; - break; - } - if (oldest >= cur) break; // No progress (single timestamp floods the window) - cur = oldest; // Inclusive; thread-level dedupe absorbs re-reads - } - - const items = [...collected.values()].sort((a, b) => b.latestAt - a.latestAt); - const nextCursor = items.length > 0 ? items[items.length - 1].latestAt : null; - return { items, nextCursor, done }; -} - -// Chronological-by-creation slice: just OPs ordered by created_at. No reply -// data is needed to order them (stable cursor), keeping the path cheap; reply -// counts are still attached later via enrichment. -async function fetchNewSlice( - relay: AbstractRelay, - groupId: string, - until: number, - exclude: Set, - n: number, -): Promise<{ items: SliceItem[]; nextCursor: number | null; done: boolean }> { - const ops = await querySync(relay, { - kinds: [11], - "#h": [groupId], - until, - limit: n + 10, // headroom for boundary OPs re-read at the inclusive cursor - }); - ops.sort((a, b) => b.created_at - a.created_at); - const fresh = ops.filter((e) => !exclude.has(e.id)); - const slice = fresh.slice(0, n); - - const items: SliceItem[] = slice.map((op) => ({ - id: op.id, - latestAt: op.created_at, - latestPubkey: op.pubkey, - op, - })); - const done = ops.length < n + 10; - const last = slice[slice.length - 1] ?? ops[ops.length - 1]; - const nextCursor = last ? last.created_at : null; - return { items, nextCursor, done }; -} - -// Reply enrichment (exact counts + sampled repliers), bounded by the frozen -// snapshot. Isolated so the future creation-by-date view can skip it entirely. -async function enrichWithReplies( - relay: AbstractRelay, - groupId: string, - items: SliceItem[], -): Promise> { - const result = new Map(); - if (items.length === 0) return result; - - const replies = await querySync(relay, { - kinds: [1111], - "#h": [groupId], - "#E": items.map((it) => it.id), - until: snapshotAt, - limit: 5000, - }); - - const acc = new Map }>(); - for (const r of replies) { - const root = r.tags.find((t) => t[0] === "E")?.[1]; - if (!root) continue; - let a = acc.get(root); - if (!a) { - a = { count: 0, pubkeys: new Set() }; - acc.set(root, a); - } - a.count++; - a.pubkeys.add(r.pubkey); - } - - for (const it of items) { - const a = acc.get(it.id); - const repliers = a - ? [...a.pubkeys].filter((p) => p !== it.op?.pubkey).slice(0, 4) - : []; - result.set(it.id, { count: a?.count ?? 0, repliers }); - } - return result; -} - -async function buildThreads( - relay: AbstractRelay, - groupId: string, - items: SliceItem[], -): Promise { - // Backfill OPs that fell outside the activity window. The group relay - // truncates multi-id queries, so fetch each one on its own. - const missing = items.filter((it) => !it.op); - if (missing.length > 0) { - const fetched = await Promise.all( - missing.map((it) => - querySync(relay, { kinds: [11], "#h": [groupId], ids: [it.id] }), - ), - ); - const byId = new Map(); - for (const evs of fetched) for (const e of evs) byId.set(e.id, e); - for (const it of items) if (!it.op) it.op = byId.get(it.id); - } - - const enriched = await enrichWithReplies(relay, groupId, items); - - const out: ThreadData[] = []; - for (const it of items) { - const op = it.op; - if (!op) continue; // OP missing (deleted/unavailable) — drop the row - const e = enriched.get(it.id); - out.push({ - id: it.id, - title: op.tags.find((t) => t[0] === "title")?.[1] ?? "(untitled)", - labels: op.tags.filter((t) => t[0] === "t" && t[1]).map((t) => t[1]), - authorPubkey: op.pubkey, - createdAt: op.created_at, - replyCount: e?.count ?? 0, - latestAt: it.latestAt, - latestPubkey: it.latestPubkey, - replierPubkeys: e?.repliers ?? [], - }); - } - return out; -} - async function runLoad(append: boolean, groupId: string) { const id = ++reqId; if (append) loadingMore = true; else loading = true; - - const relay = await ensureForumRelay(); + // Paged loads drive their own subscriptions on the shared connection + const q = relayQuery(await ensureForumRelay()); try { - const until = append ? (cursor ?? snapshotAt) : snapshotAt; - const exclude = new Set(threads.map((t) => t.id)); - const slice = - sortMode === "new" - ? await fetchNewSlice(relay, groupId, until, exclude, PAGE_SIZE) - : await fetchActivitySlice(relay, groupId, until, exclude, PAGE_SIZE); - const built = await buildThreads(relay, groupId, slice.items); - + const page = await fetchThreadPage(q, groupId, { + sort: sortMode, + until: append ? (cursor ?? snapshotAt) : snapshotAt, + snapshotAt, + exclude: new Set(threads.map((t) => t.id)), + }); if (id !== reqId) return; // Superseded by a newer load — discard results - - threads = append ? [...threads, ...built] : built; - cursor = slice.nextCursor ?? cursor; - exhausted = slice.done || built.length === 0; - - for (const t of built) { + threads = append ? [...threads, ...page.threads] : page.threads; + cursor = page.nextCursor ?? cursor; + exhausted = page.done || page.threads.length === 0; + settled = true; + for (const t of page.threads) { loadProfile(t.authorPubkey); loadProfile(t.latestPubkey); for (const p of t.replierPubkeys) loadProfile(p); } } finally { - // shared forum connection is long-lived — don't close it here if (id === reqId) { if (append) loadingMore = false; else loading = false; @@ -300,6 +99,8 @@ async function runLoad(append: boolean, groupId: string) { export async function loadThreads(groupId: string, sort: SortMode = "active") { sortMode = sort; + requested = { groupId, sort }; + settled = false; threads = []; cursor = null; exhausted = false; diff --git a/src/lib/userPosts.svelte.ts b/src/lib/userPosts.svelte.ts new file mode 100644 index 0000000..9f105bd --- /dev/null +++ b/src/lib/userPosts.svelte.ts @@ -0,0 +1,81 @@ +import { GROUP_ID, MODE } from "$lib/config"; +import { queryForum } from "$lib/relay"; +import { + fetchUserPosts, + fetchUserPostPage, + nextCursor, + type UserPostKind, + type UserPosts, +} from "$lib/forum/userPosts"; + +export type { UserPosts, UserPostKind }; + +const scope = () => (MODE === "simple" ? GROUP_ID : undefined); + +let pubkey = $state(null); +let posts = $state(null); +let loadingMore = $state>({ + roots: false, + replies: false, +}); +let initial: Promise | null = null; + +// Posts of the profile last loaded. Consumers check `pubkey` against the +// page's, so a stale result never shows under another user's name. +export const userPostsStore = { + get pubkey() { + return pubkey; + }, + get posts() { + return posts; + }, + get loadingMore() { + return loadingMore; + }, +}; + +export function loadUserPosts(pk: string): Promise { + pubkey = pk; + posts = null; + loadingMore = { roots: false, replies: false }; + initial = (async () => { + let result: UserPosts = { + roots: { items: [], done: true }, + replies: { items: [], done: true }, + }; + try { + result = await fetchUserPosts(queryForum, pk, scope()); + } catch (e) { + console.error("[profile] failed to load posts", e); + } + if (pubkey === pk) posts = result; + })(); + return initial; +} + +// Append the next page of one column. Waits for the initial load when the +// page still shows the server snapshot. +export async function loadMoreUserPosts(kind: UserPostKind): Promise { + const pk = pubkey; + if (!pk || loadingMore[kind]) return; + loadingMore[kind] = true; + try { + await initial; + const current = posts?.[kind]; + if (!posts || !current || current.done || pubkey !== pk) return; + const next = await fetchUserPostPage(queryForum, pk, kind, { + groupId: scope(), + until: nextCursor(current), + exclude: new Set(current.items.map((p) => p.id)), + }); + if (pubkey !== pk) return; + posts[kind] = { + items: [...current.items, ...next.items], + done: next.done, + }; + } catch (e) { + console.error("[profile] failed to load more posts", e); + } finally { + loadingMore[kind] = false; + } +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 8270607..1f245ed 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,6 +1,5 @@ - + -
- (menuOpen = true)} /> + +{#if relayAccess.state !== "open" && relayAccess.state !== "checking"} + +{:else}
- -
(menuOpen = true)} /> +
- - -
- {@render children()} -
-
- {#if showChat} - - (chatExpanded = !chatExpanded)} - mobileActive={mobileView === "chat"} - /> - {:else if showDiscussions} - -
+ + {#if showChat} + + (chatExpanded = !chatExpanded)} + mobileActive={mobileView === "chat"} + /> + {:else if showDiscussions} + + + {/if} +
+ {#if showChat} + {/if}
- {#if showChat} - - {/if} - +{/if} (fetch, "/api/shell"); + if (!browser) + setHeaders({ "cache-control": shell ? CACHE_CONTROL : "no-store" }); + return { shell }; +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 0a006cd..0da9f16 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -7,10 +7,26 @@ import { partialsStore } from "$lib/partials.svelte"; import { MODE, GROUP_ID, SEARCH_ENABLED } from "$lib/config"; import SearchInline from "$lib/components/SearchInline.svelte"; + import Meta, { siteName } from "$lib/components/Meta.svelte"; + import { groupStore } from "$lib/group.svelte"; + import { excerpt } from "$lib/seo"; import type { NostrUser } from "@nostr/gadgets/metadata"; const partial = $derived(partialsStore.get("home")); + const description = $derived( + partial + ? excerpt(partial.content) + : MODE === "simple" + ? (groupStore.data?.about ?? "") + : "", + ); + // A partial's leading image doubles as the social preview. + const image = $derived( + partial?.content.match(/^https?:\/\/\S+\.(?:png|jpe?g|webp|gif)$/im)?.[0] ?? + groupStore.data?.picture, + ); + // Full mode: load per-room activity + recent threads once the rooms are known. $effect(() => { if (MODE !== "full") return; @@ -35,9 +51,11 @@ } - - {MODE === "full" ? "Rooms" : "Discussions"} - + {#snippet pic(a: { name: string; picture?: string })} {#if a.picture} diff --git a/src/routes/+page.ts b/src/routes/+page.ts new file mode 100644 index 0000000..41bda5f --- /dev/null +++ b/src/routes/+page.ts @@ -0,0 +1,20 @@ +import { MODE, GROUP_ID } from "$lib/config"; +import { + snapshot, + type OverviewSnapshot, + type ThreadsSnapshot, +} from "$lib/forum/snapshot"; + +// Simple mode: the single group's listing. Full mode: the room overview. +export async function load({ fetch, url }) { + if (MODE === "simple") { + const sort = url.searchParams.get("sort") === "new" ? "?sort=new" : ""; + const threads = await snapshot( + fetch, + `/api/threads/${GROUP_ID}${sort}`, + ); + return { threads, profiles: threads?.profiles ?? null }; + } + const overview = await snapshot(fetch, "/api/overview"); + return { overview, profiles: overview?.profiles ?? null }; +} diff --git a/src/routes/api/contacts/+server.ts b/src/routes/api/contacts/+server.ts new file mode 100644 index 0000000..c0e7662 --- /dev/null +++ b/src/routes/api/contacts/+server.ts @@ -0,0 +1,11 @@ +import { json } from "@sveltejs/kit"; +import { fetchProfiles } from "$lib/forum/profiles"; +import { snapshotHandler } from "$lib/ssr/endpoint"; +import { loadShell } from "$lib/ssr/shell"; +import { profileQuery } from "$lib/ssr/relay"; +import type { RequestHandler } from "./$types"; + +export const GET: RequestHandler = snapshotHandler(async () => { + const shell = await loadShell(); + return json({ profiles: await fetchProfiles(profileQuery, shell.admins) }); +}); diff --git a/src/routes/api/overview/+server.ts b/src/routes/api/overview/+server.ts new file mode 100644 index 0000000..83a1490 --- /dev/null +++ b/src/routes/api/overview/+server.ts @@ -0,0 +1,22 @@ +import { json } from "@sveltejs/kit"; +import { MODE, GROUP_ID } from "$lib/config"; +import { fetchGroups } from "$lib/forum/groups"; +import { fetchOverview, overviewPubkeys } from "$lib/forum/overview"; +import { fetchProfiles } from "$lib/forum/profiles"; +import type { OverviewSnapshot } from "$lib/forum/snapshot"; +import { snapshotHandler } from "$lib/ssr/endpoint"; +import { forumQuery, profileQuery } from "$lib/ssr/relay"; +import type { RequestHandler } from "./$types"; + +// Room activity and the latest discussions: every room in full mode, the +// single group in simple mode (mirrors LatestDiscussions' own room set). +export const GET: RequestHandler = snapshotHandler(async () => { + const roomIds = + MODE === "full" + ? (await fetchGroups(forumQuery)).map((g) => g.id) + : [GROUP_ID]; + const overview = await fetchOverview(forumQuery, roomIds); + const profiles = await fetchProfiles(profileQuery, overviewPubkeys(overview)); + const body: OverviewSnapshot = { roomIds, ...overview, profiles }; + return json(body); +}); diff --git a/src/routes/api/profile/[id]/+server.ts b/src/routes/api/profile/[id]/+server.ts new file mode 100644 index 0000000..8606bf7 --- /dev/null +++ b/src/routes/api/profile/[id]/+server.ts @@ -0,0 +1,21 @@ +import { error, json } from "@sveltejs/kit"; +import { GROUP_ID, MODE } from "$lib/config"; +import { decodeProfileId, fetchProfiles } from "$lib/forum/profiles"; +import { fetchUserPosts } from "$lib/forum/userPosts"; +import { snapshotHandler } from "$lib/ssr/endpoint"; +import { forumQuery, profileQuery } from "$lib/ssr/relay"; +import type { RequestHandler } from "./$types"; + +export const GET: RequestHandler = snapshotHandler(async ({ params }) => { + const pubkey = decodeProfileId(params.id); + if (!pubkey) error(404, "Not found"); + const [profiles, posts] = await Promise.all([ + fetchProfiles(profileQuery, [pubkey]), + fetchUserPosts( + forumQuery, + pubkey, + MODE === "simple" ? GROUP_ID : undefined, + ), + ]); + return json({ pubkey, posts, profiles }); +}); diff --git a/src/routes/api/shell/+server.ts b/src/routes/api/shell/+server.ts new file mode 100644 index 0000000..e78c805 --- /dev/null +++ b/src/routes/api/shell/+server.ts @@ -0,0 +1,8 @@ +import { json } from "@sveltejs/kit"; +import { snapshotHandler } from "$lib/ssr/endpoint"; +import { loadShell } from "$lib/ssr/shell"; +import type { RequestHandler } from "./$types"; + +export const GET: RequestHandler = snapshotHandler(async () => + json(await loadShell()), +); diff --git a/src/routes/api/thread/[id]/+server.ts b/src/routes/api/thread/[id]/+server.ts new file mode 100644 index 0000000..64f37fc --- /dev/null +++ b/src/routes/api/thread/[id]/+server.ts @@ -0,0 +1,15 @@ +import { error, json } from "@sveltejs/kit"; +import { GROUP_ID } from "$lib/config"; +import { fetchThread, threadAuthors } from "$lib/forum/thread"; +import { fetchProfiles } from "$lib/forum/profiles"; +import { snapshotHandler } from "$lib/ssr/endpoint"; +import { forumQuery, profileQuery } from "$lib/ssr/relay"; +import type { RequestHandler } from "./$types"; + +export const GET: RequestHandler = snapshotHandler(async ({ params }) => { + if (!/^[0-9a-f]{64}$/.test(params.id)) error(404, "Not found"); + const thread = await fetchThread(forumQuery, params.id, GROUP_ID); + if (!thread) error(404, "Not found"); + const profiles = await fetchProfiles(profileQuery, threadAuthors(thread)); + return json({ thread, profiles }); +}); diff --git a/src/routes/api/threads/[group]/+server.ts b/src/routes/api/threads/[group]/+server.ts new file mode 100644 index 0000000..61b886a --- /dev/null +++ b/src/routes/api/threads/[group]/+server.ts @@ -0,0 +1,45 @@ +import { error, json } from "@sveltejs/kit"; +import { MODE, GROUP_ID } from "$lib/config"; +import { fetchGroups } from "$lib/forum/groups"; +import { + fetchThreadPage, + threadPagePubkeys, + type SortMode, +} from "$lib/forum/threads"; +import { fetchProfiles } from "$lib/forum/profiles"; +import type { ThreadsSnapshot } from "$lib/forum/snapshot"; +import { snapshotHandler } from "$lib/ssr/endpoint"; +import { forumQuery, profileQuery } from "$lib/ssr/relay"; +import type { RequestHandler } from "./$types"; + +// First page of a group's listing. Unknown rooms are a 404 so crawlers don't +// index empty shells; private rooms are simply empty to the anonymous server. +export const GET: RequestHandler = snapshotHandler(async ({ params, url }) => { + const groupId = params.group; + if (MODE === "simple") { + if (groupId !== GROUP_ID) error(404, "Not found"); + } else { + const groups = await fetchGroups(forumQuery); + if (!groups.some((g) => g.id === groupId)) error(404, "Not found"); + } + const sort: SortMode = + url.searchParams.get("sort") === "new" ? "new" : "active"; + const snapshotAt = Math.floor(Date.now() / 1000); + const page = await fetchThreadPage(forumQuery, groupId, { + sort, + until: snapshotAt, + snapshotAt, + }); + const profiles = await fetchProfiles( + profileQuery, + threadPagePubkeys(page.threads), + ); + const body: ThreadsSnapshot = { + groupId, + sort, + snapshotAt, + ...page, + profiles, + }; + return json(body); +}); diff --git a/src/routes/contacts/+page.svelte b/src/routes/contacts/+page.svelte index b07c698..c73aa07 100644 --- a/src/routes/contacts/+page.svelte +++ b/src/routes/contacts/+page.svelte @@ -11,10 +11,14 @@ import { profileStore, ensureProfile, + entryFromUser, type ProfileEntry, } from "$lib/profiles.svelte"; + import { profilePath } from "$lib/forum/profiles"; import { partialsStore } from "$lib/partials.svelte"; import PostContent from "$lib/components/PostContent.svelte"; + import { page } from "$app/state"; + import Meta from "$lib/components/Meta.svelte"; const partial = $derived(partialsStore.get("contacts")); @@ -48,11 +52,17 @@ const contacts = $derived( adminPubkeys.list.map((pk) => { - const entry = profileStore.profiles.get(pk); + const entry = profileStore.profiles.get(pk) ?? snapshotEntry(pk); return { pubkey: pk, npub: entry?.npub ?? nip19.npubEncode(pk), entry }; }), ); + // Server-rendered profile, used until the live one is fetched. + function snapshotEntry(pk: string): ProfileEntry | undefined { + const u = page.data.profiles?.[pk]; + return u ? entryFromUser(u) : undefined; + } + function displayName(c: Contact): string { return c.entry?.displayName || c.entry?.name || `${c.npub.slice(0, 12)}…`; } @@ -66,10 +76,7 @@ } - - Contacts - - +

Contacts

@@ -81,34 +88,49 @@ {/if} {#if loading} -

Loading…

+

+ Loading… +

{:else if contacts.length === 0} -

No admins listed.

+

+ No admins listed. +

{:else}