An easy to set up and operate PDS admin panel.
59

Configure Feed

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

first commit

Brooke (Jul 10, 2026, 10:54 AM -0700) 41a5d35f

+8274
+7
.dockerignore
··· 1 + **/node_modules 2 + **/dist 3 + server/.env 4 + server/data.sqlite* 5 + server/audit.log 6 + docs 7 + .git
+31
.env.example
··· 1 + # PDS this dashboard manages 2 + PDS_HOSTNAME=pds.example.com 3 + PDS_ADMIN_PASSWORD=changeme 4 + 5 + # Relay used for sync status / requestCrawl 6 + RELAY_HOSTNAME=bsky.network 7 + 8 + # Dashboard's own operator login (bcrypt hash, generate with: 9 + # node -e "console.log(require('bcryptjs').hashSync('<password>', 10))" ) 10 + # Optional: leave unset for passkey-only sign-in (enroll via: npm run enroll) 11 + OPERATOR_PASSWORD_HASH=changeme 12 + # openssl rand -hex 32 — production refuses to boot on a weak value 13 + SESSION_SECRET=changeme 14 + 15 + PORT=8787 16 + 17 + # In production the server binds to 127.0.0.1 (reverse-proxy only); override if needed 18 + # HOST=127.0.0.1 19 + 20 + # Web client used for profile links in the UI 21 + APPVIEW_URL=https://bsky.app 22 + 23 + # Flag notifications via Bluesky DM — fill all four to enable. 24 + # NOTIFY_APP_PASSWORD must be an app password created WITH direct-message access. 25 + NOTIFY_HANDLE= 26 + NOTIFY_APP_PASSWORD= 27 + NOTIFY_RECIPIENT= 28 + DASHBOARD_URL=http://localhost:5173 29 + 30 + # Audit log location (defaults to server/audit.log) 31 + # AUDIT_LOG_PATH=/var/log/pds-operator/audit.log
+10
.gitignore
··· 1 + node_modules/ 2 + dist/ 3 + .env 4 + *.log 5 + *.tsbuildinfo 6 + audit.log 7 + data.sqlite 8 + data.sqlite-shm 9 + data.sqlite-wal 10 + server/labelers.json
+28
Dockerfile
··· 1 + FROM node:22-slim AS web 2 + WORKDIR /app/web 3 + COPY web/package*.json ./ 4 + RUN npm ci 5 + COPY web/ ./ 6 + RUN npm run build 7 + 8 + FROM node:22-slim AS server 9 + WORKDIR /app/server 10 + # toolchain in case better-sqlite3 has no prebuild for this platform 11 + RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ && rm -rf /var/lib/apt/lists/* 12 + COPY server/package*.json ./ 13 + RUN npm ci 14 + COPY server/ ./ 15 + RUN npm run build && npm prune --omit=dev 16 + # labelers.json is gitignored instance config; fresh clones build with the example 17 + RUN [ -f labelers.json ] || cp labelers.json.example labelers.json 18 + 19 + FROM node:22-slim 20 + ENV NODE_ENV=production 21 + WORKDIR /app/server 22 + COPY --from=server /app/server/node_modules ./node_modules 23 + COPY --from=server /app/server/dist ./dist 24 + COPY --from=server /app/server/package.json ./package.json 25 + COPY --from=server /app/server/labelers.json ./labelers.json 26 + COPY --from=web /app/web/dist /app/web/dist 27 + EXPOSE 8787 28 + CMD ["node", "dist/index.js"]
+21
LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2026 Brooke 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+104
README.md
··· 1 + # PDS Operator 2 + 3 + Self-hosted admin dashboard for an [atproto](https://atproto.com) PDS: account 4 + list/search/takedown/enable/password-reset, moderation-labeler flags with Bluesky DM 5 + alerts, relay sync status, invite codes, passkey sign-in, and an audit log. Talks to the 6 + PDS and relay only over their public HTTPS APIs, no SSH/DB access required. 7 + 8 + ![PDS Operator](docs/screenshot-default.png) 9 + 10 + <p> 11 + <img src="docs/screenshot.png" width="49%" alt="flagged accounts view" /> 12 + <img src="docs/login.png" width="49%" alt="login with passkey" /> 13 + </p> 14 + 15 + ![invite codes, passkeys, and audit log](docs/screenshot-panels.png) 16 + 17 + ## How it works 18 + 19 + The server keeps a local SQLite copy of everything in `server/data.sqlite`. A background 20 + syncer does a full pass against the PDS on startup and every 15 minutes. Between passes 21 + it stays subscribed to the PDS firehose and each labeler's label stream, so handle 22 + changes, takedowns, and new labels land within seconds. Stream cursors survive restarts, 23 + and dead sockets are detected and reconnected. Requests to outside services (avatars, 24 + label backfill) are throttled and retried with backoff when rate limited. The SQLite 25 + file is a rebuildable cache. The only data worth backing up is `server/audit.log`. 26 + 27 + ## Setup 28 + 29 + ``` 30 + cd web && npm install 31 + cd ../server && npm install 32 + npm run setup # interactive, writes server/.env 33 + ``` 34 + 35 + The wizard asks for the PDS, relay, and DM alert config, generates a session secret, and 36 + can start the server and mint a passkey enrollment link in one go. It can also generate 37 + a fly.toml and print the fly.io deploy commands (optional, any host works). The operator password is optional. Leave it empty 38 + for passkey-only sign-in, where enrollment links from the CLI are the only way in: 39 + 40 + ``` 41 + npm run enroll # prints a single-use link + QR code, expires in 15 minutes 42 + ``` 43 + 44 + Run it again any time to enroll another device, or as recovery if all passkeys are lost 45 + (shell access is the break-glass). To configure by hand instead, copy `.env.example` to 46 + `server/.env` and generate the hash with 47 + `node -e "console.log(require('bcryptjs').hashSync('<password>', 10))"`. 48 + 49 + ### Labelers 50 + 51 + `server/labelers.json` (gitignored, copy `server/labelers.json.example`): 52 + 53 + ```json 54 + [ 55 + { 56 + "name": "skywatch blue", 57 + "did": "did:plc:e4elbtctnfqocyfcml6h2lf7", 58 + "labels": ["platform-manipulation", "engagement-abuse"] 59 + } 60 + ] 61 + ``` 62 + 63 + `labels` is the watchlist of label values that flag an account. Empty or omitted means 64 + every label from that labeler counts. The endpoint is resolved from the labeler's DID 65 + document and label names come from its published definitions. Changing the file requires 66 + a restart. 67 + 68 + Ozone quirk: `queryLabels` silently returns nothing past 20 `uriPatterns` per request, 69 + so backfill batches are capped at 20. 70 + 71 + ### DM alerts 72 + 73 + Set the four `NOTIFY_*` / `DASHBOARD_URL` values in `server/.env`: sender handle, an app 74 + password created **with** DM access, recipient handle, and the dashboard URL for deep 75 + links. The recipient has to accept DMs from the sender (follow them, or allow DMs from 76 + everyone). Unset = disabled. 77 + 78 + ## Dev 79 + 80 + ``` 81 + cd web && npm run dev # vite dev server, proxies /api to :8787 82 + cd server && npm run dev # fastify API on :8787 83 + ``` 84 + 85 + ## Production 86 + 87 + ``` 88 + cd web && npm run build # writes web/dist 89 + cd server && npm run build && NODE_ENV=production npm start 90 + ``` 91 + 92 + The server serves `web/dist` alongside the API, so only the server process runs in 93 + production. 94 + 95 + - Run behind a TLS-terminating reverse proxy. HTTPS is required for passkeys (WebAuthn 96 + won't run in a non-secure context), and passkeys are per-hostname, so enroll fresh 97 + ones on the production domain. 98 + - `NODE_ENV=production` enables the secure session cookie, binds to 127.0.0.1 (override 99 + with `HOST`), and refuses weak `SESSION_SECRET`s (`openssl rand -hex 32`). 100 + - `trustProxy` is on: `req.ip` (login rate limiting) comes from X-Forwarded-For. Correct 101 + behind the proxy, spoofable if you expose the port directly. 102 + - Set `DASHBOARD_URL` to the real URL and `chmod 600 server/.env`. 103 + - Back up `server/audit.log` (`AUDIT_LOG_PATH` overrides the location). 104 + - Run under a process supervisor. Sessions are in-memory, so a restart signs you out.
docs/login.png

This is a binary file and will not be displayed.

docs/screenshot-default.png

This is a binary file and will not be displayed.

docs/screenshot-panels.png

This is a binary file and will not be displayed.

docs/screenshot.png

This is a binary file and will not be displayed.

+30
fly.toml
··· 1 + app = "pckt-pds-operator" 2 + primary_region = "sjc" 3 + 4 + [build] 5 + 6 + [env] 7 + HOST = "0.0.0.0" 8 + PORT = "8787" 9 + PDS_HOSTNAME = "pds.pckt.cafe" 10 + RELAY_HOSTNAME = "bsky.network" 11 + APPVIEW_URL = "https://bsky.app" 12 + DASHBOARD_URL = "https://operator.pckt.cafe" 13 + DB_PATH = "/data/data.sqlite" 14 + AUDIT_LOG_PATH = "/data/audit.log" 15 + 16 + [mounts] 17 + source = "data" 18 + destination = "/data" 19 + 20 + [http_service] 21 + internal_port = 8787 22 + force_https = true 23 + # the syncer's streams and DM alerts need the machine always on 24 + auto_stop_machines = "off" 25 + auto_start_machines = true 26 + min_machines_running = 1 27 + 28 + [[vm]] 29 + size = "shared-cpu-1x" 30 + memory = "512mb"
+15
server/labelers.json.example
··· 1 + [ 2 + { 3 + "name": "skywatch blue", 4 + "did": "did:plc:e4elbtctnfqocyfcml6h2lf7", 5 + "labels": [ 6 + "platform-manipulation", 7 + "suspect-inauthentic", 8 + "nazi-symbolism", 9 + "inauthentic-fundraising", 10 + "engagement-abuse", 11 + "follow-farming", 12 + "bulk-following" 13 + ] 14 + } 15 + ]
+2630
server/package-lock.json
··· 1 + { 2 + "name": "pds-operator-server", 3 + "version": "0.1.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "pds-operator-server", 9 + "version": "0.1.0", 10 + "dependencies": { 11 + "@fastify/cookie": "^11.0.2", 12 + "@fastify/helmet": "^13.1.0", 13 + "@fastify/session": "^11.1.0", 14 + "@fastify/static": "^8.1.1", 15 + "@simplewebauthn/server": "^13.3.2", 16 + "bcryptjs": "^3.0.2", 17 + "better-sqlite3": "^12.11.1", 18 + "cbor-x": "^1.6.0", 19 + "dotenv": "^16.4.7", 20 + "fastify": "^5.2.1", 21 + "qrcode-terminal": "^0.12.0", 22 + "ws": "^8.18.0" 23 + }, 24 + "devDependencies": { 25 + "@types/bcryptjs": "^2.4.6", 26 + "@types/better-sqlite3": "^7.6.13", 27 + "@types/node": "^22.13.4", 28 + "@types/qrcode-terminal": "^0.12.2", 29 + "@types/ws": "^8.5.14", 30 + "tsx": "^4.19.2", 31 + "typescript": "^5.7.3" 32 + } 33 + }, 34 + "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { 35 + "version": "2.2.2", 36 + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.2.tgz", 37 + "integrity": "sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==", 38 + "cpu": [ 39 + "arm64" 40 + ], 41 + "license": "MIT", 42 + "optional": true, 43 + "os": [ 44 + "darwin" 45 + ] 46 + }, 47 + "node_modules/@cbor-extract/cbor-extract-darwin-x64": { 48 + "version": "2.2.2", 49 + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.2.tgz", 50 + "integrity": "sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==", 51 + "cpu": [ 52 + "x64" 53 + ], 54 + "license": "MIT", 55 + "optional": true, 56 + "os": [ 57 + "darwin" 58 + ] 59 + }, 60 + "node_modules/@cbor-extract/cbor-extract-linux-arm": { 61 + "version": "2.2.2", 62 + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.2.tgz", 63 + "integrity": "sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==", 64 + "cpu": [ 65 + "arm" 66 + ], 67 + "license": "MIT", 68 + "optional": true, 69 + "os": [ 70 + "linux" 71 + ] 72 + }, 73 + "node_modules/@cbor-extract/cbor-extract-linux-arm64": { 74 + "version": "2.2.2", 75 + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.2.tgz", 76 + "integrity": "sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==", 77 + "cpu": [ 78 + "arm64" 79 + ], 80 + "license": "MIT", 81 + "optional": true, 82 + "os": [ 83 + "linux" 84 + ] 85 + }, 86 + "node_modules/@cbor-extract/cbor-extract-linux-x64": { 87 + "version": "2.2.2", 88 + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.2.tgz", 89 + "integrity": "sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==", 90 + "cpu": [ 91 + "x64" 92 + ], 93 + "license": "MIT", 94 + "optional": true, 95 + "os": [ 96 + "linux" 97 + ] 98 + }, 99 + "node_modules/@cbor-extract/cbor-extract-win32-x64": { 100 + "version": "2.2.2", 101 + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.2.tgz", 102 + "integrity": "sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==", 103 + "cpu": [ 104 + "x64" 105 + ], 106 + "license": "MIT", 107 + "optional": true, 108 + "os": [ 109 + "win32" 110 + ] 111 + }, 112 + "node_modules/@esbuild/aix-ppc64": { 113 + "version": "0.28.1", 114 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", 115 + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", 116 + "cpu": [ 117 + "ppc64" 118 + ], 119 + "dev": true, 120 + "license": "MIT", 121 + "optional": true, 122 + "os": [ 123 + "aix" 124 + ], 125 + "engines": { 126 + "node": ">=18" 127 + } 128 + }, 129 + "node_modules/@esbuild/android-arm": { 130 + "version": "0.28.1", 131 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", 132 + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", 133 + "cpu": [ 134 + "arm" 135 + ], 136 + "dev": true, 137 + "license": "MIT", 138 + "optional": true, 139 + "os": [ 140 + "android" 141 + ], 142 + "engines": { 143 + "node": ">=18" 144 + } 145 + }, 146 + "node_modules/@esbuild/android-arm64": { 147 + "version": "0.28.1", 148 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", 149 + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", 150 + "cpu": [ 151 + "arm64" 152 + ], 153 + "dev": true, 154 + "license": "MIT", 155 + "optional": true, 156 + "os": [ 157 + "android" 158 + ], 159 + "engines": { 160 + "node": ">=18" 161 + } 162 + }, 163 + "node_modules/@esbuild/android-x64": { 164 + "version": "0.28.1", 165 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", 166 + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", 167 + "cpu": [ 168 + "x64" 169 + ], 170 + "dev": true, 171 + "license": "MIT", 172 + "optional": true, 173 + "os": [ 174 + "android" 175 + ], 176 + "engines": { 177 + "node": ">=18" 178 + } 179 + }, 180 + "node_modules/@esbuild/darwin-arm64": { 181 + "version": "0.28.1", 182 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", 183 + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", 184 + "cpu": [ 185 + "arm64" 186 + ], 187 + "dev": true, 188 + "license": "MIT", 189 + "optional": true, 190 + "os": [ 191 + "darwin" 192 + ], 193 + "engines": { 194 + "node": ">=18" 195 + } 196 + }, 197 + "node_modules/@esbuild/darwin-x64": { 198 + "version": "0.28.1", 199 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", 200 + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", 201 + "cpu": [ 202 + "x64" 203 + ], 204 + "dev": true, 205 + "license": "MIT", 206 + "optional": true, 207 + "os": [ 208 + "darwin" 209 + ], 210 + "engines": { 211 + "node": ">=18" 212 + } 213 + }, 214 + "node_modules/@esbuild/freebsd-arm64": { 215 + "version": "0.28.1", 216 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", 217 + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", 218 + "cpu": [ 219 + "arm64" 220 + ], 221 + "dev": true, 222 + "license": "MIT", 223 + "optional": true, 224 + "os": [ 225 + "freebsd" 226 + ], 227 + "engines": { 228 + "node": ">=18" 229 + } 230 + }, 231 + "node_modules/@esbuild/freebsd-x64": { 232 + "version": "0.28.1", 233 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", 234 + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", 235 + "cpu": [ 236 + "x64" 237 + ], 238 + "dev": true, 239 + "license": "MIT", 240 + "optional": true, 241 + "os": [ 242 + "freebsd" 243 + ], 244 + "engines": { 245 + "node": ">=18" 246 + } 247 + }, 248 + "node_modules/@esbuild/linux-arm": { 249 + "version": "0.28.1", 250 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", 251 + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", 252 + "cpu": [ 253 + "arm" 254 + ], 255 + "dev": true, 256 + "license": "MIT", 257 + "optional": true, 258 + "os": [ 259 + "linux" 260 + ], 261 + "engines": { 262 + "node": ">=18" 263 + } 264 + }, 265 + "node_modules/@esbuild/linux-arm64": { 266 + "version": "0.28.1", 267 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", 268 + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", 269 + "cpu": [ 270 + "arm64" 271 + ], 272 + "dev": true, 273 + "license": "MIT", 274 + "optional": true, 275 + "os": [ 276 + "linux" 277 + ], 278 + "engines": { 279 + "node": ">=18" 280 + } 281 + }, 282 + "node_modules/@esbuild/linux-ia32": { 283 + "version": "0.28.1", 284 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", 285 + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", 286 + "cpu": [ 287 + "ia32" 288 + ], 289 + "dev": true, 290 + "license": "MIT", 291 + "optional": true, 292 + "os": [ 293 + "linux" 294 + ], 295 + "engines": { 296 + "node": ">=18" 297 + } 298 + }, 299 + "node_modules/@esbuild/linux-loong64": { 300 + "version": "0.28.1", 301 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", 302 + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", 303 + "cpu": [ 304 + "loong64" 305 + ], 306 + "dev": true, 307 + "license": "MIT", 308 + "optional": true, 309 + "os": [ 310 + "linux" 311 + ], 312 + "engines": { 313 + "node": ">=18" 314 + } 315 + }, 316 + "node_modules/@esbuild/linux-mips64el": { 317 + "version": "0.28.1", 318 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", 319 + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", 320 + "cpu": [ 321 + "mips64el" 322 + ], 323 + "dev": true, 324 + "license": "MIT", 325 + "optional": true, 326 + "os": [ 327 + "linux" 328 + ], 329 + "engines": { 330 + "node": ">=18" 331 + } 332 + }, 333 + "node_modules/@esbuild/linux-ppc64": { 334 + "version": "0.28.1", 335 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", 336 + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", 337 + "cpu": [ 338 + "ppc64" 339 + ], 340 + "dev": true, 341 + "license": "MIT", 342 + "optional": true, 343 + "os": [ 344 + "linux" 345 + ], 346 + "engines": { 347 + "node": ">=18" 348 + } 349 + }, 350 + "node_modules/@esbuild/linux-riscv64": { 351 + "version": "0.28.1", 352 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", 353 + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", 354 + "cpu": [ 355 + "riscv64" 356 + ], 357 + "dev": true, 358 + "license": "MIT", 359 + "optional": true, 360 + "os": [ 361 + "linux" 362 + ], 363 + "engines": { 364 + "node": ">=18" 365 + } 366 + }, 367 + "node_modules/@esbuild/linux-s390x": { 368 + "version": "0.28.1", 369 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", 370 + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", 371 + "cpu": [ 372 + "s390x" 373 + ], 374 + "dev": true, 375 + "license": "MIT", 376 + "optional": true, 377 + "os": [ 378 + "linux" 379 + ], 380 + "engines": { 381 + "node": ">=18" 382 + } 383 + }, 384 + "node_modules/@esbuild/linux-x64": { 385 + "version": "0.28.1", 386 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", 387 + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", 388 + "cpu": [ 389 + "x64" 390 + ], 391 + "dev": true, 392 + "license": "MIT", 393 + "optional": true, 394 + "os": [ 395 + "linux" 396 + ], 397 + "engines": { 398 + "node": ">=18" 399 + } 400 + }, 401 + "node_modules/@esbuild/netbsd-arm64": { 402 + "version": "0.28.1", 403 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", 404 + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", 405 + "cpu": [ 406 + "arm64" 407 + ], 408 + "dev": true, 409 + "license": "MIT", 410 + "optional": true, 411 + "os": [ 412 + "netbsd" 413 + ], 414 + "engines": { 415 + "node": ">=18" 416 + } 417 + }, 418 + "node_modules/@esbuild/netbsd-x64": { 419 + "version": "0.28.1", 420 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", 421 + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", 422 + "cpu": [ 423 + "x64" 424 + ], 425 + "dev": true, 426 + "license": "MIT", 427 + "optional": true, 428 + "os": [ 429 + "netbsd" 430 + ], 431 + "engines": { 432 + "node": ">=18" 433 + } 434 + }, 435 + "node_modules/@esbuild/openbsd-arm64": { 436 + "version": "0.28.1", 437 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", 438 + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", 439 + "cpu": [ 440 + "arm64" 441 + ], 442 + "dev": true, 443 + "license": "MIT", 444 + "optional": true, 445 + "os": [ 446 + "openbsd" 447 + ], 448 + "engines": { 449 + "node": ">=18" 450 + } 451 + }, 452 + "node_modules/@esbuild/openbsd-x64": { 453 + "version": "0.28.1", 454 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", 455 + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", 456 + "cpu": [ 457 + "x64" 458 + ], 459 + "dev": true, 460 + "license": "MIT", 461 + "optional": true, 462 + "os": [ 463 + "openbsd" 464 + ], 465 + "engines": { 466 + "node": ">=18" 467 + } 468 + }, 469 + "node_modules/@esbuild/openharmony-arm64": { 470 + "version": "0.28.1", 471 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", 472 + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", 473 + "cpu": [ 474 + "arm64" 475 + ], 476 + "dev": true, 477 + "license": "MIT", 478 + "optional": true, 479 + "os": [ 480 + "openharmony" 481 + ], 482 + "engines": { 483 + "node": ">=18" 484 + } 485 + }, 486 + "node_modules/@esbuild/sunos-x64": { 487 + "version": "0.28.1", 488 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", 489 + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", 490 + "cpu": [ 491 + "x64" 492 + ], 493 + "dev": true, 494 + "license": "MIT", 495 + "optional": true, 496 + "os": [ 497 + "sunos" 498 + ], 499 + "engines": { 500 + "node": ">=18" 501 + } 502 + }, 503 + "node_modules/@esbuild/win32-arm64": { 504 + "version": "0.28.1", 505 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", 506 + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", 507 + "cpu": [ 508 + "arm64" 509 + ], 510 + "dev": true, 511 + "license": "MIT", 512 + "optional": true, 513 + "os": [ 514 + "win32" 515 + ], 516 + "engines": { 517 + "node": ">=18" 518 + } 519 + }, 520 + "node_modules/@esbuild/win32-ia32": { 521 + "version": "0.28.1", 522 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", 523 + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", 524 + "cpu": [ 525 + "ia32" 526 + ], 527 + "dev": true, 528 + "license": "MIT", 529 + "optional": true, 530 + "os": [ 531 + "win32" 532 + ], 533 + "engines": { 534 + "node": ">=18" 535 + } 536 + }, 537 + "node_modules/@esbuild/win32-x64": { 538 + "version": "0.28.1", 539 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", 540 + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", 541 + "cpu": [ 542 + "x64" 543 + ], 544 + "dev": true, 545 + "license": "MIT", 546 + "optional": true, 547 + "os": [ 548 + "win32" 549 + ], 550 + "engines": { 551 + "node": ">=18" 552 + } 553 + }, 554 + "node_modules/@fastify/accept-negotiator": { 555 + "version": "2.0.1", 556 + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", 557 + "integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==", 558 + "funding": [ 559 + { 560 + "type": "github", 561 + "url": "https://github.com/sponsors/fastify" 562 + }, 563 + { 564 + "type": "opencollective", 565 + "url": "https://opencollective.com/fastify" 566 + } 567 + ], 568 + "license": "MIT" 569 + }, 570 + "node_modules/@fastify/ajv-compiler": { 571 + "version": "4.0.5", 572 + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", 573 + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", 574 + "funding": [ 575 + { 576 + "type": "github", 577 + "url": "https://github.com/sponsors/fastify" 578 + }, 579 + { 580 + "type": "opencollective", 581 + "url": "https://opencollective.com/fastify" 582 + } 583 + ], 584 + "license": "MIT", 585 + "dependencies": { 586 + "ajv": "^8.12.0", 587 + "ajv-formats": "^3.0.1", 588 + "fast-uri": "^3.0.0" 589 + } 590 + }, 591 + "node_modules/@fastify/cookie": { 592 + "version": "11.1.1", 593 + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.1.1.tgz", 594 + "integrity": "sha512-sJ0NXzGVYjUB4OynPZRsIcQ1mKSP4rW45xLCN0aelRq5Vl37xVVbz5kJ6Y0a9m2T0mCUjYCuvlUA9QlTafrZWw==", 595 + "funding": [ 596 + { 597 + "type": "github", 598 + "url": "https://github.com/sponsors/fastify" 599 + }, 600 + { 601 + "type": "opencollective", 602 + "url": "https://opencollective.com/fastify" 603 + } 604 + ], 605 + "license": "MIT", 606 + "dependencies": { 607 + "cookie": "^2.0.0", 608 + "fastify-plugin": "^6.0.0" 609 + } 610 + }, 611 + "node_modules/@fastify/error": { 612 + "version": "4.2.0", 613 + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", 614 + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", 615 + "funding": [ 616 + { 617 + "type": "github", 618 + "url": "https://github.com/sponsors/fastify" 619 + }, 620 + { 621 + "type": "opencollective", 622 + "url": "https://opencollective.com/fastify" 623 + } 624 + ], 625 + "license": "MIT" 626 + }, 627 + "node_modules/@fastify/fast-json-stringify-compiler": { 628 + "version": "5.1.0", 629 + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz", 630 + "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==", 631 + "funding": [ 632 + { 633 + "type": "github", 634 + "url": "https://github.com/sponsors/fastify" 635 + }, 636 + { 637 + "type": "opencollective", 638 + "url": "https://opencollective.com/fastify" 639 + } 640 + ], 641 + "license": "MIT", 642 + "dependencies": { 643 + "fast-json-stringify": "^7.0.0" 644 + } 645 + }, 646 + "node_modules/@fastify/forwarded": { 647 + "version": "3.0.1", 648 + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", 649 + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", 650 + "funding": [ 651 + { 652 + "type": "github", 653 + "url": "https://github.com/sponsors/fastify" 654 + }, 655 + { 656 + "type": "opencollective", 657 + "url": "https://opencollective.com/fastify" 658 + } 659 + ], 660 + "license": "MIT" 661 + }, 662 + "node_modules/@fastify/helmet": { 663 + "version": "13.1.0", 664 + "resolved": "https://registry.npmjs.org/@fastify/helmet/-/helmet-13.1.0.tgz", 665 + "integrity": "sha512-SvVOU0IrzYJW1BvSkfq9G1WUdW3dnaRUvg6m0BtgGMBmML62No0VmSu087jecH58SFbicbREgZTPJ89mAguupA==", 666 + "funding": [ 667 + { 668 + "type": "github", 669 + "url": "https://github.com/sponsors/fastify" 670 + }, 671 + { 672 + "type": "opencollective", 673 + "url": "https://opencollective.com/fastify" 674 + } 675 + ], 676 + "license": "MIT", 677 + "dependencies": { 678 + "fastify-plugin": "^6.0.0", 679 + "helmet": "^8.0.0" 680 + } 681 + }, 682 + "node_modules/@fastify/merge-json-schemas": { 683 + "version": "0.2.1", 684 + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", 685 + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", 686 + "funding": [ 687 + { 688 + "type": "github", 689 + "url": "https://github.com/sponsors/fastify" 690 + }, 691 + { 692 + "type": "opencollective", 693 + "url": "https://opencollective.com/fastify" 694 + } 695 + ], 696 + "license": "MIT", 697 + "dependencies": { 698 + "dequal": "^2.0.3" 699 + } 700 + }, 701 + "node_modules/@fastify/proxy-addr": { 702 + "version": "5.1.0", 703 + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", 704 + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", 705 + "funding": [ 706 + { 707 + "type": "github", 708 + "url": "https://github.com/sponsors/fastify" 709 + }, 710 + { 711 + "type": "opencollective", 712 + "url": "https://opencollective.com/fastify" 713 + } 714 + ], 715 + "license": "MIT", 716 + "dependencies": { 717 + "@fastify/forwarded": "^3.0.0", 718 + "ipaddr.js": "^2.1.0" 719 + } 720 + }, 721 + "node_modules/@fastify/send": { 722 + "version": "4.1.0", 723 + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz", 724 + "integrity": "sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==", 725 + "funding": [ 726 + { 727 + "type": "github", 728 + "url": "https://github.com/sponsors/fastify" 729 + }, 730 + { 731 + "type": "opencollective", 732 + "url": "https://opencollective.com/fastify" 733 + } 734 + ], 735 + "license": "MIT", 736 + "dependencies": { 737 + "@lukeed/ms": "^2.0.2", 738 + "escape-html": "~1.0.3", 739 + "fast-decode-uri-component": "^1.0.1", 740 + "http-errors": "^2.0.0", 741 + "mime": "^3" 742 + } 743 + }, 744 + "node_modules/@fastify/session": { 745 + "version": "11.1.1", 746 + "resolved": "https://registry.npmjs.org/@fastify/session/-/session-11.1.1.tgz", 747 + "integrity": "sha512-nuKwTHxh3eJsI4NJeXoYVGzXUsg+kH1WfHgS7IofVyVhmjc+A6qGr+29WQy8hYZiNtmCjfG415COpf5xTBkW4Q==", 748 + "funding": [ 749 + { 750 + "type": "github", 751 + "url": "https://github.com/sponsors/fastify" 752 + }, 753 + { 754 + "type": "opencollective", 755 + "url": "https://opencollective.com/fastify" 756 + } 757 + ], 758 + "license": "MIT", 759 + "dependencies": { 760 + "fastify-plugin": "^5.0.1", 761 + "safe-stable-stringify": "^2.4.3" 762 + } 763 + }, 764 + "node_modules/@fastify/session/node_modules/fastify-plugin": { 765 + "version": "5.1.0", 766 + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", 767 + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", 768 + "funding": [ 769 + { 770 + "type": "github", 771 + "url": "https://github.com/sponsors/fastify" 772 + }, 773 + { 774 + "type": "opencollective", 775 + "url": "https://opencollective.com/fastify" 776 + } 777 + ], 778 + "license": "MIT" 779 + }, 780 + "node_modules/@fastify/static": { 781 + "version": "8.3.0", 782 + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.3.0.tgz", 783 + "integrity": "sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==", 784 + "funding": [ 785 + { 786 + "type": "github", 787 + "url": "https://github.com/sponsors/fastify" 788 + }, 789 + { 790 + "type": "opencollective", 791 + "url": "https://opencollective.com/fastify" 792 + } 793 + ], 794 + "license": "MIT", 795 + "dependencies": { 796 + "@fastify/accept-negotiator": "^2.0.0", 797 + "@fastify/send": "^4.0.0", 798 + "content-disposition": "^0.5.4", 799 + "fastify-plugin": "^5.0.0", 800 + "fastq": "^1.17.1", 801 + "glob": "^11.0.0" 802 + } 803 + }, 804 + "node_modules/@fastify/static/node_modules/fastify-plugin": { 805 + "version": "5.1.0", 806 + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", 807 + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", 808 + "funding": [ 809 + { 810 + "type": "github", 811 + "url": "https://github.com/sponsors/fastify" 812 + }, 813 + { 814 + "type": "opencollective", 815 + "url": "https://opencollective.com/fastify" 816 + } 817 + ], 818 + "license": "MIT" 819 + }, 820 + "node_modules/@hexagon/base64": { 821 + "version": "1.1.28", 822 + "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", 823 + "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", 824 + "license": "MIT" 825 + }, 826 + "node_modules/@isaacs/cliui": { 827 + "version": "9.0.0", 828 + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", 829 + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", 830 + "license": "BlueOak-1.0.0", 831 + "engines": { 832 + "node": ">=18" 833 + } 834 + }, 835 + "node_modules/@levischuck/tiny-cbor": { 836 + "version": "0.2.11", 837 + "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", 838 + "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", 839 + "license": "MIT" 840 + }, 841 + "node_modules/@lukeed/ms": { 842 + "version": "2.0.2", 843 + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", 844 + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", 845 + "license": "MIT", 846 + "engines": { 847 + "node": ">=8" 848 + } 849 + }, 850 + "node_modules/@peculiar/asn1-android": { 851 + "version": "2.8.0", 852 + "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.8.0.tgz", 853 + "integrity": "sha512-skLbS+IOGv1lUgDqtChr8xvtvEr3HMse/JGBaL2r1J1o/n7a8wqOrovMtlRq/UXLhxvmLaONP67hwtshgzwfzA==", 854 + "license": "MIT", 855 + "dependencies": { 856 + "@peculiar/asn1-schema": "^2.8.0", 857 + "asn1js": "^3.0.10", 858 + "tslib": "^2.8.1" 859 + } 860 + }, 861 + "node_modules/@peculiar/asn1-cms": { 862 + "version": "2.8.0", 863 + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.8.0.tgz", 864 + "integrity": "sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA==", 865 + "license": "MIT", 866 + "dependencies": { 867 + "@peculiar/asn1-schema": "^2.8.0", 868 + "@peculiar/asn1-x509": "^2.8.0", 869 + "@peculiar/asn1-x509-attr": "^2.8.0", 870 + "asn1js": "^3.0.10", 871 + "tslib": "^2.8.1" 872 + } 873 + }, 874 + "node_modules/@peculiar/asn1-csr": { 875 + "version": "2.8.0", 876 + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.8.0.tgz", 877 + "integrity": "sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg==", 878 + "license": "MIT", 879 + "dependencies": { 880 + "@peculiar/asn1-schema": "^2.8.0", 881 + "@peculiar/asn1-x509": "^2.8.0", 882 + "asn1js": "^3.0.10", 883 + "tslib": "^2.8.1" 884 + } 885 + }, 886 + "node_modules/@peculiar/asn1-ecc": { 887 + "version": "2.8.0", 888 + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.8.0.tgz", 889 + "integrity": "sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ==", 890 + "license": "MIT", 891 + "dependencies": { 892 + "@peculiar/asn1-schema": "^2.8.0", 893 + "@peculiar/asn1-x509": "^2.8.0", 894 + "asn1js": "^3.0.10", 895 + "tslib": "^2.8.1" 896 + } 897 + }, 898 + "node_modules/@peculiar/asn1-pfx": { 899 + "version": "2.8.0", 900 + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.8.0.tgz", 901 + "integrity": "sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg==", 902 + "license": "MIT", 903 + "dependencies": { 904 + "@peculiar/asn1-cms": "^2.8.0", 905 + "@peculiar/asn1-pkcs8": "^2.8.0", 906 + "@peculiar/asn1-rsa": "^2.8.0", 907 + "@peculiar/asn1-schema": "^2.8.0", 908 + "asn1js": "^3.0.10", 909 + "tslib": "^2.8.1" 910 + } 911 + }, 912 + "node_modules/@peculiar/asn1-pkcs8": { 913 + "version": "2.8.0", 914 + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.8.0.tgz", 915 + "integrity": "sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA==", 916 + "license": "MIT", 917 + "dependencies": { 918 + "@peculiar/asn1-schema": "^2.8.0", 919 + "@peculiar/asn1-x509": "^2.8.0", 920 + "asn1js": "^3.0.10", 921 + "tslib": "^2.8.1" 922 + } 923 + }, 924 + "node_modules/@peculiar/asn1-pkcs9": { 925 + "version": "2.8.0", 926 + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.8.0.tgz", 927 + "integrity": "sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ==", 928 + "license": "MIT", 929 + "dependencies": { 930 + "@peculiar/asn1-cms": "^2.8.0", 931 + "@peculiar/asn1-pfx": "^2.8.0", 932 + "@peculiar/asn1-pkcs8": "^2.8.0", 933 + "@peculiar/asn1-schema": "^2.8.0", 934 + "@peculiar/asn1-x509": "^2.8.0", 935 + "@peculiar/asn1-x509-attr": "^2.8.0", 936 + "asn1js": "^3.0.10", 937 + "tslib": "^2.8.1" 938 + } 939 + }, 940 + "node_modules/@peculiar/asn1-rsa": { 941 + "version": "2.8.0", 942 + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.8.0.tgz", 943 + "integrity": "sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg==", 944 + "license": "MIT", 945 + "dependencies": { 946 + "@peculiar/asn1-schema": "^2.8.0", 947 + "@peculiar/asn1-x509": "^2.8.0", 948 + "asn1js": "^3.0.10", 949 + "tslib": "^2.8.1" 950 + } 951 + }, 952 + "node_modules/@peculiar/asn1-schema": { 953 + "version": "2.8.0", 954 + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", 955 + "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", 956 + "license": "MIT", 957 + "dependencies": { 958 + "@peculiar/utils": "^2.0.2", 959 + "asn1js": "^3.0.10", 960 + "tslib": "^2.8.1" 961 + } 962 + }, 963 + "node_modules/@peculiar/asn1-x509": { 964 + "version": "2.8.0", 965 + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.8.0.tgz", 966 + "integrity": "sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg==", 967 + "license": "MIT", 968 + "dependencies": { 969 + "@peculiar/asn1-schema": "^2.8.0", 970 + "@peculiar/utils": "^2.0.2", 971 + "asn1js": "^3.0.10", 972 + "tslib": "^2.8.1" 973 + } 974 + }, 975 + "node_modules/@peculiar/asn1-x509-attr": { 976 + "version": "2.8.0", 977 + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.8.0.tgz", 978 + "integrity": "sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA==", 979 + "license": "MIT", 980 + "dependencies": { 981 + "@peculiar/asn1-schema": "^2.8.0", 982 + "@peculiar/asn1-x509": "^2.8.0", 983 + "asn1js": "^3.0.10", 984 + "tslib": "^2.8.1" 985 + } 986 + }, 987 + "node_modules/@peculiar/utils": { 988 + "version": "2.0.3", 989 + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", 990 + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", 991 + "license": "MIT", 992 + "dependencies": { 993 + "tslib": "^2.8.1" 994 + } 995 + }, 996 + "node_modules/@peculiar/x509": { 997 + "version": "1.14.3", 998 + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", 999 + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", 1000 + "license": "MIT", 1001 + "dependencies": { 1002 + "@peculiar/asn1-cms": "^2.6.0", 1003 + "@peculiar/asn1-csr": "^2.6.0", 1004 + "@peculiar/asn1-ecc": "^2.6.0", 1005 + "@peculiar/asn1-pkcs9": "^2.6.0", 1006 + "@peculiar/asn1-rsa": "^2.6.0", 1007 + "@peculiar/asn1-schema": "^2.6.0", 1008 + "@peculiar/asn1-x509": "^2.6.0", 1009 + "pvtsutils": "^1.3.6", 1010 + "reflect-metadata": "^0.2.2", 1011 + "tslib": "^2.8.1", 1012 + "tsyringe": "^4.10.0" 1013 + }, 1014 + "engines": { 1015 + "node": ">=20.0.0" 1016 + } 1017 + }, 1018 + "node_modules/@pinojs/redact": { 1019 + "version": "0.4.0", 1020 + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", 1021 + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", 1022 + "license": "MIT" 1023 + }, 1024 + "node_modules/@simplewebauthn/server": { 1025 + "version": "13.3.2", 1026 + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.3.2.tgz", 1027 + "integrity": "sha512-KEDhfcGP1PAKRVSDjA3npTQFqS2b/srm+ipoNBNHdkzrHAlaRQUTE+a5f4ywsx6thxAw1NU2rYcLEY1949RGbQ==", 1028 + "license": "MIT", 1029 + "dependencies": { 1030 + "@hexagon/base64": "^1.1.27", 1031 + "@levischuck/tiny-cbor": "^0.2.2", 1032 + "@peculiar/asn1-android": "^2.6.0", 1033 + "@peculiar/asn1-ecc": "^2.6.1", 1034 + "@peculiar/asn1-rsa": "^2.6.1", 1035 + "@peculiar/asn1-schema": "^2.6.0", 1036 + "@peculiar/asn1-x509": "^2.6.1", 1037 + "@peculiar/x509": "^1.14.3" 1038 + }, 1039 + "engines": { 1040 + "node": ">=20.0.0" 1041 + } 1042 + }, 1043 + "node_modules/@types/bcryptjs": { 1044 + "version": "2.4.6", 1045 + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", 1046 + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", 1047 + "dev": true, 1048 + "license": "MIT" 1049 + }, 1050 + "node_modules/@types/better-sqlite3": { 1051 + "version": "7.6.13", 1052 + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", 1053 + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", 1054 + "dev": true, 1055 + "license": "MIT", 1056 + "dependencies": { 1057 + "@types/node": "*" 1058 + } 1059 + }, 1060 + "node_modules/@types/node": { 1061 + "version": "22.20.1", 1062 + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", 1063 + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", 1064 + "dev": true, 1065 + "license": "MIT", 1066 + "dependencies": { 1067 + "undici-types": "~6.21.0" 1068 + } 1069 + }, 1070 + "node_modules/@types/qrcode-terminal": { 1071 + "version": "0.12.2", 1072 + "resolved": "https://registry.npmjs.org/@types/qrcode-terminal/-/qrcode-terminal-0.12.2.tgz", 1073 + "integrity": "sha512-v+RcIEJ+Uhd6ygSQ0u5YYY7ZM+la7GgPbs0V/7l/kFs2uO4S8BcIUEMoP7za4DNIqNnUD5npf0A/7kBhrCKG5Q==", 1074 + "dev": true, 1075 + "license": "MIT" 1076 + }, 1077 + "node_modules/@types/ws": { 1078 + "version": "8.18.1", 1079 + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", 1080 + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", 1081 + "dev": true, 1082 + "license": "MIT", 1083 + "dependencies": { 1084 + "@types/node": "*" 1085 + } 1086 + }, 1087 + "node_modules/abstract-logging": { 1088 + "version": "2.0.1", 1089 + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", 1090 + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", 1091 + "license": "MIT" 1092 + }, 1093 + "node_modules/ajv": { 1094 + "version": "8.20.0", 1095 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", 1096 + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", 1097 + "license": "MIT", 1098 + "dependencies": { 1099 + "fast-deep-equal": "^3.1.3", 1100 + "fast-uri": "^3.0.1", 1101 + "json-schema-traverse": "^1.0.0", 1102 + "require-from-string": "^2.0.2" 1103 + }, 1104 + "funding": { 1105 + "type": "github", 1106 + "url": "https://github.com/sponsors/epoberezkin" 1107 + } 1108 + }, 1109 + "node_modules/ajv-formats": { 1110 + "version": "3.0.1", 1111 + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", 1112 + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", 1113 + "license": "MIT", 1114 + "dependencies": { 1115 + "ajv": "^8.0.0" 1116 + }, 1117 + "peerDependencies": { 1118 + "ajv": "^8.0.0" 1119 + }, 1120 + "peerDependenciesMeta": { 1121 + "ajv": { 1122 + "optional": true 1123 + } 1124 + } 1125 + }, 1126 + "node_modules/asn1js": { 1127 + "version": "3.0.10", 1128 + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", 1129 + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", 1130 + "license": "BSD-3-Clause", 1131 + "dependencies": { 1132 + "pvtsutils": "^1.3.6", 1133 + "pvutils": "^1.1.5", 1134 + "tslib": "^2.8.1" 1135 + }, 1136 + "engines": { 1137 + "node": ">=12.0.0" 1138 + } 1139 + }, 1140 + "node_modules/atomic-sleep": { 1141 + "version": "1.0.0", 1142 + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", 1143 + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", 1144 + "license": "MIT", 1145 + "engines": { 1146 + "node": ">=8.0.0" 1147 + } 1148 + }, 1149 + "node_modules/avvio": { 1150 + "version": "9.2.0", 1151 + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", 1152 + "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", 1153 + "funding": [ 1154 + { 1155 + "type": "github", 1156 + "url": "https://github.com/sponsors/fastify" 1157 + }, 1158 + { 1159 + "type": "opencollective", 1160 + "url": "https://opencollective.com/fastify" 1161 + } 1162 + ], 1163 + "license": "MIT", 1164 + "dependencies": { 1165 + "@fastify/error": "^4.0.0", 1166 + "fastq": "^1.17.1" 1167 + } 1168 + }, 1169 + "node_modules/balanced-match": { 1170 + "version": "4.0.4", 1171 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", 1172 + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", 1173 + "license": "MIT", 1174 + "engines": { 1175 + "node": "18 || 20 || >=22" 1176 + } 1177 + }, 1178 + "node_modules/base64-js": { 1179 + "version": "1.5.1", 1180 + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 1181 + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 1182 + "funding": [ 1183 + { 1184 + "type": "github", 1185 + "url": "https://github.com/sponsors/feross" 1186 + }, 1187 + { 1188 + "type": "patreon", 1189 + "url": "https://www.patreon.com/feross" 1190 + }, 1191 + { 1192 + "type": "consulting", 1193 + "url": "https://feross.org/support" 1194 + } 1195 + ], 1196 + "license": "MIT" 1197 + }, 1198 + "node_modules/bcryptjs": { 1199 + "version": "3.0.3", 1200 + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", 1201 + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", 1202 + "license": "BSD-3-Clause", 1203 + "bin": { 1204 + "bcrypt": "bin/bcrypt" 1205 + } 1206 + }, 1207 + "node_modules/better-sqlite3": { 1208 + "version": "12.11.1", 1209 + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", 1210 + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", 1211 + "hasInstallScript": true, 1212 + "license": "MIT", 1213 + "dependencies": { 1214 + "bindings": "^1.5.0", 1215 + "prebuild-install": "^7.1.1" 1216 + }, 1217 + "engines": { 1218 + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" 1219 + } 1220 + }, 1221 + "node_modules/bindings": { 1222 + "version": "1.5.0", 1223 + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", 1224 + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 1225 + "license": "MIT", 1226 + "dependencies": { 1227 + "file-uri-to-path": "1.0.0" 1228 + } 1229 + }, 1230 + "node_modules/bl": { 1231 + "version": "4.1.0", 1232 + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 1233 + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 1234 + "license": "MIT", 1235 + "dependencies": { 1236 + "buffer": "^5.5.0", 1237 + "inherits": "^2.0.4", 1238 + "readable-stream": "^3.4.0" 1239 + } 1240 + }, 1241 + "node_modules/brace-expansion": { 1242 + "version": "5.0.7", 1243 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", 1244 + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", 1245 + "license": "MIT", 1246 + "dependencies": { 1247 + "balanced-match": "^4.0.2" 1248 + }, 1249 + "engines": { 1250 + "node": "18 || 20 || >=22" 1251 + } 1252 + }, 1253 + "node_modules/buffer": { 1254 + "version": "5.7.1", 1255 + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 1256 + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 1257 + "funding": [ 1258 + { 1259 + "type": "github", 1260 + "url": "https://github.com/sponsors/feross" 1261 + }, 1262 + { 1263 + "type": "patreon", 1264 + "url": "https://www.patreon.com/feross" 1265 + }, 1266 + { 1267 + "type": "consulting", 1268 + "url": "https://feross.org/support" 1269 + } 1270 + ], 1271 + "license": "MIT", 1272 + "dependencies": { 1273 + "base64-js": "^1.3.1", 1274 + "ieee754": "^1.1.13" 1275 + } 1276 + }, 1277 + "node_modules/cbor-extract": { 1278 + "version": "2.2.2", 1279 + "resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.2.tgz", 1280 + "integrity": "sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==", 1281 + "hasInstallScript": true, 1282 + "license": "MIT", 1283 + "optional": true, 1284 + "dependencies": { 1285 + "node-gyp-build-optional-packages": "5.1.1" 1286 + }, 1287 + "bin": { 1288 + "download-cbor-prebuilds": "bin/download-prebuilds.js" 1289 + }, 1290 + "optionalDependencies": { 1291 + "@cbor-extract/cbor-extract-darwin-arm64": "2.2.2", 1292 + "@cbor-extract/cbor-extract-darwin-x64": "2.2.2", 1293 + "@cbor-extract/cbor-extract-linux-arm": "2.2.2", 1294 + "@cbor-extract/cbor-extract-linux-arm64": "2.2.2", 1295 + "@cbor-extract/cbor-extract-linux-x64": "2.2.2", 1296 + "@cbor-extract/cbor-extract-win32-x64": "2.2.2" 1297 + } 1298 + }, 1299 + "node_modules/cbor-x": { 1300 + "version": "1.6.4", 1301 + "resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.6.4.tgz", 1302 + "integrity": "sha512-UGKHjp6RHC6QuZ2yy5LCKm7MojM4716DwoSaqwQpaH4DvZvbBTGcoDNTiG9Y2lByXZYFEs9WRkS5tLl96IrF1Q==", 1303 + "license": "MIT", 1304 + "optionalDependencies": { 1305 + "cbor-extract": "^2.2.2" 1306 + } 1307 + }, 1308 + "node_modules/chownr": { 1309 + "version": "1.1.4", 1310 + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 1311 + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", 1312 + "license": "ISC" 1313 + }, 1314 + "node_modules/content-disposition": { 1315 + "version": "0.5.4", 1316 + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 1317 + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 1318 + "license": "MIT", 1319 + "dependencies": { 1320 + "safe-buffer": "5.2.1" 1321 + }, 1322 + "engines": { 1323 + "node": ">= 0.6" 1324 + } 1325 + }, 1326 + "node_modules/cookie": { 1327 + "version": "2.0.1", 1328 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz", 1329 + "integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==", 1330 + "license": "MIT", 1331 + "engines": { 1332 + "node": ">=22" 1333 + }, 1334 + "funding": { 1335 + "type": "opencollective", 1336 + "url": "https://opencollective.com/express" 1337 + } 1338 + }, 1339 + "node_modules/cross-spawn": { 1340 + "version": "7.0.6", 1341 + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 1342 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 1343 + "license": "MIT", 1344 + "dependencies": { 1345 + "path-key": "^3.1.0", 1346 + "shebang-command": "^2.0.0", 1347 + "which": "^2.0.1" 1348 + }, 1349 + "engines": { 1350 + "node": ">= 8" 1351 + } 1352 + }, 1353 + "node_modules/decompress-response": { 1354 + "version": "6.0.0", 1355 + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 1356 + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 1357 + "license": "MIT", 1358 + "dependencies": { 1359 + "mimic-response": "^3.1.0" 1360 + }, 1361 + "engines": { 1362 + "node": ">=10" 1363 + }, 1364 + "funding": { 1365 + "url": "https://github.com/sponsors/sindresorhus" 1366 + } 1367 + }, 1368 + "node_modules/deep-extend": { 1369 + "version": "0.6.0", 1370 + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 1371 + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", 1372 + "license": "MIT", 1373 + "engines": { 1374 + "node": ">=4.0.0" 1375 + } 1376 + }, 1377 + "node_modules/depd": { 1378 + "version": "2.0.0", 1379 + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 1380 + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 1381 + "license": "MIT", 1382 + "engines": { 1383 + "node": ">= 0.8" 1384 + } 1385 + }, 1386 + "node_modules/dequal": { 1387 + "version": "2.0.3", 1388 + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", 1389 + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", 1390 + "license": "MIT", 1391 + "engines": { 1392 + "node": ">=6" 1393 + } 1394 + }, 1395 + "node_modules/detect-libc": { 1396 + "version": "2.1.2", 1397 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", 1398 + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", 1399 + "license": "Apache-2.0", 1400 + "engines": { 1401 + "node": ">=8" 1402 + } 1403 + }, 1404 + "node_modules/dotenv": { 1405 + "version": "16.6.1", 1406 + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", 1407 + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", 1408 + "license": "BSD-2-Clause", 1409 + "engines": { 1410 + "node": ">=12" 1411 + }, 1412 + "funding": { 1413 + "url": "https://dotenvx.com" 1414 + } 1415 + }, 1416 + "node_modules/end-of-stream": { 1417 + "version": "1.4.5", 1418 + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", 1419 + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", 1420 + "license": "MIT", 1421 + "dependencies": { 1422 + "once": "^1.4.0" 1423 + } 1424 + }, 1425 + "node_modules/esbuild": { 1426 + "version": "0.28.1", 1427 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", 1428 + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", 1429 + "dev": true, 1430 + "hasInstallScript": true, 1431 + "license": "MIT", 1432 + "bin": { 1433 + "esbuild": "bin/esbuild" 1434 + }, 1435 + "engines": { 1436 + "node": ">=18" 1437 + }, 1438 + "optionalDependencies": { 1439 + "@esbuild/aix-ppc64": "0.28.1", 1440 + "@esbuild/android-arm": "0.28.1", 1441 + "@esbuild/android-arm64": "0.28.1", 1442 + "@esbuild/android-x64": "0.28.1", 1443 + "@esbuild/darwin-arm64": "0.28.1", 1444 + "@esbuild/darwin-x64": "0.28.1", 1445 + "@esbuild/freebsd-arm64": "0.28.1", 1446 + "@esbuild/freebsd-x64": "0.28.1", 1447 + "@esbuild/linux-arm": "0.28.1", 1448 + "@esbuild/linux-arm64": "0.28.1", 1449 + "@esbuild/linux-ia32": "0.28.1", 1450 + "@esbuild/linux-loong64": "0.28.1", 1451 + "@esbuild/linux-mips64el": "0.28.1", 1452 + "@esbuild/linux-ppc64": "0.28.1", 1453 + "@esbuild/linux-riscv64": "0.28.1", 1454 + "@esbuild/linux-s390x": "0.28.1", 1455 + "@esbuild/linux-x64": "0.28.1", 1456 + "@esbuild/netbsd-arm64": "0.28.1", 1457 + "@esbuild/netbsd-x64": "0.28.1", 1458 + "@esbuild/openbsd-arm64": "0.28.1", 1459 + "@esbuild/openbsd-x64": "0.28.1", 1460 + "@esbuild/openharmony-arm64": "0.28.1", 1461 + "@esbuild/sunos-x64": "0.28.1", 1462 + "@esbuild/win32-arm64": "0.28.1", 1463 + "@esbuild/win32-ia32": "0.28.1", 1464 + "@esbuild/win32-x64": "0.28.1" 1465 + } 1466 + }, 1467 + "node_modules/escape-html": { 1468 + "version": "1.0.3", 1469 + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 1470 + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 1471 + "license": "MIT" 1472 + }, 1473 + "node_modules/expand-template": { 1474 + "version": "2.0.3", 1475 + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", 1476 + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", 1477 + "license": "(MIT OR WTFPL)", 1478 + "engines": { 1479 + "node": ">=6" 1480 + } 1481 + }, 1482 + "node_modules/fast-decode-uri-component": { 1483 + "version": "1.0.1", 1484 + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", 1485 + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", 1486 + "license": "MIT" 1487 + }, 1488 + "node_modules/fast-deep-equal": { 1489 + "version": "3.1.3", 1490 + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 1491 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 1492 + "license": "MIT" 1493 + }, 1494 + "node_modules/fast-json-stringify": { 1495 + "version": "7.0.1", 1496 + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", 1497 + "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==", 1498 + "funding": [ 1499 + { 1500 + "type": "github", 1501 + "url": "https://github.com/sponsors/fastify" 1502 + }, 1503 + { 1504 + "type": "opencollective", 1505 + "url": "https://opencollective.com/fastify" 1506 + } 1507 + ], 1508 + "license": "MIT", 1509 + "dependencies": { 1510 + "@fastify/merge-json-schemas": "^0.2.0", 1511 + "ajv": "^8.12.0", 1512 + "ajv-formats": "^3.0.1", 1513 + "fast-uri": "^4.0.0", 1514 + "json-schema-ref-resolver": "^3.0.0", 1515 + "rfdc": "^1.2.0" 1516 + } 1517 + }, 1518 + "node_modules/fast-json-stringify/node_modules/fast-uri": { 1519 + "version": "4.1.0", 1520 + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.0.tgz", 1521 + "integrity": "sha512-ZodJ2cRiLVWGi9IgPb3mbgSqM4CD3LexCHkuv0FfBXHJI1ADfucTD06m6clO2Cy5RZYsw/SiCVl/dyrFI/SYWA==", 1522 + "funding": [ 1523 + { 1524 + "type": "github", 1525 + "url": "https://github.com/sponsors/fastify" 1526 + }, 1527 + { 1528 + "type": "opencollective", 1529 + "url": "https://opencollective.com/fastify" 1530 + } 1531 + ], 1532 + "license": "BSD-3-Clause" 1533 + }, 1534 + "node_modules/fast-querystring": { 1535 + "version": "1.1.2", 1536 + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", 1537 + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", 1538 + "license": "MIT", 1539 + "dependencies": { 1540 + "fast-decode-uri-component": "^1.0.1" 1541 + } 1542 + }, 1543 + "node_modules/fast-uri": { 1544 + "version": "3.1.3", 1545 + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", 1546 + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", 1547 + "funding": [ 1548 + { 1549 + "type": "github", 1550 + "url": "https://github.com/sponsors/fastify" 1551 + }, 1552 + { 1553 + "type": "opencollective", 1554 + "url": "https://opencollective.com/fastify" 1555 + } 1556 + ], 1557 + "license": "BSD-3-Clause" 1558 + }, 1559 + "node_modules/fastify": { 1560 + "version": "5.10.0", 1561 + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.10.0.tgz", 1562 + "integrity": "sha512-A9L0ziuWGQHgEEVgF3davQ9vbD93IuX+lo2IsxapQmu5b/Y/ynn9m9K5JHt9dvyJXOFc5iN0Zk5GHEOqnzhWjg==", 1563 + "funding": [ 1564 + { 1565 + "type": "github", 1566 + "url": "https://github.com/sponsors/fastify" 1567 + }, 1568 + { 1569 + "type": "opencollective", 1570 + "url": "https://opencollective.com/fastify" 1571 + } 1572 + ], 1573 + "license": "MIT", 1574 + "dependencies": { 1575 + "@fastify/ajv-compiler": "^4.0.5", 1576 + "@fastify/error": "^4.0.0", 1577 + "@fastify/fast-json-stringify-compiler": "^5.0.0", 1578 + "@fastify/proxy-addr": "^5.0.0", 1579 + "abstract-logging": "^2.0.1", 1580 + "avvio": "^9.0.0", 1581 + "fast-json-stringify": "^7.0.0", 1582 + "find-my-way": "^9.6.0", 1583 + "light-my-request": "^6.0.0", 1584 + "pino": "^9.14.0 || ^10.1.0", 1585 + "process-warning": "^5.0.0", 1586 + "rfdc": "^1.3.1", 1587 + "secure-json-parse": "^4.0.0", 1588 + "semver": "^7.6.0", 1589 + "toad-cache": "^3.7.0" 1590 + } 1591 + }, 1592 + "node_modules/fastify-plugin": { 1593 + "version": "6.0.0", 1594 + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.0.tgz", 1595 + "integrity": "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==", 1596 + "funding": [ 1597 + { 1598 + "type": "github", 1599 + "url": "https://github.com/sponsors/fastify" 1600 + }, 1601 + { 1602 + "type": "opencollective", 1603 + "url": "https://opencollective.com/fastify" 1604 + } 1605 + ], 1606 + "license": "MIT" 1607 + }, 1608 + "node_modules/fastq": { 1609 + "version": "1.20.1", 1610 + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", 1611 + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", 1612 + "license": "ISC", 1613 + "dependencies": { 1614 + "reusify": "^1.0.4" 1615 + } 1616 + }, 1617 + "node_modules/file-uri-to-path": { 1618 + "version": "1.0.0", 1619 + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", 1620 + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", 1621 + "license": "MIT" 1622 + }, 1623 + "node_modules/find-my-way": { 1624 + "version": "9.6.0", 1625 + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", 1626 + "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", 1627 + "license": "MIT", 1628 + "dependencies": { 1629 + "fast-deep-equal": "^3.1.3", 1630 + "fast-querystring": "^1.0.0", 1631 + "safe-regex2": "^5.0.0" 1632 + }, 1633 + "engines": { 1634 + "node": ">=20" 1635 + } 1636 + }, 1637 + "node_modules/foreground-child": { 1638 + "version": "3.3.1", 1639 + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", 1640 + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", 1641 + "license": "ISC", 1642 + "dependencies": { 1643 + "cross-spawn": "^7.0.6", 1644 + "signal-exit": "^4.0.1" 1645 + }, 1646 + "engines": { 1647 + "node": ">=14" 1648 + }, 1649 + "funding": { 1650 + "url": "https://github.com/sponsors/isaacs" 1651 + } 1652 + }, 1653 + "node_modules/fs-constants": { 1654 + "version": "1.0.0", 1655 + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 1656 + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", 1657 + "license": "MIT" 1658 + }, 1659 + "node_modules/fsevents": { 1660 + "version": "2.3.3", 1661 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1662 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1663 + "dev": true, 1664 + "hasInstallScript": true, 1665 + "license": "MIT", 1666 + "optional": true, 1667 + "os": [ 1668 + "darwin" 1669 + ], 1670 + "engines": { 1671 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1672 + } 1673 + }, 1674 + "node_modules/github-from-package": { 1675 + "version": "0.0.0", 1676 + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", 1677 + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", 1678 + "license": "MIT" 1679 + }, 1680 + "node_modules/glob": { 1681 + "version": "11.1.0", 1682 + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", 1683 + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", 1684 + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", 1685 + "license": "BlueOak-1.0.0", 1686 + "dependencies": { 1687 + "foreground-child": "^3.3.1", 1688 + "jackspeak": "^4.1.1", 1689 + "minimatch": "^10.1.1", 1690 + "minipass": "^7.1.2", 1691 + "package-json-from-dist": "^1.0.0", 1692 + "path-scurry": "^2.0.0" 1693 + }, 1694 + "bin": { 1695 + "glob": "dist/esm/bin.mjs" 1696 + }, 1697 + "engines": { 1698 + "node": "20 || >=22" 1699 + }, 1700 + "funding": { 1701 + "url": "https://github.com/sponsors/isaacs" 1702 + } 1703 + }, 1704 + "node_modules/helmet": { 1705 + "version": "8.2.0", 1706 + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.2.0.tgz", 1707 + "integrity": "sha512-DRgTIUgnWcJ62KyarxxziuqYxKGnR6Rgg19BlbucN/dpmJbl1XOit6qvoOX0ZT+HhWe5OUVhU/a1zpGyc1xA0Q==", 1708 + "license": "MIT", 1709 + "engines": { 1710 + "node": ">=18.0.0" 1711 + }, 1712 + "funding": { 1713 + "url": "https://github.com/sponsors/EvanHahn" 1714 + } 1715 + }, 1716 + "node_modules/http-errors": { 1717 + "version": "2.0.1", 1718 + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", 1719 + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", 1720 + "license": "MIT", 1721 + "dependencies": { 1722 + "depd": "~2.0.0", 1723 + "inherits": "~2.0.4", 1724 + "setprototypeof": "~1.2.0", 1725 + "statuses": "~2.0.2", 1726 + "toidentifier": "~1.0.1" 1727 + }, 1728 + "engines": { 1729 + "node": ">= 0.8" 1730 + }, 1731 + "funding": { 1732 + "type": "opencollective", 1733 + "url": "https://opencollective.com/express" 1734 + } 1735 + }, 1736 + "node_modules/ieee754": { 1737 + "version": "1.2.1", 1738 + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 1739 + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 1740 + "funding": [ 1741 + { 1742 + "type": "github", 1743 + "url": "https://github.com/sponsors/feross" 1744 + }, 1745 + { 1746 + "type": "patreon", 1747 + "url": "https://www.patreon.com/feross" 1748 + }, 1749 + { 1750 + "type": "consulting", 1751 + "url": "https://feross.org/support" 1752 + } 1753 + ], 1754 + "license": "BSD-3-Clause" 1755 + }, 1756 + "node_modules/inherits": { 1757 + "version": "2.0.4", 1758 + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1759 + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1760 + "license": "ISC" 1761 + }, 1762 + "node_modules/ini": { 1763 + "version": "1.3.8", 1764 + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 1765 + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", 1766 + "license": "ISC" 1767 + }, 1768 + "node_modules/ipaddr.js": { 1769 + "version": "2.4.0", 1770 + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", 1771 + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", 1772 + "license": "MIT", 1773 + "engines": { 1774 + "node": ">= 10" 1775 + } 1776 + }, 1777 + "node_modules/isexe": { 1778 + "version": "2.0.0", 1779 + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1780 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 1781 + "license": "ISC" 1782 + }, 1783 + "node_modules/jackspeak": { 1784 + "version": "4.2.3", 1785 + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", 1786 + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", 1787 + "license": "BlueOak-1.0.0", 1788 + "dependencies": { 1789 + "@isaacs/cliui": "^9.0.0" 1790 + }, 1791 + "engines": { 1792 + "node": "20 || >=22" 1793 + }, 1794 + "funding": { 1795 + "url": "https://github.com/sponsors/isaacs" 1796 + } 1797 + }, 1798 + "node_modules/json-schema-ref-resolver": { 1799 + "version": "3.0.0", 1800 + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", 1801 + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", 1802 + "funding": [ 1803 + { 1804 + "type": "github", 1805 + "url": "https://github.com/sponsors/fastify" 1806 + }, 1807 + { 1808 + "type": "opencollective", 1809 + "url": "https://opencollective.com/fastify" 1810 + } 1811 + ], 1812 + "license": "MIT", 1813 + "dependencies": { 1814 + "dequal": "^2.0.3" 1815 + } 1816 + }, 1817 + "node_modules/json-schema-traverse": { 1818 + "version": "1.0.0", 1819 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", 1820 + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", 1821 + "license": "MIT" 1822 + }, 1823 + "node_modules/light-my-request": { 1824 + "version": "6.6.0", 1825 + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", 1826 + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", 1827 + "funding": [ 1828 + { 1829 + "type": "github", 1830 + "url": "https://github.com/sponsors/fastify" 1831 + }, 1832 + { 1833 + "type": "opencollective", 1834 + "url": "https://opencollective.com/fastify" 1835 + } 1836 + ], 1837 + "license": "BSD-3-Clause", 1838 + "dependencies": { 1839 + "cookie": "^1.0.1", 1840 + "process-warning": "^4.0.0", 1841 + "set-cookie-parser": "^2.6.0" 1842 + } 1843 + }, 1844 + "node_modules/light-my-request/node_modules/cookie": { 1845 + "version": "1.1.1", 1846 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", 1847 + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", 1848 + "license": "MIT", 1849 + "engines": { 1850 + "node": ">=18" 1851 + }, 1852 + "funding": { 1853 + "type": "opencollective", 1854 + "url": "https://opencollective.com/express" 1855 + } 1856 + }, 1857 + "node_modules/light-my-request/node_modules/process-warning": { 1858 + "version": "4.0.1", 1859 + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", 1860 + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", 1861 + "funding": [ 1862 + { 1863 + "type": "github", 1864 + "url": "https://github.com/sponsors/fastify" 1865 + }, 1866 + { 1867 + "type": "opencollective", 1868 + "url": "https://opencollective.com/fastify" 1869 + } 1870 + ], 1871 + "license": "MIT" 1872 + }, 1873 + "node_modules/lru-cache": { 1874 + "version": "11.5.2", 1875 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", 1876 + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", 1877 + "license": "BlueOak-1.0.0", 1878 + "engines": { 1879 + "node": "20 || >=22" 1880 + } 1881 + }, 1882 + "node_modules/mime": { 1883 + "version": "3.0.0", 1884 + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", 1885 + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", 1886 + "license": "MIT", 1887 + "bin": { 1888 + "mime": "cli.js" 1889 + }, 1890 + "engines": { 1891 + "node": ">=10.0.0" 1892 + } 1893 + }, 1894 + "node_modules/mimic-response": { 1895 + "version": "3.1.0", 1896 + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 1897 + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", 1898 + "license": "MIT", 1899 + "engines": { 1900 + "node": ">=10" 1901 + }, 1902 + "funding": { 1903 + "url": "https://github.com/sponsors/sindresorhus" 1904 + } 1905 + }, 1906 + "node_modules/minimatch": { 1907 + "version": "10.2.5", 1908 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", 1909 + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", 1910 + "license": "BlueOak-1.0.0", 1911 + "dependencies": { 1912 + "brace-expansion": "^5.0.5" 1913 + }, 1914 + "engines": { 1915 + "node": "18 || 20 || >=22" 1916 + }, 1917 + "funding": { 1918 + "url": "https://github.com/sponsors/isaacs" 1919 + } 1920 + }, 1921 + "node_modules/minimist": { 1922 + "version": "1.2.8", 1923 + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 1924 + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 1925 + "license": "MIT", 1926 + "funding": { 1927 + "url": "https://github.com/sponsors/ljharb" 1928 + } 1929 + }, 1930 + "node_modules/minipass": { 1931 + "version": "7.1.3", 1932 + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", 1933 + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", 1934 + "license": "BlueOak-1.0.0", 1935 + "engines": { 1936 + "node": ">=16 || 14 >=14.17" 1937 + } 1938 + }, 1939 + "node_modules/mkdirp-classic": { 1940 + "version": "0.5.3", 1941 + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 1942 + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", 1943 + "license": "MIT" 1944 + }, 1945 + "node_modules/napi-build-utils": { 1946 + "version": "2.0.0", 1947 + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", 1948 + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", 1949 + "license": "MIT" 1950 + }, 1951 + "node_modules/node-abi": { 1952 + "version": "3.94.0", 1953 + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", 1954 + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", 1955 + "license": "MIT", 1956 + "dependencies": { 1957 + "semver": "^7.3.5" 1958 + }, 1959 + "engines": { 1960 + "node": ">=10" 1961 + } 1962 + }, 1963 + "node_modules/node-gyp-build-optional-packages": { 1964 + "version": "5.1.1", 1965 + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", 1966 + "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", 1967 + "license": "MIT", 1968 + "optional": true, 1969 + "dependencies": { 1970 + "detect-libc": "^2.0.1" 1971 + }, 1972 + "bin": { 1973 + "node-gyp-build-optional-packages": "bin.js", 1974 + "node-gyp-build-optional-packages-optional": "optional.js", 1975 + "node-gyp-build-optional-packages-test": "build-test.js" 1976 + } 1977 + }, 1978 + "node_modules/on-exit-leak-free": { 1979 + "version": "2.1.2", 1980 + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", 1981 + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", 1982 + "license": "MIT", 1983 + "engines": { 1984 + "node": ">=14.0.0" 1985 + } 1986 + }, 1987 + "node_modules/once": { 1988 + "version": "1.4.0", 1989 + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1990 + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 1991 + "license": "ISC", 1992 + "dependencies": { 1993 + "wrappy": "1" 1994 + } 1995 + }, 1996 + "node_modules/package-json-from-dist": { 1997 + "version": "1.0.1", 1998 + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", 1999 + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", 2000 + "license": "BlueOak-1.0.0" 2001 + }, 2002 + "node_modules/path-key": { 2003 + "version": "3.1.1", 2004 + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 2005 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 2006 + "license": "MIT", 2007 + "engines": { 2008 + "node": ">=8" 2009 + } 2010 + }, 2011 + "node_modules/path-scurry": { 2012 + "version": "2.0.2", 2013 + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", 2014 + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", 2015 + "license": "BlueOak-1.0.0", 2016 + "dependencies": { 2017 + "lru-cache": "^11.0.0", 2018 + "minipass": "^7.1.2" 2019 + }, 2020 + "engines": { 2021 + "node": "18 || 20 || >=22" 2022 + }, 2023 + "funding": { 2024 + "url": "https://github.com/sponsors/isaacs" 2025 + } 2026 + }, 2027 + "node_modules/pino": { 2028 + "version": "10.3.1", 2029 + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", 2030 + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", 2031 + "license": "MIT", 2032 + "dependencies": { 2033 + "@pinojs/redact": "^0.4.0", 2034 + "atomic-sleep": "^1.0.0", 2035 + "on-exit-leak-free": "^2.1.0", 2036 + "pino-abstract-transport": "^3.0.0", 2037 + "pino-std-serializers": "^7.0.0", 2038 + "process-warning": "^5.0.0", 2039 + "quick-format-unescaped": "^4.0.3", 2040 + "real-require": "^0.2.0", 2041 + "safe-stable-stringify": "^2.3.1", 2042 + "sonic-boom": "^4.0.1", 2043 + "thread-stream": "^4.0.0" 2044 + }, 2045 + "bin": { 2046 + "pino": "bin.js" 2047 + } 2048 + }, 2049 + "node_modules/pino-abstract-transport": { 2050 + "version": "3.0.0", 2051 + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", 2052 + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", 2053 + "license": "MIT", 2054 + "dependencies": { 2055 + "split2": "^4.0.0" 2056 + } 2057 + }, 2058 + "node_modules/pino-std-serializers": { 2059 + "version": "7.1.0", 2060 + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", 2061 + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", 2062 + "license": "MIT" 2063 + }, 2064 + "node_modules/prebuild-install": { 2065 + "version": "7.1.3", 2066 + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", 2067 + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", 2068 + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", 2069 + "license": "MIT", 2070 + "dependencies": { 2071 + "detect-libc": "^2.0.0", 2072 + "expand-template": "^2.0.3", 2073 + "github-from-package": "0.0.0", 2074 + "minimist": "^1.2.3", 2075 + "mkdirp-classic": "^0.5.3", 2076 + "napi-build-utils": "^2.0.0", 2077 + "node-abi": "^3.3.0", 2078 + "pump": "^3.0.0", 2079 + "rc": "^1.2.7", 2080 + "simple-get": "^4.0.0", 2081 + "tar-fs": "^2.0.0", 2082 + "tunnel-agent": "^0.6.0" 2083 + }, 2084 + "bin": { 2085 + "prebuild-install": "bin.js" 2086 + }, 2087 + "engines": { 2088 + "node": ">=10" 2089 + } 2090 + }, 2091 + "node_modules/process-warning": { 2092 + "version": "5.0.0", 2093 + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", 2094 + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", 2095 + "funding": [ 2096 + { 2097 + "type": "github", 2098 + "url": "https://github.com/sponsors/fastify" 2099 + }, 2100 + { 2101 + "type": "opencollective", 2102 + "url": "https://opencollective.com/fastify" 2103 + } 2104 + ], 2105 + "license": "MIT" 2106 + }, 2107 + "node_modules/pump": { 2108 + "version": "3.0.4", 2109 + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", 2110 + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", 2111 + "license": "MIT", 2112 + "dependencies": { 2113 + "end-of-stream": "^1.1.0", 2114 + "once": "^1.3.1" 2115 + } 2116 + }, 2117 + "node_modules/pvtsutils": { 2118 + "version": "1.3.6", 2119 + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", 2120 + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", 2121 + "license": "MIT", 2122 + "dependencies": { 2123 + "tslib": "^2.8.1" 2124 + } 2125 + }, 2126 + "node_modules/pvutils": { 2127 + "version": "1.1.5", 2128 + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", 2129 + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", 2130 + "license": "MIT", 2131 + "engines": { 2132 + "node": ">=16.0.0" 2133 + } 2134 + }, 2135 + "node_modules/qrcode-terminal": { 2136 + "version": "0.12.0", 2137 + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", 2138 + "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==", 2139 + "bin": { 2140 + "qrcode-terminal": "bin/qrcode-terminal.js" 2141 + } 2142 + }, 2143 + "node_modules/quick-format-unescaped": { 2144 + "version": "4.0.4", 2145 + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", 2146 + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", 2147 + "license": "MIT" 2148 + }, 2149 + "node_modules/rc": { 2150 + "version": "1.2.8", 2151 + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 2152 + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 2153 + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", 2154 + "dependencies": { 2155 + "deep-extend": "^0.6.0", 2156 + "ini": "~1.3.0", 2157 + "minimist": "^1.2.0", 2158 + "strip-json-comments": "~2.0.1" 2159 + }, 2160 + "bin": { 2161 + "rc": "cli.js" 2162 + } 2163 + }, 2164 + "node_modules/readable-stream": { 2165 + "version": "3.6.2", 2166 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 2167 + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 2168 + "license": "MIT", 2169 + "dependencies": { 2170 + "inherits": "^2.0.3", 2171 + "string_decoder": "^1.1.1", 2172 + "util-deprecate": "^1.0.1" 2173 + }, 2174 + "engines": { 2175 + "node": ">= 6" 2176 + } 2177 + }, 2178 + "node_modules/real-require": { 2179 + "version": "0.2.0", 2180 + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", 2181 + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", 2182 + "license": "MIT", 2183 + "engines": { 2184 + "node": ">= 12.13.0" 2185 + } 2186 + }, 2187 + "node_modules/reflect-metadata": { 2188 + "version": "0.2.2", 2189 + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", 2190 + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", 2191 + "license": "Apache-2.0" 2192 + }, 2193 + "node_modules/require-from-string": { 2194 + "version": "2.0.2", 2195 + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", 2196 + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", 2197 + "license": "MIT", 2198 + "engines": { 2199 + "node": ">=0.10.0" 2200 + } 2201 + }, 2202 + "node_modules/ret": { 2203 + "version": "0.5.0", 2204 + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", 2205 + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", 2206 + "license": "MIT", 2207 + "engines": { 2208 + "node": ">=10" 2209 + } 2210 + }, 2211 + "node_modules/reusify": { 2212 + "version": "1.1.0", 2213 + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", 2214 + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", 2215 + "license": "MIT", 2216 + "engines": { 2217 + "iojs": ">=1.0.0", 2218 + "node": ">=0.10.0" 2219 + } 2220 + }, 2221 + "node_modules/rfdc": { 2222 + "version": "1.4.1", 2223 + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", 2224 + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", 2225 + "license": "MIT" 2226 + }, 2227 + "node_modules/safe-buffer": { 2228 + "version": "5.2.1", 2229 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 2230 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 2231 + "funding": [ 2232 + { 2233 + "type": "github", 2234 + "url": "https://github.com/sponsors/feross" 2235 + }, 2236 + { 2237 + "type": "patreon", 2238 + "url": "https://www.patreon.com/feross" 2239 + }, 2240 + { 2241 + "type": "consulting", 2242 + "url": "https://feross.org/support" 2243 + } 2244 + ], 2245 + "license": "MIT" 2246 + }, 2247 + "node_modules/safe-regex2": { 2248 + "version": "5.1.1", 2249 + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", 2250 + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", 2251 + "funding": [ 2252 + { 2253 + "type": "github", 2254 + "url": "https://github.com/sponsors/fastify" 2255 + }, 2256 + { 2257 + "type": "opencollective", 2258 + "url": "https://opencollective.com/fastify" 2259 + } 2260 + ], 2261 + "license": "MIT", 2262 + "dependencies": { 2263 + "ret": "~0.5.0" 2264 + }, 2265 + "bin": { 2266 + "safe-regex2": "bin/safe-regex2.js" 2267 + } 2268 + }, 2269 + "node_modules/safe-stable-stringify": { 2270 + "version": "2.5.0", 2271 + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", 2272 + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", 2273 + "license": "MIT", 2274 + "engines": { 2275 + "node": ">=10" 2276 + } 2277 + }, 2278 + "node_modules/secure-json-parse": { 2279 + "version": "4.1.0", 2280 + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", 2281 + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", 2282 + "funding": [ 2283 + { 2284 + "type": "github", 2285 + "url": "https://github.com/sponsors/fastify" 2286 + }, 2287 + { 2288 + "type": "opencollective", 2289 + "url": "https://opencollective.com/fastify" 2290 + } 2291 + ], 2292 + "license": "BSD-3-Clause" 2293 + }, 2294 + "node_modules/semver": { 2295 + "version": "7.8.5", 2296 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", 2297 + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", 2298 + "license": "ISC", 2299 + "bin": { 2300 + "semver": "bin/semver.js" 2301 + }, 2302 + "engines": { 2303 + "node": ">=10" 2304 + } 2305 + }, 2306 + "node_modules/set-cookie-parser": { 2307 + "version": "2.7.2", 2308 + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", 2309 + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", 2310 + "license": "MIT" 2311 + }, 2312 + "node_modules/setprototypeof": { 2313 + "version": "1.2.0", 2314 + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 2315 + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 2316 + "license": "ISC" 2317 + }, 2318 + "node_modules/shebang-command": { 2319 + "version": "2.0.0", 2320 + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 2321 + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 2322 + "license": "MIT", 2323 + "dependencies": { 2324 + "shebang-regex": "^3.0.0" 2325 + }, 2326 + "engines": { 2327 + "node": ">=8" 2328 + } 2329 + }, 2330 + "node_modules/shebang-regex": { 2331 + "version": "3.0.0", 2332 + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 2333 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 2334 + "license": "MIT", 2335 + "engines": { 2336 + "node": ">=8" 2337 + } 2338 + }, 2339 + "node_modules/signal-exit": { 2340 + "version": "4.1.0", 2341 + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", 2342 + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", 2343 + "license": "ISC", 2344 + "engines": { 2345 + "node": ">=14" 2346 + }, 2347 + "funding": { 2348 + "url": "https://github.com/sponsors/isaacs" 2349 + } 2350 + }, 2351 + "node_modules/simple-concat": { 2352 + "version": "1.0.1", 2353 + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 2354 + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", 2355 + "funding": [ 2356 + { 2357 + "type": "github", 2358 + "url": "https://github.com/sponsors/feross" 2359 + }, 2360 + { 2361 + "type": "patreon", 2362 + "url": "https://www.patreon.com/feross" 2363 + }, 2364 + { 2365 + "type": "consulting", 2366 + "url": "https://feross.org/support" 2367 + } 2368 + ], 2369 + "license": "MIT" 2370 + }, 2371 + "node_modules/simple-get": { 2372 + "version": "4.0.1", 2373 + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", 2374 + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", 2375 + "funding": [ 2376 + { 2377 + "type": "github", 2378 + "url": "https://github.com/sponsors/feross" 2379 + }, 2380 + { 2381 + "type": "patreon", 2382 + "url": "https://www.patreon.com/feross" 2383 + }, 2384 + { 2385 + "type": "consulting", 2386 + "url": "https://feross.org/support" 2387 + } 2388 + ], 2389 + "license": "MIT", 2390 + "dependencies": { 2391 + "decompress-response": "^6.0.0", 2392 + "once": "^1.3.1", 2393 + "simple-concat": "^1.0.0" 2394 + } 2395 + }, 2396 + "node_modules/sonic-boom": { 2397 + "version": "4.2.1", 2398 + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", 2399 + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", 2400 + "license": "MIT", 2401 + "dependencies": { 2402 + "atomic-sleep": "^1.0.0" 2403 + } 2404 + }, 2405 + "node_modules/split2": { 2406 + "version": "4.2.0", 2407 + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", 2408 + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", 2409 + "license": "ISC", 2410 + "engines": { 2411 + "node": ">= 10.x" 2412 + } 2413 + }, 2414 + "node_modules/statuses": { 2415 + "version": "2.0.2", 2416 + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", 2417 + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", 2418 + "license": "MIT", 2419 + "engines": { 2420 + "node": ">= 0.8" 2421 + } 2422 + }, 2423 + "node_modules/string_decoder": { 2424 + "version": "1.3.0", 2425 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 2426 + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 2427 + "license": "MIT", 2428 + "dependencies": { 2429 + "safe-buffer": "~5.2.0" 2430 + } 2431 + }, 2432 + "node_modules/strip-json-comments": { 2433 + "version": "2.0.1", 2434 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 2435 + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", 2436 + "license": "MIT", 2437 + "engines": { 2438 + "node": ">=0.10.0" 2439 + } 2440 + }, 2441 + "node_modules/tar-fs": { 2442 + "version": "2.1.5", 2443 + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", 2444 + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", 2445 + "license": "MIT", 2446 + "dependencies": { 2447 + "chownr": "^1.1.1", 2448 + "mkdirp-classic": "^0.5.2", 2449 + "pump": "^3.0.0", 2450 + "tar-stream": "^2.1.4" 2451 + } 2452 + }, 2453 + "node_modules/tar-stream": { 2454 + "version": "2.2.0", 2455 + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 2456 + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 2457 + "license": "MIT", 2458 + "dependencies": { 2459 + "bl": "^4.0.3", 2460 + "end-of-stream": "^1.4.1", 2461 + "fs-constants": "^1.0.0", 2462 + "inherits": "^2.0.3", 2463 + "readable-stream": "^3.1.1" 2464 + }, 2465 + "engines": { 2466 + "node": ">=6" 2467 + } 2468 + }, 2469 + "node_modules/thread-stream": { 2470 + "version": "4.2.0", 2471 + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", 2472 + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", 2473 + "license": "MIT", 2474 + "dependencies": { 2475 + "real-require": "^1.0.0" 2476 + }, 2477 + "engines": { 2478 + "node": ">=20" 2479 + } 2480 + }, 2481 + "node_modules/thread-stream/node_modules/real-require": { 2482 + "version": "1.0.0", 2483 + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", 2484 + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", 2485 + "license": "MIT" 2486 + }, 2487 + "node_modules/toad-cache": { 2488 + "version": "3.7.4", 2489 + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", 2490 + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", 2491 + "license": "MIT", 2492 + "engines": { 2493 + "node": ">=20" 2494 + } 2495 + }, 2496 + "node_modules/toidentifier": { 2497 + "version": "1.0.1", 2498 + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 2499 + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 2500 + "license": "MIT", 2501 + "engines": { 2502 + "node": ">=0.6" 2503 + } 2504 + }, 2505 + "node_modules/tslib": { 2506 + "version": "2.8.1", 2507 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 2508 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 2509 + "license": "0BSD" 2510 + }, 2511 + "node_modules/tsx": { 2512 + "version": "4.23.0", 2513 + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz", 2514 + "integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==", 2515 + "dev": true, 2516 + "license": "MIT", 2517 + "dependencies": { 2518 + "esbuild": "~0.28.0" 2519 + }, 2520 + "bin": { 2521 + "tsx": "dist/cli.mjs" 2522 + }, 2523 + "engines": { 2524 + "node": ">=18.0.0" 2525 + }, 2526 + "optionalDependencies": { 2527 + "fsevents": "~2.3.3" 2528 + } 2529 + }, 2530 + "node_modules/tsyringe": { 2531 + "version": "4.10.0", 2532 + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", 2533 + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", 2534 + "license": "MIT", 2535 + "dependencies": { 2536 + "tslib": "^1.9.3" 2537 + }, 2538 + "engines": { 2539 + "node": ">= 6.0.0" 2540 + } 2541 + }, 2542 + "node_modules/tsyringe/node_modules/tslib": { 2543 + "version": "1.14.1", 2544 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 2545 + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", 2546 + "license": "0BSD" 2547 + }, 2548 + "node_modules/tunnel-agent": { 2549 + "version": "0.6.0", 2550 + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 2551 + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", 2552 + "license": "Apache-2.0", 2553 + "dependencies": { 2554 + "safe-buffer": "^5.0.1" 2555 + }, 2556 + "engines": { 2557 + "node": "*" 2558 + } 2559 + }, 2560 + "node_modules/typescript": { 2561 + "version": "5.9.3", 2562 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 2563 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 2564 + "dev": true, 2565 + "license": "Apache-2.0", 2566 + "bin": { 2567 + "tsc": "bin/tsc", 2568 + "tsserver": "bin/tsserver" 2569 + }, 2570 + "engines": { 2571 + "node": ">=14.17" 2572 + } 2573 + }, 2574 + "node_modules/undici-types": { 2575 + "version": "6.21.0", 2576 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 2577 + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 2578 + "dev": true, 2579 + "license": "MIT" 2580 + }, 2581 + "node_modules/util-deprecate": { 2582 + "version": "1.0.2", 2583 + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 2584 + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 2585 + "license": "MIT" 2586 + }, 2587 + "node_modules/which": { 2588 + "version": "2.0.2", 2589 + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 2590 + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 2591 + "license": "ISC", 2592 + "dependencies": { 2593 + "isexe": "^2.0.0" 2594 + }, 2595 + "bin": { 2596 + "node-which": "bin/node-which" 2597 + }, 2598 + "engines": { 2599 + "node": ">= 8" 2600 + } 2601 + }, 2602 + "node_modules/wrappy": { 2603 + "version": "1.0.2", 2604 + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 2605 + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 2606 + "license": "ISC" 2607 + }, 2608 + "node_modules/ws": { 2609 + "version": "8.21.0", 2610 + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", 2611 + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", 2612 + "license": "MIT", 2613 + "engines": { 2614 + "node": ">=10.0.0" 2615 + }, 2616 + "peerDependencies": { 2617 + "bufferutil": "^4.0.1", 2618 + "utf-8-validate": ">=5.0.2" 2619 + }, 2620 + "peerDependenciesMeta": { 2621 + "bufferutil": { 2622 + "optional": true 2623 + }, 2624 + "utf-8-validate": { 2625 + "optional": true 2626 + } 2627 + } 2628 + } 2629 + } 2630 + }
+36
server/package.json
··· 1 + { 2 + "name": "pds-operator-server", 3 + "version": "0.1.0", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "dev": "tsx watch src/index.ts", 8 + "build": "tsc -p tsconfig.json", 9 + "start": "node dist/index.js", 10 + "setup": "tsx src/cli.ts setup", 11 + "enroll": "tsx src/cli.ts enroll" 12 + }, 13 + "dependencies": { 14 + "@fastify/cookie": "^11.0.2", 15 + "@fastify/helmet": "^13.1.0", 16 + "@fastify/session": "^11.1.0", 17 + "@fastify/static": "^8.1.1", 18 + "@simplewebauthn/server": "^13.3.2", 19 + "bcryptjs": "^3.0.2", 20 + "better-sqlite3": "^12.11.1", 21 + "cbor-x": "^1.6.0", 22 + "dotenv": "^16.4.7", 23 + "fastify": "^5.2.1", 24 + "qrcode-terminal": "^0.12.0", 25 + "ws": "^8.18.0" 26 + }, 27 + "devDependencies": { 28 + "@types/bcryptjs": "^2.4.6", 29 + "@types/better-sqlite3": "^7.6.13", 30 + "@types/node": "^22.13.4", 31 + "@types/qrcode-terminal": "^0.12.2", 32 + "@types/ws": "^8.5.14", 33 + "tsx": "^4.19.2", 34 + "typescript": "^5.7.3" 35 + } 36 + }
+31
server/src/auditLog.ts
··· 1 + import { appendFile, readFile } from "node:fs/promises"; 2 + import path from "node:path"; 3 + import { fileURLToPath } from "node:url"; 4 + 5 + export interface AuditEntry { 6 + at: string; 7 + operator: string; 8 + action: string; 9 + target?: string; 10 + } 11 + 12 + // anchored to the server directory, not the process working directory 13 + const LOG_PATH = 14 + process.env.AUDIT_LOG_PATH ?? 15 + path.join(path.dirname(fileURLToPath(import.meta.url)), "../audit.log"); 16 + 17 + export async function recordAction(entry: Omit<AuditEntry, "at">) { 18 + const full: AuditEntry = { at: new Date().toISOString(), ...entry }; 19 + await appendFile(LOG_PATH, JSON.stringify(full) + "\n"); 20 + return full; 21 + } 22 + 23 + export async function readRecent(limit = 50): Promise<AuditEntry[]> { 24 + const raw = await readFile(LOG_PATH, "utf8").catch(() => ""); 25 + return raw 26 + .split("\n") 27 + .filter(Boolean) 28 + .slice(-limit) 29 + .map((line) => JSON.parse(line) as AuditEntry) 30 + .reverse(); 31 + }
+63
server/src/auth.ts
··· 1 + import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; 2 + import bcrypt from "bcryptjs"; 3 + 4 + declare module "@fastify/session" { 5 + interface FastifySessionObject { 6 + operator?: string; 7 + } 8 + } 9 + 10 + const MAX_ATTEMPTS = 5; 11 + const WINDOW_MS = 15 * 60 * 1000; 12 + 13 + export function registerAuthRoutes( 14 + app: FastifyInstance, 15 + passwordHash: string | null, 16 + appviewUrl: string, 17 + pdsHostname: string, 18 + ) { 19 + const attempts = new Map<string, { count: number; resetAt: number }>(); 20 + 21 + app.post("/api/login", async (req, reply) => { 22 + if (!passwordHash) { 23 + return reply.code(403).send({ error: "password sign-in is disabled" }); 24 + } 25 + const now = Date.now(); 26 + const entry = attempts.get(req.ip); 27 + if (entry && entry.resetAt <= now) attempts.delete(req.ip); 28 + if (entry && entry.resetAt > now && entry.count >= MAX_ATTEMPTS) { 29 + return reply.code(429).send({ error: "too many attempts — try again later" }); 30 + } 31 + 32 + const { password } = req.body as { password?: string }; 33 + if (!password || !bcrypt.compareSync(password, passwordHash)) { 34 + const current = attempts.get(req.ip); 35 + if (current && current.resetAt > now) current.count += 1; 36 + else attempts.set(req.ip, { count: 1, resetAt: now + WINDOW_MS }); 37 + return reply.code(401).send({ error: "invalid credentials" }); 38 + } 39 + attempts.delete(req.ip); 40 + req.session.operator = "operator"; 41 + return { ok: true }; 42 + }); 43 + 44 + app.post("/api/logout", async (req) => { 45 + await req.session.destroy(); 46 + return { ok: true }; 47 + }); 48 + 49 + app.get("/api/session", async (req) => { 50 + return { 51 + authenticated: Boolean(req.session.operator), 52 + appviewUrl, 53 + pdsHostname, 54 + passwordLogin: Boolean(passwordHash), 55 + }; 56 + }); 57 + } 58 + 59 + export async function requireAuth(req: FastifyRequest, reply: FastifyReply) { 60 + if (!req.session.operator) { 61 + reply.code(401).send({ error: "not authenticated" }); 62 + } 63 + }
+327
server/src/cli.ts
··· 1 + import "dotenv/config"; 2 + import { createInterface } from "node:readline/promises"; 3 + import { randomBytes } from "node:crypto"; 4 + import { existsSync, writeFileSync } from "node:fs"; 5 + import { spawn, type ChildProcess } from "node:child_process"; 6 + import path from "node:path"; 7 + import { fileURLToPath } from "node:url"; 8 + import bcrypt from "bcryptjs"; 9 + import qrcode from "qrcode-terminal"; 10 + import { openDb, createEnrollToken } from "./db.js"; 11 + 12 + const serverDir = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); 13 + const envPath = path.join(serverDir, ".env"); 14 + 15 + function printEnrollLink(base?: string) { 16 + const db = openDb(); 17 + const { token, expiresAt } = createEnrollToken(db); 18 + const resolvedBase = ( 19 + base ?? process.env.DASHBOARD_URL ?? `http://localhost:${process.env.PORT ?? 8787}` 20 + ).replace(/\/$/, ""); 21 + const url = `${resolvedBase}/?enroll=${token}`; 22 + const minutes = Math.round((expiresAt - Date.now()) / 60_000); 23 + 24 + console.log("\npasskey enrollment link (single use, expires in %d minutes):\n", minutes); 25 + console.log(` ${url}\n`); 26 + qrcode.generate(url, { small: true }); 27 + } 28 + 29 + /** 30 + * The enrollment link needs a running server (and, without a production build, the 31 + * vite dev server too). Start whatever is missing, wait for the API, then mint. 32 + */ 33 + async function startServerAndEnroll(port: string, dashboardUrl: string) { 34 + const children: ChildProcess[] = []; 35 + const apiUrl = `http://localhost:${port}/api/session`; 36 + 37 + // "ours" = the session endpoint answers with its expected shape. Any other 38 + // response means a different process owns the port (loopback hijack included). 39 + const probe = async (): Promise<"ours" | "other" | "none"> => { 40 + try { 41 + const res = await fetch(apiUrl); 42 + const body = await res.json().catch(() => null); 43 + return res.ok && body && "authenticated" in body ? "ours" : "other"; 44 + } catch { 45 + return "none"; 46 + } 47 + }; 48 + 49 + const initial = await probe(); 50 + if (initial === "other") { 51 + console.error( 52 + `something else is answering on localhost:${port} — pick a different PORT in .env and rerun`, 53 + ); 54 + return; 55 + } 56 + 57 + if (initial === "none") { 58 + const logs: string[] = []; 59 + const capture = (child: ChildProcess) => { 60 + for (const stream of [child.stdout, child.stderr]) { 61 + stream?.on("data", (chunk: Buffer) => { 62 + logs.push(...chunk.toString().split("\n").filter(Boolean)); 63 + if (logs.length > 30) logs.splice(0, logs.length - 30); 64 + }); 65 + } 66 + return child; 67 + }; 68 + const built = 69 + existsSync(path.join(serverDir, "dist/index.js")) && 70 + existsSync(path.join(serverDir, "../web/dist/index.html")); 71 + const opts = { stdio: ["ignore", "pipe", "pipe"] as ("ignore" | "pipe")[] }; 72 + if (built) { 73 + children.push(capture(spawn("node", ["dist/index.js"], { cwd: serverDir, ...opts }))); 74 + } else { 75 + children.push(capture(spawn("npx", ["tsx", "src/index.ts"], { cwd: serverDir, ...opts }))); 76 + children.push( 77 + capture(spawn("npm", ["run", "dev"], { cwd: path.join(serverDir, "../web"), ...opts })), 78 + ); 79 + } 80 + process.stdout.write("starting the server"); 81 + const deadline = Date.now() + 30_000; 82 + let state: "ours" | "other" | "none" = "none"; 83 + while (Date.now() < deadline) { 84 + state = await probe(); 85 + if (state !== "none") break; 86 + process.stdout.write("."); 87 + await new Promise((r) => setTimeout(r, 1000)); 88 + } 89 + console.log(""); 90 + if (state !== "ours") { 91 + for (const c of children) c.kill(); 92 + console.error("server didn't come up within 30s. last output:\n"); 93 + for (const line of logs.slice(-15)) console.error(` ${line}`); 94 + console.error("\nstart it yourself, then: npm run enroll"); 95 + return; 96 + } 97 + } 98 + 99 + printEnrollLink(dashboardUrl); 100 + 101 + if (children.length > 0) { 102 + const rl = createInterface({ input: process.stdin, output: process.stdout }); 103 + await rl.question("\npress enter when you've enrolled (stops the temporary server): "); 104 + rl.close(); 105 + for (const c of children) c.kill(); 106 + console.log("done. start the server normally with npm run dev (or npm start)."); 107 + } 108 + } 109 + 110 + async function setup() { 111 + const rl = createInterface({ input: process.stdin, output: process.stdout }); 112 + // buffer lines ourselves: readline drops input that arrives between questions, 113 + // which breaks piped/scripted runs 114 + const buffered: string[] = []; 115 + const waiting: ((line: string) => void)[] = []; 116 + let closed = false; 117 + rl.on("line", (line) => { 118 + const next = waiting.shift(); 119 + if (next) next(line); 120 + else buffered.push(line); 121 + }); 122 + rl.on("close", () => { 123 + closed = true; 124 + while (waiting.length > 0) waiting.shift()!(""); 125 + }); 126 + const readAnswer = (): Promise<string> => { 127 + const line = buffered.shift(); 128 + if (line !== undefined) return Promise.resolve(line); 129 + if (closed) return Promise.resolve(""); 130 + return new Promise((resolve) => waiting.push(resolve)); 131 + }; 132 + // prompts go through readline's own prompt state so line refreshes 133 + // (backspace, arrows) redraw correctly 134 + const ask = async (q: string, fallback = ""): Promise<string> => { 135 + rl.setPrompt(fallback ? `${q} [${fallback}]: ` : `${q}: `); 136 + rl.prompt(); 137 + const answer = (await readAnswer()).trim(); 138 + return answer || fallback; 139 + }; 140 + // sudo-style hidden prompt: mute stdout while the secret is typed, so 141 + // readline's echo goes nowhere (readline/promises has no _writeToOutput hook) 142 + const askHidden = async (q: string): Promise<string> => { 143 + rl.setPrompt(`${q}: `); 144 + rl.prompt(); 145 + const originalWrite = process.stdout.write.bind(process.stdout); 146 + (process.stdout as { write: unknown }).write = () => true; 147 + try { 148 + return (await readAnswer()).trim(); 149 + } finally { 150 + (process.stdout as { write: unknown }).write = originalWrite; 151 + originalWrite("\n"); 152 + } 153 + }; 154 + 155 + console.log("pds operator setup\n"); 156 + 157 + if (existsSync(envPath)) { 158 + const overwrite = await ask(`${envPath} exists, overwrite? (y/N)`, "n"); 159 + if (overwrite.toLowerCase() !== "y") { 160 + rl.close(); 161 + console.log("leaving .env alone."); 162 + return; 163 + } 164 + } 165 + 166 + const pdsHostname = await ask("pds hostname (e.g. pds.example.com)"); 167 + const pdsAdminPassword = await askHidden("pds admin password"); 168 + const relayHostname = await ask("relay hostname", "bsky.network"); 169 + const appviewUrl = await ask("appview url for profile links", "https://bsky.app"); 170 + const dashboardUrl = await ask("dashboard url (used in DM deep links)", "http://localhost:5173"); 171 + const port = await ask("api port", "8787"); 172 + 173 + console.log("\noperator password is optional. leave empty for passkey-only sign-in"); 174 + console.log("(recovery is shell access: npm run enroll mints a new passkey link)."); 175 + const operatorPassword = await askHidden("operator password (empty for passkey-only)"); 176 + 177 + console.log("\nflag alerts via bluesky dm. leave the handle empty to skip."); 178 + const notifyHandle = await ask("sender handle (needs an app password with DM access)"); 179 + let notifyAppPassword = ""; 180 + let notifyRecipient = ""; 181 + if (notifyHandle) { 182 + notifyAppPassword = await askHidden("sender app password"); 183 + notifyRecipient = await ask("recipient handle"); 184 + } 185 + 186 + const lines = [ 187 + `PDS_HOSTNAME=${pdsHostname}`, 188 + `PDS_ADMIN_PASSWORD=${pdsAdminPassword}`, 189 + `RELAY_HOSTNAME=${relayHostname}`, 190 + operatorPassword 191 + ? `OPERATOR_PASSWORD_HASH=${bcrypt.hashSync(operatorPassword, 10)}` 192 + : `# passkey-only: no OPERATOR_PASSWORD_HASH`, 193 + `SESSION_SECRET=${randomBytes(32).toString("hex")}`, 194 + `PORT=${port}`, 195 + `APPVIEW_URL=${appviewUrl}`, 196 + `DASHBOARD_URL=${dashboardUrl}`, 197 + `NOTIFY_HANDLE=${notifyHandle}`, 198 + `NOTIFY_APP_PASSWORD=${notifyAppPassword}`, 199 + `NOTIFY_RECIPIENT=${notifyRecipient}`, 200 + "", 201 + ]; 202 + writeFileSync(envPath, lines.join("\n"), { mode: 0o600 }); 203 + console.log(`\nwrote ${envPath}`); 204 + console.log("labelers: copy labelers.json.example to labelers.json and edit the watchlist."); 205 + 206 + const flyToml = path.join(serverDir, "../fly.toml"); 207 + let flyGenerated = false; 208 + const wantFly = (await ask("\ngenerate fly.toml for deploying to fly.io? (y/N)", "n")) 209 + .toLowerCase(); 210 + if (wantFly === "y" && (!existsSync(flyToml) || 211 + (await ask(`${flyToml} exists, overwrite? (y/N)`, "n")).toLowerCase() === "y")) { 212 + flyGenerated = true; 213 + const appName = await ask("fly app name", "pds-operator"); 214 + const region = await ask("fly region", "iad"); 215 + const flyUrl = await ask("public dashboard url", `https://${appName}.fly.dev`); 216 + 217 + writeFileSync( 218 + flyToml, 219 + `app = "${appName}" 220 + primary_region = "${region}" 221 + 222 + [build] 223 + 224 + [env] 225 + HOST = "0.0.0.0" 226 + PORT = "8787" 227 + PDS_HOSTNAME = "${pdsHostname}" 228 + RELAY_HOSTNAME = "${relayHostname}" 229 + APPVIEW_URL = "${appviewUrl}" 230 + DASHBOARD_URL = "${flyUrl.replace(/\/$/, "")}" 231 + DB_PATH = "/data/data.sqlite" 232 + AUDIT_LOG_PATH = "/data/audit.log" 233 + 234 + [mounts] 235 + source = "data" 236 + destination = "/data" 237 + 238 + [http_service] 239 + internal_port = 8787 240 + force_https = true 241 + # the syncer's streams and DM alerts need the machine always on 242 + auto_stop_machines = "off" 243 + auto_start_machines = true 244 + min_machines_running = 1 245 + 246 + [[vm]] 247 + size = "shared-cpu-1x" 248 + memory = "512mb" 249 + `, 250 + ); 251 + console.log(`\nwrote ${flyToml}. deploy steps:\n`); 252 + const secrets = [ 253 + `PDS_ADMIN_PASSWORD=${pdsAdminPassword}`, 254 + `SESSION_SECRET=${randomBytes(32).toString("hex")}`, 255 + ...(operatorPassword 256 + ? [`OPERATOR_PASSWORD_HASH=${bcrypt.hashSync(operatorPassword, 10)}`] 257 + : []), 258 + ...(notifyHandle 259 + ? [ 260 + `NOTIFY_HANDLE=${notifyHandle}`, 261 + `NOTIFY_APP_PASSWORD=${notifyAppPassword}`, 262 + `NOTIFY_RECIPIENT=${notifyRecipient}`, 263 + ] 264 + : []), 265 + ]; 266 + console.log(` fly apps create ${appName}`); 267 + console.log(` fly volumes create data -a ${appName} --region ${region} --size 1 --yes`); 268 + console.log(` fly secrets set -a ${appName} --stage <values from your answers>`); 269 + console.log(" fly deploy --remote-only"); 270 + console.log(` fly ssh console -a ${appName} -C "node dist/cli.js enroll" # passkey`); 271 + 272 + const deployNow = (await ask("\nrun these now? (y/N)", "n")).toLowerCase(); 273 + if (deployNow === "y") { 274 + const repoRoot = path.join(serverDir, ".."); 275 + const run = (args: string[]) => 276 + new Promise<number>((resolve) => { 277 + console.log(`\n$ fly ${args.join(" ").slice(0, 80)}${args.join(" ").length > 80 ? " …" : ""}`); 278 + const child = spawn("fly", args, { stdio: "inherit", cwd: repoRoot }); 279 + child.on("error", () => resolve(127)); 280 + child.on("exit", (code) => resolve(code ?? 1)); 281 + }); 282 + 283 + if ((await run(["version"])) !== 0) { 284 + console.error("\nflyctl not found — install it (https://fly.io/docs/flyctl/install/),"); 285 + console.error("sign in with `fly auth login`, then run the steps above."); 286 + } else { 287 + // create/volume may already exist on a rerun — carry on 288 + await run(["apps", "create", appName]); 289 + await run(["volumes", "create", "data", "-a", appName, "--region", region, "--size", "1", "--yes"]); 290 + const staged = await run(["secrets", "set", "-a", appName, "--stage", ...secrets]); 291 + const deployed = staged === 0 ? await run(["deploy", "--remote-only"]) : 1; 292 + if (deployed === 0) { 293 + console.log("\ndeployed. minting a passkey enrollment link on the fly machine:"); 294 + await run(["ssh", "console", "-a", appName, "-C", "node dist/cli.js enroll"]); 295 + console.log(`\nopen the link above to enroll. your dashboard: ${flyUrl}`); 296 + } else { 297 + console.error("\ndeploy didn't finish — fix the error above and rerun the steps."); 298 + } 299 + } 300 + } 301 + } 302 + 303 + if (flyGenerated) { 304 + // a locally minted token lands in the local sqlite and passkeys bind to the 305 + // local hostname — for fly, enrollment has to happen on the deployed machine 306 + rl.close(); 307 + return; 308 + } 309 + 310 + const enrollNow = ( 311 + await ask("\nstart the server and mint a passkey enrollment link? (y/N)", "n") 312 + ).toLowerCase(); 313 + rl.close(); 314 + if (enrollNow === "y") await startServerAndEnroll(port, dashboardUrl); 315 + else console.log("\nlater: npm run enroll (with the server running)"); 316 + } 317 + 318 + const command = process.argv[2]; 319 + if (command === "setup") { 320 + await setup(); 321 + } else if (command === "enroll") { 322 + printEnrollLink(); 323 + console.log("\nthe server has to be running for the link to work."); 324 + } else { 325 + console.log("usage: cli <setup|enroll>"); 326 + process.exit(1); 327 + }
+118
server/src/db.ts
··· 1 + import Database from "better-sqlite3"; 2 + import path from "node:path"; 3 + import { fileURLToPath } from "node:url"; 4 + import { createHash, randomBytes } from "node:crypto"; 5 + 6 + export interface AccountRow { 7 + did: string; 8 + handle: string; 9 + email: string | null; 10 + indexed_at: string; 11 + status: "active" | "takendown" | "deactivated"; 12 + avatar: string | null; 13 + } 14 + 15 + export interface LabelRow { 16 + did: string; 17 + src: string; 18 + val: string; 19 + cts: string; 20 + } 21 + 22 + export function openDb(file?: string) { 23 + const dbPath = 24 + file ?? 25 + process.env.DB_PATH ?? 26 + path.join(path.dirname(fileURLToPath(import.meta.url)), "../data.sqlite"); 27 + const db = new Database(dbPath); 28 + db.pragma("journal_mode = WAL"); 29 + db.exec(` 30 + CREATE TABLE IF NOT EXISTS accounts ( 31 + did TEXT PRIMARY KEY, 32 + handle TEXT NOT NULL DEFAULT '(unknown)', 33 + email TEXT, 34 + indexed_at TEXT NOT NULL DEFAULT '', 35 + status TEXT NOT NULL DEFAULT 'active', 36 + avatar TEXT, 37 + updated_at INTEGER NOT NULL 38 + ); 39 + CREATE INDEX IF NOT EXISTS accounts_indexed_at ON accounts(indexed_at DESC); 40 + 41 + CREATE TABLE IF NOT EXISTS labels ( 42 + did TEXT NOT NULL, 43 + src TEXT NOT NULL, 44 + val TEXT NOT NULL, 45 + cts TEXT NOT NULL, 46 + PRIMARY KEY (did, src, val) 47 + ); 48 + CREATE INDEX IF NOT EXISTS labels_did ON labels(did); 49 + 50 + CREATE TABLE IF NOT EXISTS sync_state ( 51 + key TEXT PRIMARY KEY, 52 + value TEXT NOT NULL 53 + ); 54 + 55 + CREATE TABLE IF NOT EXISTS passkeys ( 56 + id TEXT PRIMARY KEY, 57 + public_key BLOB NOT NULL, 58 + counter INTEGER NOT NULL, 59 + transports TEXT, 60 + name TEXT, 61 + created_at INTEGER NOT NULL 62 + ); 63 + 64 + CREATE TABLE IF NOT EXISTS enroll_tokens ( 65 + token_hash TEXT PRIMARY KEY, 66 + expires_at INTEGER NOT NULL 67 + ); 68 + `); 69 + return db; 70 + } 71 + 72 + export type Db = ReturnType<typeof openDb>; 73 + 74 + export function getSyncState(db: Db, key: string): string | null { 75 + const row = db.prepare("SELECT value FROM sync_state WHERE key = ?").get(key) as 76 + | { value: string } 77 + | undefined; 78 + return row?.value ?? null; 79 + } 80 + 81 + export function setSyncState(db: Db, key: string, value: string) { 82 + db.prepare( 83 + "INSERT INTO sync_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", 84 + ).run(key, value); 85 + } 86 + 87 + // ---- one-time passkey enrollment tokens (minted by the CLI, hashed at rest) ---- 88 + 89 + const ENROLL_TOKEN_TTL_MS = 15 * 60 * 1000; 90 + 91 + function hashToken(token: string): string { 92 + return createHash("sha256").update(token).digest("hex"); 93 + } 94 + 95 + export function createEnrollToken(db: Db): { token: string; expiresAt: number } { 96 + db.prepare("DELETE FROM enroll_tokens WHERE expires_at <= ?").run(Date.now()); 97 + const token = randomBytes(32).toString("base64url"); 98 + const expiresAt = Date.now() + ENROLL_TOKEN_TTL_MS; 99 + db.prepare("INSERT INTO enroll_tokens (token_hash, expires_at) VALUES (?, ?)").run( 100 + hashToken(token), 101 + expiresAt, 102 + ); 103 + return { token, expiresAt }; 104 + } 105 + 106 + export function checkEnrollToken(db: Db, token: string): boolean { 107 + const row = db 108 + .prepare("SELECT 1 FROM enroll_tokens WHERE token_hash = ? AND expires_at > ?") 109 + .get(hashToken(token), Date.now()); 110 + return Boolean(row); 111 + } 112 + 113 + export function consumeEnrollToken(db: Db, token: string): boolean { 114 + const res = db 115 + .prepare("DELETE FROM enroll_tokens WHERE token_hash = ? AND expires_at > ?") 116 + .run(hashToken(token), Date.now()); 117 + return res.changes > 0; 118 + }
+164
server/src/index.ts
··· 1 + import "dotenv/config"; 2 + import Fastify from "fastify"; 3 + import fastifyCookie from "@fastify/cookie"; 4 + import fastifySession from "@fastify/session"; 5 + import fastifyStatic from "@fastify/static"; 6 + import fastifyHelmet from "@fastify/helmet"; 7 + import { fileURLToPath } from "node:url"; 8 + import path from "node:path"; 9 + import { existsSync, readFileSync } from "node:fs"; 10 + 11 + import { PdsClient } from "./pdsClient.js"; 12 + import { RelayClient } from "./relayClient.js"; 13 + import { LabelerClient, fetchLabelNames, type WatchedLabeler } from "./labelerClient.js"; 14 + import { openDb } from "./db.js"; 15 + import { Syncer } from "./sync.js"; 16 + import { registerAuthRoutes } from "./auth.js"; 17 + import { registerAccountRoutes } from "./routes/accounts.js"; 18 + import { registerStatusRoutes } from "./routes/status.js"; 19 + import { registerPasskeyRoutes } from "./routes/passkeys.js"; 20 + import { registerAuditRoutes } from "./routes/audit.js"; 21 + import { registerInviteRoutes } from "./routes/invites.js"; 22 + import { BskyDmNotifier } from "./notifier.js"; 23 + 24 + const { 25 + PDS_HOSTNAME, 26 + PDS_ADMIN_PASSWORD, 27 + RELAY_HOSTNAME, 28 + OPERATOR_PASSWORD_HASH, 29 + SESSION_SECRET, 30 + PORT, 31 + } = process.env; 32 + 33 + for (const [name, val] of Object.entries({ 34 + PDS_HOSTNAME, 35 + PDS_ADMIN_PASSWORD, 36 + RELAY_HOSTNAME, 37 + SESSION_SECRET, 38 + })) { 39 + if (!val) throw new Error(`missing required env var: ${name}`); 40 + } 41 + 42 + const isProd = process.env.NODE_ENV === "production"; 43 + 44 + // forged operator sessions are game over — refuse to boot on a weak secret 45 + if (isProd && (SESSION_SECRET!.length < 32 || SESSION_SECRET!.includes("not-for-prod"))) { 46 + throw new Error("SESSION_SECRET must be a strong random value in production (openssl rand -hex 32)"); 47 + } 48 + 49 + // trustProxy: in production the app binds to loopback behind a TLS-terminating 50 + // reverse proxy, so X-Forwarded-For is trustworthy and req.ip (login rate 51 + // limiting) reflects the real client 52 + const app = Fastify({ logger: true, trustProxy: true }); 53 + 54 + await app.register(fastifyHelmet, { 55 + contentSecurityPolicy: { 56 + directives: { 57 + defaultSrc: ["'self'"], 58 + scriptSrc: ["'self'"], 59 + styleSrc: ["'self'", "'unsafe-inline'"], 60 + imgSrc: ["'self'", "https:", "data:"], // avatars come from the appview CDN 61 + connectSrc: ["'self'"], 62 + frameAncestors: ["'none'"], 63 + }, 64 + }, 65 + }); 66 + 67 + await app.register(fastifyCookie); 68 + await app.register(fastifySession, { 69 + secret: SESSION_SECRET!, 70 + cookie: { 71 + secure: isProd, 72 + httpOnly: true, 73 + sameSite: "lax", 74 + maxAge: 1000 * 60 * 60 * 12, 75 + }, 76 + }); 77 + 78 + const pds = new PdsClient(PDS_HOSTNAME!, PDS_ADMIN_PASSWORD!); 79 + const relay = new RelayClient(RELAY_HOSTNAME!); 80 + // labelers.json: [{ "name"?, "did", "labels": [...] }] — labels empty/omitted means all labels flag. 81 + // Falls back to LABELER_DID / FLAG_LABELS env vars if the file doesn't exist. 82 + const labelersPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "../labelers.json"); 83 + let labelerConfigs: { did: string; labels?: string[] }[] = []; 84 + if (existsSync(labelersPath)) { 85 + labelerConfigs = JSON.parse(readFileSync(labelersPath, "utf8")); 86 + } else if (process.env.LABELER_DID) { 87 + labelerConfigs = [ 88 + { 89 + did: process.env.LABELER_DID, 90 + labels: (process.env.FLAG_LABELS ?? "").split(",").map((s) => s.trim()).filter(Boolean), 91 + }, 92 + ]; 93 + } 94 + const labelers: WatchedLabeler[] = labelerConfigs.map((c) => ({ 95 + did: c.did, 96 + client: new LabelerClient(c.did), 97 + watch: new Set(c.labels ?? []), 98 + })); 99 + 100 + // flag notifications via Bluesky DM — enabled only when all four vars are set 101 + const { NOTIFY_HANDLE, NOTIFY_APP_PASSWORD, NOTIFY_RECIPIENT, DASHBOARD_URL } = process.env; 102 + const notifier = 103 + NOTIFY_HANDLE && NOTIFY_APP_PASSWORD && NOTIFY_RECIPIENT && DASHBOARD_URL 104 + ? new BskyDmNotifier( 105 + { 106 + handle: NOTIFY_HANDLE, 107 + appPassword: NOTIFY_APP_PASSWORD, 108 + recipient: NOTIFY_RECIPIENT, 109 + dashboardUrl: DASHBOARD_URL.replace(/\/$/, ""), 110 + }, 111 + app.log, 112 + ) 113 + : null; 114 + if (!notifier) app.log.info("flag DM notifications disabled (NOTIFY_* env vars not set)"); 115 + 116 + // human-readable label names per labeler (src -> val -> display name), loaded in the 117 + // background; raw identifiers are shown until each fetch lands 118 + const labelNames = new Map<string, Map<string, string>>(); 119 + for (const l of labelers) { 120 + void fetchLabelNames(l.did) 121 + .then((names) => labelNames.set(l.did, names)) 122 + .catch((err) => app.log.warn({ err, labeler: l.did }, "label names fetch failed")); 123 + } 124 + 125 + const db = openDb(); 126 + const syncer = new Syncer(db, pds, labelers, app.log, notifier, labelNames); 127 + syncer.start(); 128 + 129 + const appviewUrl = (process.env.APPVIEW_URL ?? "https://bsky.app").replace(/\/$/, ""); 130 + 131 + // no password hash = passkey-only; the first passkey comes from `npm run enroll` 132 + if (!OPERATOR_PASSWORD_HASH) { 133 + const enrolled = db.prepare("SELECT COUNT(*) AS c FROM passkeys").get() as { c: number }; 134 + app.log.info( 135 + enrolled.c > 0 136 + ? "password sign-in disabled (passkey-only)" 137 + : "password sign-in disabled and no passkeys enrolled — run `npm run enroll`", 138 + ); 139 + } 140 + 141 + registerAuthRoutes(app, OPERATOR_PASSWORD_HASH ?? null, appviewUrl, PDS_HOSTNAME!); 142 + registerAccountRoutes(app, pds, db, labelers, labelNames); 143 + registerStatusRoutes(app, pds, relay, PDS_HOSTNAME!, db); 144 + registerPasskeyRoutes(app, db, PDS_HOSTNAME!); 145 + registerAuditRoutes(app); 146 + registerInviteRoutes(app, pds); 147 + 148 + const webDist = path.join(path.dirname(fileURLToPath(import.meta.url)), "../../web/dist"); 149 + await app.register(fastifyStatic, { root: webDist }); 150 + app.setNotFoundHandler((req, reply) => { 151 + if (req.raw.url?.startsWith("/api/")) { 152 + reply.code(404).send({ error: "not found" }); 153 + } else { 154 + reply.sendFile("index.html"); 155 + } 156 + }); 157 + 158 + const port = Number(PORT ?? 8787); 159 + // production sits behind a reverse proxy — don't expose the app port directly 160 + const host = process.env.HOST ?? (isProd ? "127.0.0.1" : "0.0.0.0"); 161 + app.listen({ port, host }).catch((err) => { 162 + app.log.error(err); 163 + process.exit(1); 164 + });
+108
server/src/labelerClient.ts
··· 1 + export interface Label { 2 + src: string; 3 + uri: string; 4 + val: string; 5 + cts: string; 6 + neg?: boolean; 7 + exp?: string; 8 + } 9 + 10 + /** A labeler to listen to, plus which of its label values count as flags (empty = all). */ 11 + export interface WatchedLabeler { 12 + did: string; 13 + client: LabelerClient; 14 + watch: Set<string>; 15 + } 16 + 17 + export class LabelerClient { 18 + private endpoint: string | null = null; 19 + 20 + constructor(public readonly did: string) {} 21 + 22 + async resolveEndpoint(): Promise<string> { 23 + if (this.endpoint) return this.endpoint; 24 + const res = await fetch(`https://plc.directory/${this.did}`); 25 + if (!res.ok) throw new Error(`labeler DID resolution failed: ${res.status}`); 26 + const doc = (await res.json()) as { 27 + service?: { id: string; type: string; serviceEndpoint: string }[]; 28 + }; 29 + const svc = doc.service?.find((s) => s.id === "#atproto_labeler" || s.type === "AtprotoLabeler"); 30 + if (!svc) throw new Error(`no #atproto_labeler service in DID document for ${this.did}`); 31 + this.endpoint = svc.serviceEndpoint; 32 + return this.endpoint; 33 + } 34 + 35 + /** 36 + * Query current labels for a batch of URIs (DIDs). Ozone silently returns an empty 37 + * result — not an error — past 20 uriPatterns per query, so callers must batch at ≤20. 38 + */ 39 + async queryLabels(uris: string[]): Promise<Label[]> { 40 + const endpoint = await this.resolveEndpoint(); 41 + const params = new URLSearchParams({ limit: "250" }); 42 + for (const uri of uris) params.append("uriPatterns", uri); 43 + const res = await fetch(`${endpoint}/xrpc/com.atproto.label.queryLabels?${params}`); 44 + if (!res.ok) throw new Error(`labeler queryLabels failed: ${res.status}`); 45 + const { labels } = (await res.json()) as { labels: Label[] }; 46 + return labels; 47 + } 48 + } 49 + 50 + /** 51 + * Fetch a labeler's published label definitions (app.bsky.labeler.service/self on its PDS) 52 + * and map label identifiers to human-readable names, e.g. 53 + * "platform-manipulation" → "Platform Abuse & Manipulation". 54 + */ 55 + export async function fetchLabelNames(did: string): Promise<Map<string, string>> { 56 + const docUrl = did.startsWith("did:web:") 57 + ? `https://${did.slice("did:web:".length)}/.well-known/did.json` 58 + : `https://plc.directory/${did}`; 59 + const docRes = await fetch(docUrl); 60 + if (!docRes.ok) throw new Error(`DID doc fetch for ${did} failed: ${docRes.status}`); 61 + const doc = (await docRes.json()) as { 62 + service?: { id: string; type: string; serviceEndpoint: string }[]; 63 + }; 64 + const pds = doc.service?.find( 65 + (s) => s.id === "#atproto_pds" || s.type === "AtprotoPersonalDataServer", 66 + ); 67 + if (!pds) throw new Error(`no #atproto_pds service in DID doc for ${did}`); 68 + 69 + const recRes = await fetch( 70 + `${pds.serviceEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=app.bsky.labeler.service&rkey=self`, 71 + ); 72 + if (!recRes.ok) throw new Error(`labeler service record fetch failed: ${recRes.status}`); 73 + const record = (await recRes.json()) as { 74 + value?: { 75 + policies?: { 76 + labelValueDefinitions?: { 77 + identifier: string; 78 + locales?: { lang: string; name: string }[]; 79 + }[]; 80 + }; 81 + }; 82 + }; 83 + 84 + const names = new Map<string, string>(); 85 + for (const def of record.value?.policies?.labelValueDefinitions ?? []) { 86 + const locale = 87 + def.locales?.find((l) => l.lang?.startsWith("en")) ?? def.locales?.[0]; 88 + if (locale?.name) names.set(def.identifier, locale.name); 89 + } 90 + return names; 91 + } 92 + 93 + /** Reduce raw labels to the currently-active set per URI (negations and expirations applied). */ 94 + export function activeLabels(raw: Label[], src: string): Map<string, Map<string, string>> { 95 + // apply in timestamp order so a later neg (retraction) cancels an earlier label 96 + const sorted = [...raw].sort((a, b) => a.cts.localeCompare(b.cts)); 97 + const nowIso = new Date().toISOString(); 98 + const byUri = new Map<string, Map<string, string>>(); // uri -> val -> cts 99 + for (const label of sorted) { 100 + if (label.src !== src) continue; 101 + if (label.exp && label.exp <= nowIso) continue; 102 + const vals = byUri.get(label.uri) ?? new Map<string, string>(); 103 + if (label.neg) vals.delete(label.val); 104 + else vals.set(label.val, label.cts); 105 + byUri.set(label.uri, vals); 106 + } 107 + return byUri; 108 + }
+139
server/src/notifier.ts
··· 1 + import type { FastifyBaseLogger } from "fastify"; 2 + 3 + const CHAT_PROXY = "did:web:api.bsky.chat#bsky_chat"; 4 + 5 + export interface NotifierConfig { 6 + /** handle of the account that sends the DMs (linked via app password) */ 7 + handle: string; 8 + /** app password for the sender — must be created with DM access enabled */ 9 + appPassword: string; 10 + /** handle that receives the flag notifications */ 11 + recipient: string; 12 + /** base URL of this dashboard, used to build deep links */ 13 + dashboardUrl: string; 14 + } 15 + 16 + interface Facet { 17 + index: { byteStart: number; byteEnd: number }; 18 + features: Record<string, unknown>[]; 19 + } 20 + 21 + async function resolveHandleToDid(handle: string): Promise<string> { 22 + const res = await fetch( 23 + `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`, 24 + ); 25 + if (!res.ok) throw new Error(`resolveHandle(${handle}) failed: ${res.status}`); 26 + const { did } = (await res.json()) as { did: string }; 27 + return did; 28 + } 29 + 30 + async function resolvePdsEndpoint(did: string): Promise<string> { 31 + const url = did.startsWith("did:web:") 32 + ? `https://${did.slice("did:web:".length)}/.well-known/did.json` 33 + : `https://plc.directory/${did}`; 34 + const res = await fetch(url); 35 + if (!res.ok) throw new Error(`DID doc fetch for ${did} failed: ${res.status}`); 36 + const doc = (await res.json()) as { 37 + service?: { id: string; type: string; serviceEndpoint: string }[]; 38 + }; 39 + const svc = doc.service?.find( 40 + (s) => s.id === "#atproto_pds" || s.type === "AtprotoPersonalDataServer", 41 + ); 42 + if (!svc) throw new Error(`no #atproto_pds service in DID doc for ${did}`); 43 + return svc.serviceEndpoint; 44 + } 45 + 46 + export class BskyDmNotifier { 47 + private session: { pds: string; accessJwt: string } | null = null; 48 + private recipientDid: string | null = null; 49 + private convoId: string | null = null; 50 + 51 + constructor( 52 + private cfg: NotifierConfig, 53 + private log: FastifyBaseLogger, 54 + ) {} 55 + 56 + /** Resolve the sender's DID + PDS from their handle, then create a session there. */ 57 + private async ensureSession(): Promise<{ pds: string; accessJwt: string }> { 58 + if (this.session) return this.session; 59 + const did = await resolveHandleToDid(this.cfg.handle); 60 + const pds = await resolvePdsEndpoint(did); 61 + const res = await fetch(`${pds}/xrpc/com.atproto.server.createSession`, { 62 + method: "POST", 63 + headers: { "Content-Type": "application/json" }, 64 + body: JSON.stringify({ identifier: did, password: this.cfg.appPassword }), 65 + }); 66 + if (!res.ok) { 67 + const body = await res.text().catch(() => ""); 68 + throw new Error(`createSession for ${this.cfg.handle} failed: ${res.status} ${body}`); 69 + } 70 + const { accessJwt } = (await res.json()) as { accessJwt: string }; 71 + this.session = { pds, accessJwt }; 72 + return this.session; 73 + } 74 + 75 + /** Chat XRPC via the sender's PDS, service-proxied to the Bluesky chat appview. */ 76 + private async chatXrpc(path: string, opts: RequestInit = {}, retry = true): Promise<any> { 77 + const { pds, accessJwt } = await this.ensureSession(); 78 + const res = await fetch(`${pds}/xrpc/${path}`, { 79 + ...opts, 80 + headers: { 81 + ...opts.headers, 82 + Authorization: `Bearer ${accessJwt}`, 83 + "atproto-proxy": CHAT_PROXY, 84 + "Content-Type": "application/json", 85 + }, 86 + }); 87 + if (res.status === 401 && retry) { 88 + this.session = null; // token expired — re-auth once 89 + return this.chatXrpc(path, opts, false); 90 + } 91 + if (!res.ok) { 92 + const body = await res.text().catch(() => ""); 93 + throw new Error(`chat ${path} failed: ${res.status} ${body}`); 94 + } 95 + return res.json(); 96 + } 97 + 98 + private async ensureConvo(): Promise<string> { 99 + if (this.convoId) return this.convoId; 100 + this.recipientDid ??= await resolveHandleToDid(this.cfg.recipient); 101 + const { convo } = await this.chatXrpc( 102 + `chat.bsky.convo.getConvoForMembers?members=${encodeURIComponent(this.recipientDid)}`, 103 + ); 104 + this.convoId = convo.id as string; 105 + return this.convoId; 106 + } 107 + 108 + async notifyFlags(account: { did: string; handle: string }, labels: string[]): Promise<void> { 109 + try { 110 + const convoId = await this.ensureConvo(); 111 + 112 + // rich text with byte-offset facets: mention the account, link to the dashboard 113 + let text = ""; 114 + const facets: Facet[] = []; 115 + const plain = (s: string) => { 116 + text += s; 117 + }; 118 + const faceted = (s: string, feature: Record<string, unknown>) => { 119 + const byteStart = Buffer.byteLength(text); 120 + text += s; 121 + facets.push({ index: { byteStart, byteEnd: Buffer.byteLength(text) }, features: [feature] }); 122 + }; 123 + 124 + const link = `${this.cfg.dashboardUrl}/?q=${encodeURIComponent(account.handle)}`; 125 + plain("⚠️ "); 126 + faceted(`@${account.handle}`, { $type: "app.bsky.richtext.facet#mention", did: account.did }); 127 + plain(` flagged: ${labels.join(", ")}\n`); 128 + faceted(link, { $type: "app.bsky.richtext.facet#link", uri: link }); 129 + 130 + await this.chatXrpc("chat.bsky.convo.sendMessage", { 131 + method: "POST", 132 + body: JSON.stringify({ convoId, message: { text, facets } }), 133 + }); 134 + this.log.info({ account: account.handle, labels }, "flag notification sent"); 135 + } catch (err) { 136 + this.log.error({ err, account: account.handle }, "flag notification failed"); 137 + } 138 + } 139 + }
+191
server/src/pdsClient.ts
··· 1 + import WebSocket from "ws"; 2 + import { decodeMultiple } from "cbor-x"; 3 + 4 + export interface RepoEntry { 5 + did: string; 6 + active?: boolean; 7 + status?: string; 8 + } 9 + 10 + export interface AccountInfo { 11 + did: string; 12 + handle: string; 13 + email?: string; 14 + indexedAt: string; 15 + } 16 + 17 + export interface InviteCode { 18 + code: string; 19 + available: number; 20 + disabled: boolean; 21 + forAccount: string; 22 + createdBy: string; 23 + createdAt: string; 24 + uses: { usedBy: string; usedAt: string }[]; 25 + } 26 + 27 + export interface AdminAccount { 28 + did: string; 29 + handle: string; 30 + email?: string; 31 + indexedAt: string; 32 + status?: "active" | "takendown" | "deactivated"; 33 + avatar?: string; 34 + labels?: string[]; 35 + } 36 + 37 + export class PdsClient { 38 + constructor( 39 + public readonly hostname: string, 40 + private adminPassword: string, 41 + ) {} 42 + 43 + private authHeader() { 44 + const token = Buffer.from(`admin:${this.adminPassword}`).toString("base64"); 45 + return `Basic ${token}`; 46 + } 47 + 48 + private async xrpc(path: string, opts: RequestInit = {}) { 49 + const res = await fetch(`https://${this.hostname}/xrpc/${path}`, { 50 + ...opts, 51 + headers: { 52 + ...opts.headers, 53 + Authorization: this.authHeader(), 54 + "Content-Type": "application/json", 55 + }, 56 + }); 57 + if (!res.ok) { 58 + const body = await res.text().catch(() => ""); 59 + throw new Error(`PDS ${path} failed: ${res.status} ${body}`); 60 + } 61 + // some procedures (e.g. disableInviteCodes) return 200 with an empty body 62 + const text = await res.text(); 63 + return text ? JSON.parse(text) : {}; 64 + } 65 + 66 + async listRepos(cursor?: string, limit = 500): Promise<{ repos: RepoEntry[]; cursor?: string }> { 67 + const params = new URLSearchParams({ limit: String(limit) }); 68 + if (cursor) params.set("cursor", cursor); 69 + return this.xrpc(`com.atproto.sync.listRepos?${params}`) as Promise<{ 70 + repos: RepoEntry[]; 71 + cursor?: string; 72 + }>; 73 + } 74 + 75 + async listAllRepos(): Promise<RepoEntry[]> { 76 + const repos: RepoEntry[] = []; 77 + let cursor: string | undefined; 78 + do { 79 + const page = await this.listRepos(cursor); 80 + repos.push(...page.repos); 81 + cursor = page.repos.length > 0 ? page.cursor : undefined; 82 + } while (cursor); 83 + return repos; 84 + } 85 + 86 + /** Admin account info (handle/email/indexedAt); `dids` capped at 100 per call by the PDS. */ 87 + async accountInfos(dids: string[]): Promise<AccountInfo[]> { 88 + if (dids.length === 0) return []; 89 + const params = new URLSearchParams(); 90 + for (const did of dids) params.append("dids", did); 91 + const { infos } = (await this.xrpc(`com.atproto.admin.getAccountInfos?${params}`)) as { 92 + infos: AccountInfo[]; 93 + }; 94 + return infos; 95 + } 96 + 97 + async repoStatus(did: string): Promise<RepoEntry> { 98 + return this.xrpc( 99 + `com.atproto.sync.getRepoStatus?did=${encodeURIComponent(did)}`, 100 + ) as Promise<RepoEntry>; 101 + } 102 + 103 + async getAccount(did: string): Promise<AdminAccount> { 104 + return this.xrpc(`com.atproto.admin.getAccountInfo?did=${encodeURIComponent(did)}`); 105 + } 106 + 107 + async setAccountTakedown(did: string, takedown: boolean) { 108 + return this.xrpc("com.atproto.admin.updateSubjectStatus", { 109 + method: "POST", 110 + body: JSON.stringify({ 111 + subject: { $type: "com.atproto.admin.defs#repoRef", did }, 112 + takedown: takedown ? { applied: true } : { applied: false }, 113 + }), 114 + }); 115 + } 116 + 117 + async getInviteCodes(limit = 100): Promise<{ codes: InviteCode[] }> { 118 + return this.xrpc(`com.atproto.admin.getInviteCodes?sort=recent&limit=${limit}`) as Promise<{ 119 + codes: InviteCode[]; 120 + }>; 121 + } 122 + 123 + async createInviteCode(useCount = 1): Promise<{ code: string }> { 124 + return this.xrpc("com.atproto.server.createInviteCode", { 125 + method: "POST", 126 + body: JSON.stringify({ useCount }), 127 + }) as Promise<{ code: string }>; 128 + } 129 + 130 + async disableInviteCodes(codes: string[]) { 131 + return this.xrpc("com.atproto.admin.disableInviteCodes", { 132 + method: "POST", 133 + body: JSON.stringify({ codes }), 134 + }); 135 + } 136 + 137 + async resetAccountPassword(did: string) { 138 + return this.xrpc("com.atproto.admin.sendAccountPasswordResetEmail", { 139 + method: "POST", 140 + body: JSON.stringify({ did }), 141 + }); 142 + } 143 + 144 + /** 145 + * No public "current max seq" endpoint exists on the PDS. Approximate by opening a 146 + * subscribeRepos socket starting at `fromSeq` (e.g. the relay's last-known seq) so the 147 + * server immediately sends buffered backfill frames rather than waiting on live traffic, 148 + * and taking the highest seq seen within the read window. When there's a large backlog 149 + * this returns "how far it got in windowMs," not necessarily the true live head — still 150 + * far more useful than an unbounded wait for the next real-time event. 151 + */ 152 + async approximateHeadSeq(fromSeq: number, windowMs = 3000): Promise<number | null> { 153 + return new Promise((resolve) => { 154 + const ws = new WebSocket( 155 + `wss://${this.hostname}/xrpc/com.atproto.sync.subscribeRepos?cursor=${fromSeq}`, 156 + ); 157 + let maxSeq: number | null = null; 158 + let opened = false; 159 + 160 + const finish = () => { 161 + clearTimeout(timer); 162 + ws.terminate(); 163 + // Socket opened but no frames arrived: the PDS has nothing past fromSeq, so the 164 + // head is (at least) fromSeq — report "caught up" rather than "unknown". 165 + resolve(maxSeq ?? (opened ? fromSeq : null)); 166 + }; 167 + const timer = setTimeout(finish, windowMs); 168 + 169 + ws.once("open", () => { 170 + opened = true; 171 + }); 172 + ws.on("message", (data: Buffer) => { 173 + const frame = readFrame(data); 174 + if (typeof frame.body?.seq === "number") { 175 + maxSeq = maxSeq == null ? frame.body.seq : Math.max(maxSeq, frame.body.seq); 176 + } 177 + }); 178 + 179 + ws.once("error", finish); 180 + }); 181 + } 182 + } 183 + 184 + /** 185 + * Each event-stream frame is two concatenated CBOR values: a header ({op, t}) then a body. 186 + * Shared by subscribeRepos and subscribeLabels consumers. 187 + */ 188 + export function readFrame(buf: Buffer): { header?: { op?: number; t?: string }; body?: any } { 189 + const values = decodeMultiple(buf) as unknown[] | undefined; 190 + return { header: values?.[0] as { op?: number; t?: string }, body: values?.[1] }; 191 + }
+27
server/src/relayClient.ts
··· 1 + export interface HostStatus { 2 + hostname: string; 3 + seq: number; 4 + accountCount: number; 5 + status: "active" | "idle" | "offline" | "throttled" | "banned"; 6 + } 7 + 8 + export class RelayClient { 9 + constructor(private hostname: string) {} 10 + 11 + async getHostStatus(pdsHostname: string): Promise<HostStatus> { 12 + const res = await fetch( 13 + `https://${this.hostname}/xrpc/com.atproto.sync.getHostStatus?hostname=${encodeURIComponent(pdsHostname)}`, 14 + ); 15 + if (!res.ok) throw new Error(`relay getHostStatus failed: ${res.status}`); 16 + return res.json(); 17 + } 18 + 19 + async requestCrawl(pdsHostname: string): Promise<void> { 20 + const res = await fetch(`https://${this.hostname}/xrpc/com.atproto.sync.requestCrawl`, { 21 + method: "POST", 22 + headers: { "Content-Type": "application/json" }, 23 + body: JSON.stringify({ hostname: pdsHostname }), 24 + }); 25 + if (!res.ok) throw new Error(`relay requestCrawl failed: ${res.status}`); 26 + } 27 + }
+156
server/src/routes/accounts.ts
··· 1 + import type { FastifyInstance } from "fastify"; 2 + import type { PdsClient } from "../pdsClient.js"; 3 + import type { WatchedLabeler } from "../labelerClient.js"; 4 + import type { AccountRow, Db, LabelRow } from "../db.js"; 5 + import { requireAuth } from "../auth.js"; 6 + import { recordAction } from "../auditLog.js"; 7 + 8 + interface ApiAccount { 9 + did: string; 10 + handle: string; 11 + email?: string; 12 + indexedAt: string; 13 + status: string; 14 + avatar?: string; 15 + labels: string[]; 16 + } 17 + 18 + export function registerAccountRoutes( 19 + app: FastifyInstance, 20 + pds: PdsClient, 21 + db: Db, 22 + labelers: WatchedLabeler[], 23 + labelNames: Map<string, Map<string, string>>, 24 + ) { 25 + const watchBysrc = new Map(labelers.map((l) => [l.did, l.watch])); 26 + 27 + function isWatched(row: LabelRow): boolean { 28 + const watch = watchBysrc.get(row.src); 29 + if (!watch) return false; 30 + return watch.size === 0 || watch.has(row.val); 31 + } 32 + 33 + const displayName = (src: string, val: string) => labelNames.get(src)?.get(val) ?? val; 34 + 35 + function toApi(rows: AccountRow[]): ApiAccount[] { 36 + if (rows.length === 0) return []; 37 + const params = rows.map(() => "?").join(","); 38 + const labelRows = db 39 + .prepare(`SELECT did, src, val, cts FROM labels WHERE did IN (${params})`) 40 + .all(...rows.map((r) => r.did)) as LabelRow[]; 41 + const labelsByDid = new Map<string, Set<string>>(); 42 + for (const row of labelRows) { 43 + if (!isWatched(row)) continue; 44 + const set = labelsByDid.get(row.did) ?? new Set<string>(); 45 + set.add(displayName(row.src, row.val)); 46 + labelsByDid.set(row.did, set); 47 + } 48 + return rows.map((r) => ({ 49 + did: r.did, 50 + handle: r.handle, 51 + email: r.email ?? undefined, 52 + indexedAt: r.indexed_at, 53 + status: r.status, 54 + avatar: r.avatar ?? undefined, 55 + labels: [...(labelsByDid.get(r.did) ?? [])].sort(), 56 + })); 57 + } 58 + 59 + // SQL predicate for "this account carries a watched label" — used to float flagged 60 + // accounts to the top of the list 61 + const flagConds: string[] = []; 62 + const flagParams: unknown[] = []; 63 + for (const l of labelers) { 64 + if (l.watch.size === 0) { 65 + flagConds.push("l.src = ?"); 66 + flagParams.push(l.did); 67 + } else { 68 + flagConds.push(`(l.src = ? AND l.val IN (${[...l.watch].map(() => "?").join(",")}))`); 69 + flagParams.push(l.did, ...l.watch); 70 + } 71 + } 72 + const flagExpr = 73 + flagConds.length > 0 74 + ? `EXISTS (SELECT 1 FROM labels l WHERE l.did = accounts.did AND (${flagConds.join(" OR ")}))` 75 + : "0"; 76 + 77 + app.get("/api/accounts", { preHandler: requireAuth }, async (req) => { 78 + const query = req.query as { 79 + q?: string; 80 + offset?: string; 81 + limit?: string; 82 + hideTakendown?: string; 83 + }; 84 + const limit = Math.min(Math.max(Number(query.limit) || 100, 1), 500); 85 + const offset = Math.max(Number(query.offset) || 0, 0); 86 + 87 + const q = query.q?.trim(); 88 + const where: string[] = []; 89 + const params: unknown[] = []; 90 + if (q) { 91 + const pattern = "%" + q.replace(/[\\%_]/g, (m) => "\\" + m) + "%"; 92 + where.push( 93 + "(handle LIKE ? ESCAPE '\\' OR did LIKE ? ESCAPE '\\' OR email LIKE ? ESCAPE '\\')", 94 + ); 95 + params.push(pattern, pattern, pattern); 96 + } 97 + if (query.hideTakendown === "1") { 98 + if (q) { 99 + // an exact handle/did/email match should surface even when taken down 100 + where.push( 101 + "(status != 'takendown' OR handle = ? COLLATE NOCASE OR did = ? OR email = ? COLLATE NOCASE)", 102 + ); 103 + params.push(q, q, q); 104 + } else { 105 + where.push("status != 'takendown'"); 106 + } 107 + } 108 + const whereSql = where.length > 0 ? `WHERE ${where.join(" AND ")}` : ""; 109 + 110 + const { total } = db 111 + .prepare(`SELECT COUNT(*) AS total FROM accounts ${whereSql}`) 112 + .get(...params) as { total: number }; 113 + const { flaggedTotal } = db 114 + .prepare( 115 + `SELECT COUNT(*) AS flaggedTotal FROM accounts ${whereSql} ${whereSql ? "AND" : "WHERE"} ${flagExpr}`, 116 + ) 117 + .get(...params, ...flagParams) as { flaggedTotal: number }; 118 + const rows = db 119 + .prepare( 120 + `SELECT *, ${flagExpr} AS is_flagged FROM accounts ${whereSql} 121 + ORDER BY is_flagged DESC, indexed_at DESC LIMIT ? OFFSET ?`, 122 + ) 123 + .all(...flagParams, ...params, limit, offset) as AccountRow[]; 124 + 125 + return { accounts: toApi(rows), total, flaggedTotal }; 126 + }); 127 + 128 + app.post("/api/accounts/:did/takedown", { preHandler: requireAuth }, async (req) => { 129 + const { did } = req.params as { did: string }; 130 + await pds.setAccountTakedown(did, true); 131 + db.prepare("UPDATE accounts SET status = 'takendown', updated_at = ? WHERE did = ?").run( 132 + Date.now(), 133 + did, 134 + ); 135 + await recordAction({ operator: req.session.operator!, action: "takedown", target: did }); 136 + return { ok: true }; 137 + }); 138 + 139 + app.post("/api/accounts/:did/enable", { preHandler: requireAuth }, async (req) => { 140 + const { did } = req.params as { did: string }; 141 + await pds.setAccountTakedown(did, false); 142 + db.prepare("UPDATE accounts SET status = 'active', updated_at = ? WHERE did = ?").run( 143 + Date.now(), 144 + did, 145 + ); 146 + await recordAction({ operator: req.session.operator!, action: "enable", target: did }); 147 + return { ok: true }; 148 + }); 149 + 150 + app.post("/api/accounts/:did/reset-password", { preHandler: requireAuth }, async (req) => { 151 + const { did } = req.params as { did: string }; 152 + await pds.resetAccountPassword(did); 153 + await recordAction({ operator: req.session.operator!, action: "reset-password", target: did }); 154 + return { ok: true }; 155 + }); 156 + }
+11
server/src/routes/audit.ts
··· 1 + import type { FastifyInstance } from "fastify"; 2 + import { requireAuth } from "../auth.js"; 3 + import { readRecent } from "../auditLog.js"; 4 + 5 + export function registerAuditRoutes(app: FastifyInstance) { 6 + app.get("/api/audit", { preHandler: requireAuth }, async (req) => { 7 + const { limit } = req.query as { limit?: string }; 8 + const n = Math.min(Math.max(Number(limit) || 50, 1), 500); 9 + return { entries: await readRecent(n) }; 10 + }); 11 + }
+24
server/src/routes/invites.ts
··· 1 + import type { FastifyInstance } from "fastify"; 2 + import type { PdsClient } from "../pdsClient.js"; 3 + import { requireAuth } from "../auth.js"; 4 + import { recordAction } from "../auditLog.js"; 5 + 6 + export function registerInviteRoutes(app: FastifyInstance, pds: PdsClient) { 7 + app.get("/api/invites", { preHandler: requireAuth }, async () => { 8 + return pds.getInviteCodes(); 9 + }); 10 + 11 + app.post("/api/invites", { preHandler: requireAuth }, async (req) => { 12 + const { useCount } = (req.body ?? {}) as { useCount?: number }; 13 + const { code } = await pds.createInviteCode(Math.min(Math.max(useCount ?? 1, 1), 100)); 14 + await recordAction({ operator: req.session.operator!, action: "invite-created", target: code }); 15 + return { code }; 16 + }); 17 + 18 + app.post("/api/invites/disable", { preHandler: requireAuth }, async (req) => { 19 + const { code } = req.body as { code: string }; 20 + await pds.disableInviteCodes([code]); 21 + await recordAction({ operator: req.session.operator!, action: "invite-disabled", target: code }); 22 + return { ok: true }; 23 + }); 24 + }
+206
server/src/routes/passkeys.ts
··· 1 + import type { FastifyInstance, FastifyRequest } from "fastify"; 2 + import { 3 + generateRegistrationOptions, 4 + verifyRegistrationResponse, 5 + generateAuthenticationOptions, 6 + verifyAuthenticationResponse, 7 + type AuthenticatorTransportFuture, 8 + } from "@simplewebauthn/server"; 9 + import { checkEnrollToken, consumeEnrollToken, type Db } from "../db.js"; 10 + import { requireAuth } from "../auth.js"; 11 + import { recordAction } from "../auditLog.js"; 12 + 13 + declare module "@fastify/session" { 14 + interface FastifySessionObject { 15 + webauthnChallenge?: string; 16 + } 17 + } 18 + 19 + interface PasskeyRow { 20 + id: string; 21 + public_key: Buffer; 22 + counter: number; 23 + transports: string | null; 24 + name: string | null; 25 + created_at: number; 26 + } 27 + 28 + // The dashboard runs on localhost in dev and its real domain in prod; derive the 29 + // WebAuthn relying-party ID from the request so credentials bind to whichever host 30 + // the operator is actually using. 31 + function rpFromRequest(req: FastifyRequest): { rpID: string; origin: string } { 32 + const origin = (req.headers.origin as string | undefined) ?? `http://${req.headers.host}`; 33 + return { rpID: new URL(origin).hostname, origin }; 34 + } 35 + 36 + export function registerPasskeyRoutes(app: FastifyInstance, db: Db, pdsHostname: string) { 37 + const allPasskeys = () => db.prepare("SELECT * FROM passkeys").all() as PasskeyRow[]; 38 + 39 + // Enrollment is authorized by an operator session OR a one-time token minted by 40 + // the CLI (`npm run enroll`) — the bootstrap path for passkey-only deployments. 41 + // Guessing tokens is rate-limited per IP on top of the 15-minute expiry. 42 + const enrollAttempts = new Map<string, { count: number; resetAt: number }>(); 43 + function enrollAuthorized(req: FastifyRequest): boolean { 44 + if (req.session.operator) return true; 45 + const token = (req.body as { enrollToken?: string } | null)?.enrollToken; 46 + if (!token) return false; 47 + const now = Date.now(); 48 + const entry = enrollAttempts.get(req.ip); 49 + if (entry && entry.resetAt <= now) enrollAttempts.delete(req.ip); 50 + const current = enrollAttempts.get(req.ip); 51 + if (current && current.count >= 5) return false; 52 + if (checkEnrollToken(db, token)) return true; 53 + if (current) current.count += 1; 54 + else enrollAttempts.set(req.ip, { count: 1, resetAt: now + 15 * 60 * 1000 }); 55 + return false; 56 + } 57 + 58 + // ---- enrollment (session or one-time CLI token) -------------------------- 59 + 60 + app.post("/api/passkeys/options", async (req, reply) => { 61 + if (!enrollAuthorized(req)) return reply.code(401).send({ error: "not authorized" }); 62 + const { rpID } = rpFromRequest(req); 63 + const options = await generateRegistrationOptions({ 64 + rpName: `${pdsHostname} admin`, 65 + rpID, 66 + userName: "operator", 67 + attestationType: "none", 68 + excludeCredentials: allPasskeys().map((p) => ({ 69 + id: p.id, 70 + transports: p.transports ? (JSON.parse(p.transports) as AuthenticatorTransportFuture[]) : undefined, 71 + })), 72 + authenticatorSelection: { residentKey: "preferred", userVerification: "preferred" }, 73 + }); 74 + req.session.webauthnChallenge = options.challenge; 75 + return options; 76 + }); 77 + 78 + app.post("/api/passkeys", async (req, reply) => { 79 + if (!enrollAuthorized(req)) return reply.code(401).send({ error: "not authorized" }); 80 + const { rpID, origin } = rpFromRequest(req); 81 + const { name, response, enrollToken } = req.body as { 82 + name?: string; 83 + response: any; 84 + enrollToken?: string; 85 + }; 86 + const expectedChallenge = req.session.webauthnChallenge; 87 + req.session.webauthnChallenge = undefined; 88 + if (!expectedChallenge) return reply.code(400).send({ error: "no pending registration" }); 89 + 90 + const verification = await verifyRegistrationResponse({ 91 + response, 92 + expectedChallenge, 93 + expectedOrigin: origin, 94 + expectedRPID: rpID, 95 + }).catch((err) => { 96 + req.log.warn({ err }, "passkey registration failed"); 97 + return null; 98 + }); 99 + if (!verification?.verified || !verification.registrationInfo) { 100 + return reply.code(400).send({ error: "passkey verification failed" }); 101 + } 102 + 103 + // consume the one-time token only after a verified registration 104 + if (!req.session.operator) { 105 + if (!enrollToken || !consumeEnrollToken(db, enrollToken)) { 106 + return reply.code(401).send({ error: "enrollment link expired" }); 107 + } 108 + req.session.operator = "operator"; // enrolling the passkey signs you in 109 + } 110 + 111 + const { credential } = verification.registrationInfo; 112 + db.prepare( 113 + "INSERT INTO passkeys (id, public_key, counter, transports, name, created_at) VALUES (?, ?, ?, ?, ?, ?)", 114 + ).run( 115 + credential.id, 116 + Buffer.from(credential.publicKey), 117 + credential.counter, 118 + JSON.stringify(credential.transports ?? []), 119 + name?.trim() || null, 120 + Date.now(), 121 + ); 122 + await recordAction({ 123 + operator: req.session.operator!, 124 + action: "passkey-added", 125 + target: credential.id, 126 + }); 127 + return { ok: true }; 128 + }); 129 + 130 + app.get("/api/passkeys", { preHandler: requireAuth }, async () => { 131 + return { 132 + passkeys: allPasskeys().map((p) => ({ 133 + id: p.id, 134 + name: p.name ?? "unnamed", 135 + createdAt: p.created_at, 136 + })), 137 + }; 138 + }); 139 + 140 + app.delete("/api/passkeys/:id", { preHandler: requireAuth }, async (req) => { 141 + const { id } = req.params as { id: string }; 142 + db.prepare("DELETE FROM passkeys WHERE id = ?").run(id); 143 + await recordAction({ operator: req.session.operator!, action: "passkey-removed", target: id }); 144 + return { ok: true }; 145 + }); 146 + 147 + // ---- sign-in (no session yet) -------------------------------------------- 148 + 149 + app.post("/api/login/passkey/options", async (req, reply) => { 150 + if (allPasskeys().length === 0) { 151 + return reply.code(400).send({ error: "no passkeys enrolled" }); 152 + } 153 + const { rpID } = rpFromRequest(req); 154 + const options = await generateAuthenticationOptions({ 155 + rpID, 156 + userVerification: "preferred", 157 + allowCredentials: allPasskeys().map((p) => ({ 158 + id: p.id, 159 + transports: p.transports ? (JSON.parse(p.transports) as AuthenticatorTransportFuture[]) : undefined, 160 + })), 161 + }); 162 + req.session.webauthnChallenge = options.challenge; 163 + return options; 164 + }); 165 + 166 + app.post("/api/login/passkey", async (req, reply) => { 167 + const { rpID, origin } = rpFromRequest(req); 168 + const response = req.body as any; 169 + const expectedChallenge = req.session.webauthnChallenge; 170 + req.session.webauthnChallenge = undefined; 171 + if (!expectedChallenge) return reply.code(400).send({ error: "no pending authentication" }); 172 + 173 + const row = db.prepare("SELECT * FROM passkeys WHERE id = ?").get(response?.id) as 174 + | PasskeyRow 175 + | undefined; 176 + if (!row) return reply.code(401).send({ error: "unknown passkey" }); 177 + 178 + const verification = await verifyAuthenticationResponse({ 179 + response, 180 + expectedChallenge, 181 + expectedOrigin: origin, 182 + expectedRPID: rpID, 183 + credential: { 184 + id: row.id, 185 + publicKey: new Uint8Array(row.public_key), 186 + counter: row.counter, 187 + transports: row.transports 188 + ? (JSON.parse(row.transports) as AuthenticatorTransportFuture[]) 189 + : undefined, 190 + }, 191 + }).catch((err) => { 192 + req.log.warn({ err }, "passkey authentication failed"); 193 + return null; 194 + }); 195 + if (!verification?.verified) { 196 + return reply.code(401).send({ error: "passkey verification failed" }); 197 + } 198 + 199 + db.prepare("UPDATE passkeys SET counter = ? WHERE id = ?").run( 200 + verification.authenticationInfo.newCounter, 201 + row.id, 202 + ); 203 + req.session.operator = "operator"; 204 + return { ok: true }; 205 + }); 206 + }
+34
server/src/routes/status.ts
··· 1 + import type { FastifyInstance } from "fastify"; 2 + import type { PdsClient } from "../pdsClient.js"; 3 + import type { RelayClient } from "../relayClient.js"; 4 + import { getSyncState, type Db } from "../db.js"; 5 + import { requireAuth } from "../auth.js"; 6 + import { recordAction } from "../auditLog.js"; 7 + 8 + export function registerStatusRoutes( 9 + app: FastifyInstance, 10 + pds: PdsClient, 11 + relay: RelayClient, 12 + pdsHostname: string, 13 + db: Db, 14 + ) { 15 + app.get("/api/status/cursor", { preHandler: requireAuth }, async () => { 16 + const hostStatus = await relay.getHostStatus(pdsHostname); 17 + const headSeq = await pds.approximateHeadSeq(hostStatus.seq); 18 + const gap = headSeq != null ? headSeq - hostStatus.seq : null; 19 + return { 20 + relaySeq: hostStatus.seq, 21 + relayStatus: hostStatus.status, 22 + accountCount: hostStatus.accountCount, 23 + pdsHeadSeqApprox: headSeq, 24 + gap, 25 + lastFullSync: getSyncState(db, "last_full_sync"), 26 + }; 27 + }); 28 + 29 + app.post("/api/status/request-crawl", { preHandler: requireAuth }, async (req) => { 30 + await relay.requestCrawl(pdsHostname); 31 + await recordAction({ operator: req.session.operator!, action: "request-crawl", target: pdsHostname }); 32 + return { ok: true }; 33 + }); 34 + }
server/src/sync.ts

This is a binary file and will not be displayed.

+14
server/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "NodeNext", 5 + "moduleResolution": "NodeNext", 6 + "outDir": "dist", 7 + "rootDir": "src", 8 + "strict": true, 9 + "esModuleInterop": true, 10 + "skipLibCheck": true, 11 + "resolveJsonModule": true 12 + }, 13 + "include": ["src"] 14 + }
+13
web/index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>PDS Operator</title> 7 + <link rel="stylesheet" href="/src/style.css" /> 8 + </head> 9 + <body> 10 + <div id="root"></div> 11 + <script type="module" src="/src/main.tsx"></script> 12 + </body> 13 + </html>
+1830
web/package-lock.json
··· 1 + { 2 + "name": "pds-operator-web", 3 + "version": "0.1.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "pds-operator-web", 9 + "version": "0.1.0", 10 + "dependencies": { 11 + "@simplewebauthn/browser": "^13.3.0", 12 + "react": "^19.0.0", 13 + "react-dom": "^19.0.0" 14 + }, 15 + "devDependencies": { 16 + "@types/react": "^19.0.10", 17 + "@types/react-dom": "^19.0.4", 18 + "@vitejs/plugin-react": "^4.3.4", 19 + "typescript": "^5.7.3", 20 + "vite": "^6.1.0" 21 + } 22 + }, 23 + "node_modules/@babel/code-frame": { 24 + "version": "7.29.7", 25 + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", 26 + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", 27 + "dev": true, 28 + "license": "MIT", 29 + "dependencies": { 30 + "@babel/helper-validator-identifier": "^7.29.7", 31 + "js-tokens": "^4.0.0", 32 + "picocolors": "^1.1.1" 33 + }, 34 + "engines": { 35 + "node": ">=6.9.0" 36 + } 37 + }, 38 + "node_modules/@babel/compat-data": { 39 + "version": "7.29.7", 40 + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", 41 + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", 42 + "dev": true, 43 + "license": "MIT", 44 + "engines": { 45 + "node": ">=6.9.0" 46 + } 47 + }, 48 + "node_modules/@babel/core": { 49 + "version": "7.29.7", 50 + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", 51 + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", 52 + "dev": true, 53 + "license": "MIT", 54 + "dependencies": { 55 + "@babel/code-frame": "^7.29.7", 56 + "@babel/generator": "^7.29.7", 57 + "@babel/helper-compilation-targets": "^7.29.7", 58 + "@babel/helper-module-transforms": "^7.29.7", 59 + "@babel/helpers": "^7.29.7", 60 + "@babel/parser": "^7.29.7", 61 + "@babel/template": "^7.29.7", 62 + "@babel/traverse": "^7.29.7", 63 + "@babel/types": "^7.29.7", 64 + "@jridgewell/remapping": "^2.3.5", 65 + "convert-source-map": "^2.0.0", 66 + "debug": "^4.1.0", 67 + "gensync": "^1.0.0-beta.2", 68 + "json5": "^2.2.3", 69 + "semver": "^6.3.1" 70 + }, 71 + "engines": { 72 + "node": ">=6.9.0" 73 + }, 74 + "funding": { 75 + "type": "opencollective", 76 + "url": "https://opencollective.com/babel" 77 + } 78 + }, 79 + "node_modules/@babel/generator": { 80 + "version": "7.29.7", 81 + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", 82 + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", 83 + "dev": true, 84 + "license": "MIT", 85 + "dependencies": { 86 + "@babel/parser": "^7.29.7", 87 + "@babel/types": "^7.29.7", 88 + "@jridgewell/gen-mapping": "^0.3.12", 89 + "@jridgewell/trace-mapping": "^0.3.28", 90 + "jsesc": "^3.0.2" 91 + }, 92 + "engines": { 93 + "node": ">=6.9.0" 94 + } 95 + }, 96 + "node_modules/@babel/helper-compilation-targets": { 97 + "version": "7.29.7", 98 + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", 99 + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", 100 + "dev": true, 101 + "license": "MIT", 102 + "dependencies": { 103 + "@babel/compat-data": "^7.29.7", 104 + "@babel/helper-validator-option": "^7.29.7", 105 + "browserslist": "^4.24.0", 106 + "lru-cache": "^5.1.1", 107 + "semver": "^6.3.1" 108 + }, 109 + "engines": { 110 + "node": ">=6.9.0" 111 + } 112 + }, 113 + "node_modules/@babel/helper-globals": { 114 + "version": "7.29.7", 115 + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", 116 + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", 117 + "dev": true, 118 + "license": "MIT", 119 + "engines": { 120 + "node": ">=6.9.0" 121 + } 122 + }, 123 + "node_modules/@babel/helper-module-imports": { 124 + "version": "7.29.7", 125 + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", 126 + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", 127 + "dev": true, 128 + "license": "MIT", 129 + "dependencies": { 130 + "@babel/traverse": "^7.29.7", 131 + "@babel/types": "^7.29.7" 132 + }, 133 + "engines": { 134 + "node": ">=6.9.0" 135 + } 136 + }, 137 + "node_modules/@babel/helper-module-transforms": { 138 + "version": "7.29.7", 139 + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", 140 + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", 141 + "dev": true, 142 + "license": "MIT", 143 + "dependencies": { 144 + "@babel/helper-module-imports": "^7.29.7", 145 + "@babel/helper-validator-identifier": "^7.29.7", 146 + "@babel/traverse": "^7.29.7" 147 + }, 148 + "engines": { 149 + "node": ">=6.9.0" 150 + }, 151 + "peerDependencies": { 152 + "@babel/core": "^7.0.0" 153 + } 154 + }, 155 + "node_modules/@babel/helper-plugin-utils": { 156 + "version": "7.29.7", 157 + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", 158 + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", 159 + "dev": true, 160 + "license": "MIT", 161 + "engines": { 162 + "node": ">=6.9.0" 163 + } 164 + }, 165 + "node_modules/@babel/helper-string-parser": { 166 + "version": "7.29.7", 167 + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", 168 + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", 169 + "dev": true, 170 + "license": "MIT", 171 + "engines": { 172 + "node": ">=6.9.0" 173 + } 174 + }, 175 + "node_modules/@babel/helper-validator-identifier": { 176 + "version": "7.29.7", 177 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", 178 + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", 179 + "dev": true, 180 + "license": "MIT", 181 + "engines": { 182 + "node": ">=6.9.0" 183 + } 184 + }, 185 + "node_modules/@babel/helper-validator-option": { 186 + "version": "7.29.7", 187 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", 188 + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", 189 + "dev": true, 190 + "license": "MIT", 191 + "engines": { 192 + "node": ">=6.9.0" 193 + } 194 + }, 195 + "node_modules/@babel/helpers": { 196 + "version": "7.29.7", 197 + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", 198 + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", 199 + "dev": true, 200 + "license": "MIT", 201 + "dependencies": { 202 + "@babel/template": "^7.29.7", 203 + "@babel/types": "^7.29.7" 204 + }, 205 + "engines": { 206 + "node": ">=6.9.0" 207 + } 208 + }, 209 + "node_modules/@babel/parser": { 210 + "version": "7.29.7", 211 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", 212 + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", 213 + "dev": true, 214 + "license": "MIT", 215 + "dependencies": { 216 + "@babel/types": "^7.29.7" 217 + }, 218 + "bin": { 219 + "parser": "bin/babel-parser.js" 220 + }, 221 + "engines": { 222 + "node": ">=6.0.0" 223 + } 224 + }, 225 + "node_modules/@babel/plugin-transform-react-jsx-self": { 226 + "version": "7.29.7", 227 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", 228 + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", 229 + "dev": true, 230 + "license": "MIT", 231 + "dependencies": { 232 + "@babel/helper-plugin-utils": "^7.29.7" 233 + }, 234 + "engines": { 235 + "node": ">=6.9.0" 236 + }, 237 + "peerDependencies": { 238 + "@babel/core": "^7.0.0-0" 239 + } 240 + }, 241 + "node_modules/@babel/plugin-transform-react-jsx-source": { 242 + "version": "7.29.7", 243 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", 244 + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", 245 + "dev": true, 246 + "license": "MIT", 247 + "dependencies": { 248 + "@babel/helper-plugin-utils": "^7.29.7" 249 + }, 250 + "engines": { 251 + "node": ">=6.9.0" 252 + }, 253 + "peerDependencies": { 254 + "@babel/core": "^7.0.0-0" 255 + } 256 + }, 257 + "node_modules/@babel/template": { 258 + "version": "7.29.7", 259 + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", 260 + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", 261 + "dev": true, 262 + "license": "MIT", 263 + "dependencies": { 264 + "@babel/code-frame": "^7.29.7", 265 + "@babel/parser": "^7.29.7", 266 + "@babel/types": "^7.29.7" 267 + }, 268 + "engines": { 269 + "node": ">=6.9.0" 270 + } 271 + }, 272 + "node_modules/@babel/traverse": { 273 + "version": "7.29.7", 274 + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", 275 + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", 276 + "dev": true, 277 + "license": "MIT", 278 + "dependencies": { 279 + "@babel/code-frame": "^7.29.7", 280 + "@babel/generator": "^7.29.7", 281 + "@babel/helper-globals": "^7.29.7", 282 + "@babel/parser": "^7.29.7", 283 + "@babel/template": "^7.29.7", 284 + "@babel/types": "^7.29.7", 285 + "debug": "^4.3.1" 286 + }, 287 + "engines": { 288 + "node": ">=6.9.0" 289 + } 290 + }, 291 + "node_modules/@babel/types": { 292 + "version": "7.29.7", 293 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", 294 + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", 295 + "dev": true, 296 + "license": "MIT", 297 + "dependencies": { 298 + "@babel/helper-string-parser": "^7.29.7", 299 + "@babel/helper-validator-identifier": "^7.29.7" 300 + }, 301 + "engines": { 302 + "node": ">=6.9.0" 303 + } 304 + }, 305 + "node_modules/@esbuild/aix-ppc64": { 306 + "version": "0.25.12", 307 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", 308 + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", 309 + "cpu": [ 310 + "ppc64" 311 + ], 312 + "dev": true, 313 + "license": "MIT", 314 + "optional": true, 315 + "os": [ 316 + "aix" 317 + ], 318 + "engines": { 319 + "node": ">=18" 320 + } 321 + }, 322 + "node_modules/@esbuild/android-arm": { 323 + "version": "0.25.12", 324 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", 325 + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", 326 + "cpu": [ 327 + "arm" 328 + ], 329 + "dev": true, 330 + "license": "MIT", 331 + "optional": true, 332 + "os": [ 333 + "android" 334 + ], 335 + "engines": { 336 + "node": ">=18" 337 + } 338 + }, 339 + "node_modules/@esbuild/android-arm64": { 340 + "version": "0.25.12", 341 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", 342 + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", 343 + "cpu": [ 344 + "arm64" 345 + ], 346 + "dev": true, 347 + "license": "MIT", 348 + "optional": true, 349 + "os": [ 350 + "android" 351 + ], 352 + "engines": { 353 + "node": ">=18" 354 + } 355 + }, 356 + "node_modules/@esbuild/android-x64": { 357 + "version": "0.25.12", 358 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", 359 + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", 360 + "cpu": [ 361 + "x64" 362 + ], 363 + "dev": true, 364 + "license": "MIT", 365 + "optional": true, 366 + "os": [ 367 + "android" 368 + ], 369 + "engines": { 370 + "node": ">=18" 371 + } 372 + }, 373 + "node_modules/@esbuild/darwin-arm64": { 374 + "version": "0.25.12", 375 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", 376 + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", 377 + "cpu": [ 378 + "arm64" 379 + ], 380 + "dev": true, 381 + "license": "MIT", 382 + "optional": true, 383 + "os": [ 384 + "darwin" 385 + ], 386 + "engines": { 387 + "node": ">=18" 388 + } 389 + }, 390 + "node_modules/@esbuild/darwin-x64": { 391 + "version": "0.25.12", 392 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", 393 + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", 394 + "cpu": [ 395 + "x64" 396 + ], 397 + "dev": true, 398 + "license": "MIT", 399 + "optional": true, 400 + "os": [ 401 + "darwin" 402 + ], 403 + "engines": { 404 + "node": ">=18" 405 + } 406 + }, 407 + "node_modules/@esbuild/freebsd-arm64": { 408 + "version": "0.25.12", 409 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", 410 + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", 411 + "cpu": [ 412 + "arm64" 413 + ], 414 + "dev": true, 415 + "license": "MIT", 416 + "optional": true, 417 + "os": [ 418 + "freebsd" 419 + ], 420 + "engines": { 421 + "node": ">=18" 422 + } 423 + }, 424 + "node_modules/@esbuild/freebsd-x64": { 425 + "version": "0.25.12", 426 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", 427 + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", 428 + "cpu": [ 429 + "x64" 430 + ], 431 + "dev": true, 432 + "license": "MIT", 433 + "optional": true, 434 + "os": [ 435 + "freebsd" 436 + ], 437 + "engines": { 438 + "node": ">=18" 439 + } 440 + }, 441 + "node_modules/@esbuild/linux-arm": { 442 + "version": "0.25.12", 443 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", 444 + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", 445 + "cpu": [ 446 + "arm" 447 + ], 448 + "dev": true, 449 + "license": "MIT", 450 + "optional": true, 451 + "os": [ 452 + "linux" 453 + ], 454 + "engines": { 455 + "node": ">=18" 456 + } 457 + }, 458 + "node_modules/@esbuild/linux-arm64": { 459 + "version": "0.25.12", 460 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", 461 + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", 462 + "cpu": [ 463 + "arm64" 464 + ], 465 + "dev": true, 466 + "license": "MIT", 467 + "optional": true, 468 + "os": [ 469 + "linux" 470 + ], 471 + "engines": { 472 + "node": ">=18" 473 + } 474 + }, 475 + "node_modules/@esbuild/linux-ia32": { 476 + "version": "0.25.12", 477 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", 478 + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", 479 + "cpu": [ 480 + "ia32" 481 + ], 482 + "dev": true, 483 + "license": "MIT", 484 + "optional": true, 485 + "os": [ 486 + "linux" 487 + ], 488 + "engines": { 489 + "node": ">=18" 490 + } 491 + }, 492 + "node_modules/@esbuild/linux-loong64": { 493 + "version": "0.25.12", 494 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", 495 + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", 496 + "cpu": [ 497 + "loong64" 498 + ], 499 + "dev": true, 500 + "license": "MIT", 501 + "optional": true, 502 + "os": [ 503 + "linux" 504 + ], 505 + "engines": { 506 + "node": ">=18" 507 + } 508 + }, 509 + "node_modules/@esbuild/linux-mips64el": { 510 + "version": "0.25.12", 511 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", 512 + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", 513 + "cpu": [ 514 + "mips64el" 515 + ], 516 + "dev": true, 517 + "license": "MIT", 518 + "optional": true, 519 + "os": [ 520 + "linux" 521 + ], 522 + "engines": { 523 + "node": ">=18" 524 + } 525 + }, 526 + "node_modules/@esbuild/linux-ppc64": { 527 + "version": "0.25.12", 528 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", 529 + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", 530 + "cpu": [ 531 + "ppc64" 532 + ], 533 + "dev": true, 534 + "license": "MIT", 535 + "optional": true, 536 + "os": [ 537 + "linux" 538 + ], 539 + "engines": { 540 + "node": ">=18" 541 + } 542 + }, 543 + "node_modules/@esbuild/linux-riscv64": { 544 + "version": "0.25.12", 545 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", 546 + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", 547 + "cpu": [ 548 + "riscv64" 549 + ], 550 + "dev": true, 551 + "license": "MIT", 552 + "optional": true, 553 + "os": [ 554 + "linux" 555 + ], 556 + "engines": { 557 + "node": ">=18" 558 + } 559 + }, 560 + "node_modules/@esbuild/linux-s390x": { 561 + "version": "0.25.12", 562 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", 563 + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", 564 + "cpu": [ 565 + "s390x" 566 + ], 567 + "dev": true, 568 + "license": "MIT", 569 + "optional": true, 570 + "os": [ 571 + "linux" 572 + ], 573 + "engines": { 574 + "node": ">=18" 575 + } 576 + }, 577 + "node_modules/@esbuild/linux-x64": { 578 + "version": "0.25.12", 579 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", 580 + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", 581 + "cpu": [ 582 + "x64" 583 + ], 584 + "dev": true, 585 + "license": "MIT", 586 + "optional": true, 587 + "os": [ 588 + "linux" 589 + ], 590 + "engines": { 591 + "node": ">=18" 592 + } 593 + }, 594 + "node_modules/@esbuild/netbsd-arm64": { 595 + "version": "0.25.12", 596 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", 597 + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", 598 + "cpu": [ 599 + "arm64" 600 + ], 601 + "dev": true, 602 + "license": "MIT", 603 + "optional": true, 604 + "os": [ 605 + "netbsd" 606 + ], 607 + "engines": { 608 + "node": ">=18" 609 + } 610 + }, 611 + "node_modules/@esbuild/netbsd-x64": { 612 + "version": "0.25.12", 613 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", 614 + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", 615 + "cpu": [ 616 + "x64" 617 + ], 618 + "dev": true, 619 + "license": "MIT", 620 + "optional": true, 621 + "os": [ 622 + "netbsd" 623 + ], 624 + "engines": { 625 + "node": ">=18" 626 + } 627 + }, 628 + "node_modules/@esbuild/openbsd-arm64": { 629 + "version": "0.25.12", 630 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", 631 + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", 632 + "cpu": [ 633 + "arm64" 634 + ], 635 + "dev": true, 636 + "license": "MIT", 637 + "optional": true, 638 + "os": [ 639 + "openbsd" 640 + ], 641 + "engines": { 642 + "node": ">=18" 643 + } 644 + }, 645 + "node_modules/@esbuild/openbsd-x64": { 646 + "version": "0.25.12", 647 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", 648 + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", 649 + "cpu": [ 650 + "x64" 651 + ], 652 + "dev": true, 653 + "license": "MIT", 654 + "optional": true, 655 + "os": [ 656 + "openbsd" 657 + ], 658 + "engines": { 659 + "node": ">=18" 660 + } 661 + }, 662 + "node_modules/@esbuild/openharmony-arm64": { 663 + "version": "0.25.12", 664 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", 665 + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", 666 + "cpu": [ 667 + "arm64" 668 + ], 669 + "dev": true, 670 + "license": "MIT", 671 + "optional": true, 672 + "os": [ 673 + "openharmony" 674 + ], 675 + "engines": { 676 + "node": ">=18" 677 + } 678 + }, 679 + "node_modules/@esbuild/sunos-x64": { 680 + "version": "0.25.12", 681 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", 682 + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", 683 + "cpu": [ 684 + "x64" 685 + ], 686 + "dev": true, 687 + "license": "MIT", 688 + "optional": true, 689 + "os": [ 690 + "sunos" 691 + ], 692 + "engines": { 693 + "node": ">=18" 694 + } 695 + }, 696 + "node_modules/@esbuild/win32-arm64": { 697 + "version": "0.25.12", 698 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", 699 + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", 700 + "cpu": [ 701 + "arm64" 702 + ], 703 + "dev": true, 704 + "license": "MIT", 705 + "optional": true, 706 + "os": [ 707 + "win32" 708 + ], 709 + "engines": { 710 + "node": ">=18" 711 + } 712 + }, 713 + "node_modules/@esbuild/win32-ia32": { 714 + "version": "0.25.12", 715 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", 716 + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", 717 + "cpu": [ 718 + "ia32" 719 + ], 720 + "dev": true, 721 + "license": "MIT", 722 + "optional": true, 723 + "os": [ 724 + "win32" 725 + ], 726 + "engines": { 727 + "node": ">=18" 728 + } 729 + }, 730 + "node_modules/@esbuild/win32-x64": { 731 + "version": "0.25.12", 732 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", 733 + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", 734 + "cpu": [ 735 + "x64" 736 + ], 737 + "dev": true, 738 + "license": "MIT", 739 + "optional": true, 740 + "os": [ 741 + "win32" 742 + ], 743 + "engines": { 744 + "node": ">=18" 745 + } 746 + }, 747 + "node_modules/@jridgewell/gen-mapping": { 748 + "version": "0.3.13", 749 + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", 750 + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", 751 + "dev": true, 752 + "license": "MIT", 753 + "dependencies": { 754 + "@jridgewell/sourcemap-codec": "^1.5.0", 755 + "@jridgewell/trace-mapping": "^0.3.24" 756 + } 757 + }, 758 + "node_modules/@jridgewell/remapping": { 759 + "version": "2.3.5", 760 + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", 761 + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", 762 + "dev": true, 763 + "license": "MIT", 764 + "dependencies": { 765 + "@jridgewell/gen-mapping": "^0.3.5", 766 + "@jridgewell/trace-mapping": "^0.3.24" 767 + } 768 + }, 769 + "node_modules/@jridgewell/resolve-uri": { 770 + "version": "3.1.2", 771 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 772 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 773 + "dev": true, 774 + "license": "MIT", 775 + "engines": { 776 + "node": ">=6.0.0" 777 + } 778 + }, 779 + "node_modules/@jridgewell/sourcemap-codec": { 780 + "version": "1.5.5", 781 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", 782 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", 783 + "dev": true, 784 + "license": "MIT" 785 + }, 786 + "node_modules/@jridgewell/trace-mapping": { 787 + "version": "0.3.31", 788 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", 789 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", 790 + "dev": true, 791 + "license": "MIT", 792 + "dependencies": { 793 + "@jridgewell/resolve-uri": "^3.1.0", 794 + "@jridgewell/sourcemap-codec": "^1.4.14" 795 + } 796 + }, 797 + "node_modules/@rolldown/pluginutils": { 798 + "version": "1.0.0-beta.27", 799 + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", 800 + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", 801 + "dev": true, 802 + "license": "MIT" 803 + }, 804 + "node_modules/@rollup/rollup-android-arm-eabi": { 805 + "version": "4.62.2", 806 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", 807 + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", 808 + "cpu": [ 809 + "arm" 810 + ], 811 + "dev": true, 812 + "license": "MIT", 813 + "optional": true, 814 + "os": [ 815 + "android" 816 + ] 817 + }, 818 + "node_modules/@rollup/rollup-android-arm64": { 819 + "version": "4.62.2", 820 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", 821 + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", 822 + "cpu": [ 823 + "arm64" 824 + ], 825 + "dev": true, 826 + "license": "MIT", 827 + "optional": true, 828 + "os": [ 829 + "android" 830 + ] 831 + }, 832 + "node_modules/@rollup/rollup-darwin-arm64": { 833 + "version": "4.62.2", 834 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", 835 + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", 836 + "cpu": [ 837 + "arm64" 838 + ], 839 + "dev": true, 840 + "license": "MIT", 841 + "optional": true, 842 + "os": [ 843 + "darwin" 844 + ] 845 + }, 846 + "node_modules/@rollup/rollup-darwin-x64": { 847 + "version": "4.62.2", 848 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", 849 + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", 850 + "cpu": [ 851 + "x64" 852 + ], 853 + "dev": true, 854 + "license": "MIT", 855 + "optional": true, 856 + "os": [ 857 + "darwin" 858 + ] 859 + }, 860 + "node_modules/@rollup/rollup-freebsd-arm64": { 861 + "version": "4.62.2", 862 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", 863 + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", 864 + "cpu": [ 865 + "arm64" 866 + ], 867 + "dev": true, 868 + "license": "MIT", 869 + "optional": true, 870 + "os": [ 871 + "freebsd" 872 + ] 873 + }, 874 + "node_modules/@rollup/rollup-freebsd-x64": { 875 + "version": "4.62.2", 876 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", 877 + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", 878 + "cpu": [ 879 + "x64" 880 + ], 881 + "dev": true, 882 + "license": "MIT", 883 + "optional": true, 884 + "os": [ 885 + "freebsd" 886 + ] 887 + }, 888 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 889 + "version": "4.62.2", 890 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", 891 + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", 892 + "cpu": [ 893 + "arm" 894 + ], 895 + "dev": true, 896 + "license": "MIT", 897 + "optional": true, 898 + "os": [ 899 + "linux" 900 + ] 901 + }, 902 + "node_modules/@rollup/rollup-linux-arm-musleabihf": { 903 + "version": "4.62.2", 904 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", 905 + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", 906 + "cpu": [ 907 + "arm" 908 + ], 909 + "dev": true, 910 + "license": "MIT", 911 + "optional": true, 912 + "os": [ 913 + "linux" 914 + ] 915 + }, 916 + "node_modules/@rollup/rollup-linux-arm64-gnu": { 917 + "version": "4.62.2", 918 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", 919 + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", 920 + "cpu": [ 921 + "arm64" 922 + ], 923 + "dev": true, 924 + "license": "MIT", 925 + "optional": true, 926 + "os": [ 927 + "linux" 928 + ] 929 + }, 930 + "node_modules/@rollup/rollup-linux-arm64-musl": { 931 + "version": "4.62.2", 932 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", 933 + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", 934 + "cpu": [ 935 + "arm64" 936 + ], 937 + "dev": true, 938 + "license": "MIT", 939 + "optional": true, 940 + "os": [ 941 + "linux" 942 + ] 943 + }, 944 + "node_modules/@rollup/rollup-linux-loong64-gnu": { 945 + "version": "4.62.2", 946 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", 947 + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", 948 + "cpu": [ 949 + "loong64" 950 + ], 951 + "dev": true, 952 + "license": "MIT", 953 + "optional": true, 954 + "os": [ 955 + "linux" 956 + ] 957 + }, 958 + "node_modules/@rollup/rollup-linux-loong64-musl": { 959 + "version": "4.62.2", 960 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", 961 + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", 962 + "cpu": [ 963 + "loong64" 964 + ], 965 + "dev": true, 966 + "license": "MIT", 967 + "optional": true, 968 + "os": [ 969 + "linux" 970 + ] 971 + }, 972 + "node_modules/@rollup/rollup-linux-ppc64-gnu": { 973 + "version": "4.62.2", 974 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", 975 + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", 976 + "cpu": [ 977 + "ppc64" 978 + ], 979 + "dev": true, 980 + "license": "MIT", 981 + "optional": true, 982 + "os": [ 983 + "linux" 984 + ] 985 + }, 986 + "node_modules/@rollup/rollup-linux-ppc64-musl": { 987 + "version": "4.62.2", 988 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", 989 + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", 990 + "cpu": [ 991 + "ppc64" 992 + ], 993 + "dev": true, 994 + "license": "MIT", 995 + "optional": true, 996 + "os": [ 997 + "linux" 998 + ] 999 + }, 1000 + "node_modules/@rollup/rollup-linux-riscv64-gnu": { 1001 + "version": "4.62.2", 1002 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", 1003 + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", 1004 + "cpu": [ 1005 + "riscv64" 1006 + ], 1007 + "dev": true, 1008 + "license": "MIT", 1009 + "optional": true, 1010 + "os": [ 1011 + "linux" 1012 + ] 1013 + }, 1014 + "node_modules/@rollup/rollup-linux-riscv64-musl": { 1015 + "version": "4.62.2", 1016 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", 1017 + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", 1018 + "cpu": [ 1019 + "riscv64" 1020 + ], 1021 + "dev": true, 1022 + "license": "MIT", 1023 + "optional": true, 1024 + "os": [ 1025 + "linux" 1026 + ] 1027 + }, 1028 + "node_modules/@rollup/rollup-linux-s390x-gnu": { 1029 + "version": "4.62.2", 1030 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", 1031 + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", 1032 + "cpu": [ 1033 + "s390x" 1034 + ], 1035 + "dev": true, 1036 + "license": "MIT", 1037 + "optional": true, 1038 + "os": [ 1039 + "linux" 1040 + ] 1041 + }, 1042 + "node_modules/@rollup/rollup-linux-x64-gnu": { 1043 + "version": "4.62.2", 1044 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", 1045 + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", 1046 + "cpu": [ 1047 + "x64" 1048 + ], 1049 + "dev": true, 1050 + "license": "MIT", 1051 + "optional": true, 1052 + "os": [ 1053 + "linux" 1054 + ] 1055 + }, 1056 + "node_modules/@rollup/rollup-linux-x64-musl": { 1057 + "version": "4.62.2", 1058 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", 1059 + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", 1060 + "cpu": [ 1061 + "x64" 1062 + ], 1063 + "dev": true, 1064 + "license": "MIT", 1065 + "optional": true, 1066 + "os": [ 1067 + "linux" 1068 + ] 1069 + }, 1070 + "node_modules/@rollup/rollup-openbsd-x64": { 1071 + "version": "4.62.2", 1072 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", 1073 + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", 1074 + "cpu": [ 1075 + "x64" 1076 + ], 1077 + "dev": true, 1078 + "license": "MIT", 1079 + "optional": true, 1080 + "os": [ 1081 + "openbsd" 1082 + ] 1083 + }, 1084 + "node_modules/@rollup/rollup-openharmony-arm64": { 1085 + "version": "4.62.2", 1086 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", 1087 + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", 1088 + "cpu": [ 1089 + "arm64" 1090 + ], 1091 + "dev": true, 1092 + "license": "MIT", 1093 + "optional": true, 1094 + "os": [ 1095 + "openharmony" 1096 + ] 1097 + }, 1098 + "node_modules/@rollup/rollup-win32-arm64-msvc": { 1099 + "version": "4.62.2", 1100 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", 1101 + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", 1102 + "cpu": [ 1103 + "arm64" 1104 + ], 1105 + "dev": true, 1106 + "license": "MIT", 1107 + "optional": true, 1108 + "os": [ 1109 + "win32" 1110 + ] 1111 + }, 1112 + "node_modules/@rollup/rollup-win32-ia32-msvc": { 1113 + "version": "4.62.2", 1114 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", 1115 + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", 1116 + "cpu": [ 1117 + "ia32" 1118 + ], 1119 + "dev": true, 1120 + "license": "MIT", 1121 + "optional": true, 1122 + "os": [ 1123 + "win32" 1124 + ] 1125 + }, 1126 + "node_modules/@rollup/rollup-win32-x64-gnu": { 1127 + "version": "4.62.2", 1128 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", 1129 + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", 1130 + "cpu": [ 1131 + "x64" 1132 + ], 1133 + "dev": true, 1134 + "license": "MIT", 1135 + "optional": true, 1136 + "os": [ 1137 + "win32" 1138 + ] 1139 + }, 1140 + "node_modules/@rollup/rollup-win32-x64-msvc": { 1141 + "version": "4.62.2", 1142 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", 1143 + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", 1144 + "cpu": [ 1145 + "x64" 1146 + ], 1147 + "dev": true, 1148 + "license": "MIT", 1149 + "optional": true, 1150 + "os": [ 1151 + "win32" 1152 + ] 1153 + }, 1154 + "node_modules/@simplewebauthn/browser": { 1155 + "version": "13.3.0", 1156 + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz", 1157 + "integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==", 1158 + "license": "MIT" 1159 + }, 1160 + "node_modules/@types/babel__core": { 1161 + "version": "7.20.5", 1162 + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", 1163 + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", 1164 + "dev": true, 1165 + "license": "MIT", 1166 + "dependencies": { 1167 + "@babel/parser": "^7.20.7", 1168 + "@babel/types": "^7.20.7", 1169 + "@types/babel__generator": "*", 1170 + "@types/babel__template": "*", 1171 + "@types/babel__traverse": "*" 1172 + } 1173 + }, 1174 + "node_modules/@types/babel__generator": { 1175 + "version": "7.27.0", 1176 + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", 1177 + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", 1178 + "dev": true, 1179 + "license": "MIT", 1180 + "dependencies": { 1181 + "@babel/types": "^7.0.0" 1182 + } 1183 + }, 1184 + "node_modules/@types/babel__template": { 1185 + "version": "7.4.4", 1186 + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", 1187 + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", 1188 + "dev": true, 1189 + "license": "MIT", 1190 + "dependencies": { 1191 + "@babel/parser": "^7.1.0", 1192 + "@babel/types": "^7.0.0" 1193 + } 1194 + }, 1195 + "node_modules/@types/babel__traverse": { 1196 + "version": "7.28.0", 1197 + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", 1198 + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", 1199 + "dev": true, 1200 + "license": "MIT", 1201 + "dependencies": { 1202 + "@babel/types": "^7.28.2" 1203 + } 1204 + }, 1205 + "node_modules/@types/estree": { 1206 + "version": "1.0.9", 1207 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", 1208 + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", 1209 + "dev": true, 1210 + "license": "MIT" 1211 + }, 1212 + "node_modules/@types/react": { 1213 + "version": "19.2.17", 1214 + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", 1215 + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", 1216 + "dev": true, 1217 + "license": "MIT", 1218 + "dependencies": { 1219 + "csstype": "^3.2.2" 1220 + } 1221 + }, 1222 + "node_modules/@types/react-dom": { 1223 + "version": "19.2.3", 1224 + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", 1225 + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", 1226 + "dev": true, 1227 + "license": "MIT", 1228 + "peerDependencies": { 1229 + "@types/react": "^19.2.0" 1230 + } 1231 + }, 1232 + "node_modules/@vitejs/plugin-react": { 1233 + "version": "4.7.0", 1234 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", 1235 + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", 1236 + "dev": true, 1237 + "license": "MIT", 1238 + "dependencies": { 1239 + "@babel/core": "^7.28.0", 1240 + "@babel/plugin-transform-react-jsx-self": "^7.27.1", 1241 + "@babel/plugin-transform-react-jsx-source": "^7.27.1", 1242 + "@rolldown/pluginutils": "1.0.0-beta.27", 1243 + "@types/babel__core": "^7.20.5", 1244 + "react-refresh": "^0.17.0" 1245 + }, 1246 + "engines": { 1247 + "node": "^14.18.0 || >=16.0.0" 1248 + }, 1249 + "peerDependencies": { 1250 + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" 1251 + } 1252 + }, 1253 + "node_modules/baseline-browser-mapping": { 1254 + "version": "2.10.42", 1255 + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", 1256 + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", 1257 + "dev": true, 1258 + "license": "Apache-2.0", 1259 + "bin": { 1260 + "baseline-browser-mapping": "dist/cli.cjs" 1261 + }, 1262 + "engines": { 1263 + "node": ">=6.0.0" 1264 + } 1265 + }, 1266 + "node_modules/browserslist": { 1267 + "version": "4.28.5", 1268 + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", 1269 + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", 1270 + "dev": true, 1271 + "funding": [ 1272 + { 1273 + "type": "opencollective", 1274 + "url": "https://opencollective.com/browserslist" 1275 + }, 1276 + { 1277 + "type": "tidelift", 1278 + "url": "https://tidelift.com/funding/github/npm/browserslist" 1279 + }, 1280 + { 1281 + "type": "github", 1282 + "url": "https://github.com/sponsors/ai" 1283 + } 1284 + ], 1285 + "license": "MIT", 1286 + "dependencies": { 1287 + "baseline-browser-mapping": "^2.10.42", 1288 + "caniuse-lite": "^1.0.30001800", 1289 + "electron-to-chromium": "^1.5.387", 1290 + "node-releases": "^2.0.50", 1291 + "update-browserslist-db": "^1.2.3" 1292 + }, 1293 + "bin": { 1294 + "browserslist": "cli.js" 1295 + }, 1296 + "engines": { 1297 + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 1298 + } 1299 + }, 1300 + "node_modules/caniuse-lite": { 1301 + "version": "1.0.30001803", 1302 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", 1303 + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", 1304 + "dev": true, 1305 + "funding": [ 1306 + { 1307 + "type": "opencollective", 1308 + "url": "https://opencollective.com/browserslist" 1309 + }, 1310 + { 1311 + "type": "tidelift", 1312 + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 1313 + }, 1314 + { 1315 + "type": "github", 1316 + "url": "https://github.com/sponsors/ai" 1317 + } 1318 + ], 1319 + "license": "CC-BY-4.0" 1320 + }, 1321 + "node_modules/convert-source-map": { 1322 + "version": "2.0.0", 1323 + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", 1324 + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", 1325 + "dev": true, 1326 + "license": "MIT" 1327 + }, 1328 + "node_modules/csstype": { 1329 + "version": "3.2.3", 1330 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", 1331 + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", 1332 + "dev": true, 1333 + "license": "MIT" 1334 + }, 1335 + "node_modules/debug": { 1336 + "version": "4.4.3", 1337 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 1338 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 1339 + "dev": true, 1340 + "license": "MIT", 1341 + "dependencies": { 1342 + "ms": "^2.1.3" 1343 + }, 1344 + "engines": { 1345 + "node": ">=6.0" 1346 + }, 1347 + "peerDependenciesMeta": { 1348 + "supports-color": { 1349 + "optional": true 1350 + } 1351 + } 1352 + }, 1353 + "node_modules/electron-to-chromium": { 1354 + "version": "1.5.389", 1355 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", 1356 + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", 1357 + "dev": true, 1358 + "license": "ISC" 1359 + }, 1360 + "node_modules/esbuild": { 1361 + "version": "0.25.12", 1362 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", 1363 + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", 1364 + "dev": true, 1365 + "hasInstallScript": true, 1366 + "license": "MIT", 1367 + "bin": { 1368 + "esbuild": "bin/esbuild" 1369 + }, 1370 + "engines": { 1371 + "node": ">=18" 1372 + }, 1373 + "optionalDependencies": { 1374 + "@esbuild/aix-ppc64": "0.25.12", 1375 + "@esbuild/android-arm": "0.25.12", 1376 + "@esbuild/android-arm64": "0.25.12", 1377 + "@esbuild/android-x64": "0.25.12", 1378 + "@esbuild/darwin-arm64": "0.25.12", 1379 + "@esbuild/darwin-x64": "0.25.12", 1380 + "@esbuild/freebsd-arm64": "0.25.12", 1381 + "@esbuild/freebsd-x64": "0.25.12", 1382 + "@esbuild/linux-arm": "0.25.12", 1383 + "@esbuild/linux-arm64": "0.25.12", 1384 + "@esbuild/linux-ia32": "0.25.12", 1385 + "@esbuild/linux-loong64": "0.25.12", 1386 + "@esbuild/linux-mips64el": "0.25.12", 1387 + "@esbuild/linux-ppc64": "0.25.12", 1388 + "@esbuild/linux-riscv64": "0.25.12", 1389 + "@esbuild/linux-s390x": "0.25.12", 1390 + "@esbuild/linux-x64": "0.25.12", 1391 + "@esbuild/netbsd-arm64": "0.25.12", 1392 + "@esbuild/netbsd-x64": "0.25.12", 1393 + "@esbuild/openbsd-arm64": "0.25.12", 1394 + "@esbuild/openbsd-x64": "0.25.12", 1395 + "@esbuild/openharmony-arm64": "0.25.12", 1396 + "@esbuild/sunos-x64": "0.25.12", 1397 + "@esbuild/win32-arm64": "0.25.12", 1398 + "@esbuild/win32-ia32": "0.25.12", 1399 + "@esbuild/win32-x64": "0.25.12" 1400 + } 1401 + }, 1402 + "node_modules/escalade": { 1403 + "version": "3.2.0", 1404 + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 1405 + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 1406 + "dev": true, 1407 + "license": "MIT", 1408 + "engines": { 1409 + "node": ">=6" 1410 + } 1411 + }, 1412 + "node_modules/fdir": { 1413 + "version": "6.5.0", 1414 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", 1415 + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", 1416 + "dev": true, 1417 + "license": "MIT", 1418 + "engines": { 1419 + "node": ">=12.0.0" 1420 + }, 1421 + "peerDependencies": { 1422 + "picomatch": "^3 || ^4" 1423 + }, 1424 + "peerDependenciesMeta": { 1425 + "picomatch": { 1426 + "optional": true 1427 + } 1428 + } 1429 + }, 1430 + "node_modules/fsevents": { 1431 + "version": "2.3.3", 1432 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1433 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1434 + "dev": true, 1435 + "hasInstallScript": true, 1436 + "license": "MIT", 1437 + "optional": true, 1438 + "os": [ 1439 + "darwin" 1440 + ], 1441 + "engines": { 1442 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1443 + } 1444 + }, 1445 + "node_modules/gensync": { 1446 + "version": "1.0.0-beta.2", 1447 + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", 1448 + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", 1449 + "dev": true, 1450 + "license": "MIT", 1451 + "engines": { 1452 + "node": ">=6.9.0" 1453 + } 1454 + }, 1455 + "node_modules/js-tokens": { 1456 + "version": "4.0.0", 1457 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 1458 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 1459 + "dev": true, 1460 + "license": "MIT" 1461 + }, 1462 + "node_modules/jsesc": { 1463 + "version": "3.1.0", 1464 + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", 1465 + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", 1466 + "dev": true, 1467 + "license": "MIT", 1468 + "bin": { 1469 + "jsesc": "bin/jsesc" 1470 + }, 1471 + "engines": { 1472 + "node": ">=6" 1473 + } 1474 + }, 1475 + "node_modules/json5": { 1476 + "version": "2.2.3", 1477 + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", 1478 + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", 1479 + "dev": true, 1480 + "license": "MIT", 1481 + "bin": { 1482 + "json5": "lib/cli.js" 1483 + }, 1484 + "engines": { 1485 + "node": ">=6" 1486 + } 1487 + }, 1488 + "node_modules/lru-cache": { 1489 + "version": "5.1.1", 1490 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", 1491 + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", 1492 + "dev": true, 1493 + "license": "ISC", 1494 + "dependencies": { 1495 + "yallist": "^3.0.2" 1496 + } 1497 + }, 1498 + "node_modules/ms": { 1499 + "version": "2.1.3", 1500 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1501 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1502 + "dev": true, 1503 + "license": "MIT" 1504 + }, 1505 + "node_modules/nanoid": { 1506 + "version": "3.3.15", 1507 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", 1508 + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", 1509 + "dev": true, 1510 + "funding": [ 1511 + { 1512 + "type": "github", 1513 + "url": "https://github.com/sponsors/ai" 1514 + } 1515 + ], 1516 + "license": "MIT", 1517 + "bin": { 1518 + "nanoid": "bin/nanoid.cjs" 1519 + }, 1520 + "engines": { 1521 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1522 + } 1523 + }, 1524 + "node_modules/node-releases": { 1525 + "version": "2.0.51", 1526 + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", 1527 + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", 1528 + "dev": true, 1529 + "license": "MIT", 1530 + "engines": { 1531 + "node": ">=18" 1532 + } 1533 + }, 1534 + "node_modules/picocolors": { 1535 + "version": "1.1.1", 1536 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 1537 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 1538 + "dev": true, 1539 + "license": "ISC" 1540 + }, 1541 + "node_modules/picomatch": { 1542 + "version": "4.0.5", 1543 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", 1544 + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", 1545 + "dev": true, 1546 + "license": "MIT", 1547 + "engines": { 1548 + "node": ">=12" 1549 + }, 1550 + "funding": { 1551 + "url": "https://github.com/sponsors/jonschlinkert" 1552 + } 1553 + }, 1554 + "node_modules/postcss": { 1555 + "version": "8.5.16", 1556 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", 1557 + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", 1558 + "dev": true, 1559 + "funding": [ 1560 + { 1561 + "type": "opencollective", 1562 + "url": "https://opencollective.com/postcss/" 1563 + }, 1564 + { 1565 + "type": "tidelift", 1566 + "url": "https://tidelift.com/funding/github/npm/postcss" 1567 + }, 1568 + { 1569 + "type": "github", 1570 + "url": "https://github.com/sponsors/ai" 1571 + } 1572 + ], 1573 + "license": "MIT", 1574 + "dependencies": { 1575 + "nanoid": "^3.3.12", 1576 + "picocolors": "^1.1.1", 1577 + "source-map-js": "^1.2.1" 1578 + }, 1579 + "engines": { 1580 + "node": "^10 || ^12 || >=14" 1581 + } 1582 + }, 1583 + "node_modules/react": { 1584 + "version": "19.2.7", 1585 + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", 1586 + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", 1587 + "license": "MIT", 1588 + "engines": { 1589 + "node": ">=0.10.0" 1590 + } 1591 + }, 1592 + "node_modules/react-dom": { 1593 + "version": "19.2.7", 1594 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", 1595 + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", 1596 + "license": "MIT", 1597 + "dependencies": { 1598 + "scheduler": "^0.27.0" 1599 + }, 1600 + "peerDependencies": { 1601 + "react": "^19.2.7" 1602 + } 1603 + }, 1604 + "node_modules/react-refresh": { 1605 + "version": "0.17.0", 1606 + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", 1607 + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", 1608 + "dev": true, 1609 + "license": "MIT", 1610 + "engines": { 1611 + "node": ">=0.10.0" 1612 + } 1613 + }, 1614 + "node_modules/rollup": { 1615 + "version": "4.62.2", 1616 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", 1617 + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", 1618 + "dev": true, 1619 + "license": "MIT", 1620 + "dependencies": { 1621 + "@types/estree": "1.0.9" 1622 + }, 1623 + "bin": { 1624 + "rollup": "dist/bin/rollup" 1625 + }, 1626 + "engines": { 1627 + "node": ">=18.0.0", 1628 + "npm": ">=8.0.0" 1629 + }, 1630 + "optionalDependencies": { 1631 + "@rollup/rollup-android-arm-eabi": "4.62.2", 1632 + "@rollup/rollup-android-arm64": "4.62.2", 1633 + "@rollup/rollup-darwin-arm64": "4.62.2", 1634 + "@rollup/rollup-darwin-x64": "4.62.2", 1635 + "@rollup/rollup-freebsd-arm64": "4.62.2", 1636 + "@rollup/rollup-freebsd-x64": "4.62.2", 1637 + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", 1638 + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", 1639 + "@rollup/rollup-linux-arm64-gnu": "4.62.2", 1640 + "@rollup/rollup-linux-arm64-musl": "4.62.2", 1641 + "@rollup/rollup-linux-loong64-gnu": "4.62.2", 1642 + "@rollup/rollup-linux-loong64-musl": "4.62.2", 1643 + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", 1644 + "@rollup/rollup-linux-ppc64-musl": "4.62.2", 1645 + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", 1646 + "@rollup/rollup-linux-riscv64-musl": "4.62.2", 1647 + "@rollup/rollup-linux-s390x-gnu": "4.62.2", 1648 + "@rollup/rollup-linux-x64-gnu": "4.62.2", 1649 + "@rollup/rollup-linux-x64-musl": "4.62.2", 1650 + "@rollup/rollup-openbsd-x64": "4.62.2", 1651 + "@rollup/rollup-openharmony-arm64": "4.62.2", 1652 + "@rollup/rollup-win32-arm64-msvc": "4.62.2", 1653 + "@rollup/rollup-win32-ia32-msvc": "4.62.2", 1654 + "@rollup/rollup-win32-x64-gnu": "4.62.2", 1655 + "@rollup/rollup-win32-x64-msvc": "4.62.2", 1656 + "fsevents": "~2.3.2" 1657 + } 1658 + }, 1659 + "node_modules/scheduler": { 1660 + "version": "0.27.0", 1661 + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", 1662 + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", 1663 + "license": "MIT" 1664 + }, 1665 + "node_modules/semver": { 1666 + "version": "6.3.1", 1667 + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", 1668 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", 1669 + "dev": true, 1670 + "license": "ISC", 1671 + "bin": { 1672 + "semver": "bin/semver.js" 1673 + } 1674 + }, 1675 + "node_modules/source-map-js": { 1676 + "version": "1.2.1", 1677 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 1678 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 1679 + "dev": true, 1680 + "license": "BSD-3-Clause", 1681 + "engines": { 1682 + "node": ">=0.10.0" 1683 + } 1684 + }, 1685 + "node_modules/tinyglobby": { 1686 + "version": "0.2.17", 1687 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", 1688 + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", 1689 + "dev": true, 1690 + "license": "MIT", 1691 + "dependencies": { 1692 + "fdir": "^6.5.0", 1693 + "picomatch": "^4.0.4" 1694 + }, 1695 + "engines": { 1696 + "node": ">=12.0.0" 1697 + }, 1698 + "funding": { 1699 + "url": "https://github.com/sponsors/SuperchupuDev" 1700 + } 1701 + }, 1702 + "node_modules/typescript": { 1703 + "version": "5.9.3", 1704 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 1705 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 1706 + "dev": true, 1707 + "license": "Apache-2.0", 1708 + "bin": { 1709 + "tsc": "bin/tsc", 1710 + "tsserver": "bin/tsserver" 1711 + }, 1712 + "engines": { 1713 + "node": ">=14.17" 1714 + } 1715 + }, 1716 + "node_modules/update-browserslist-db": { 1717 + "version": "1.2.3", 1718 + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", 1719 + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", 1720 + "dev": true, 1721 + "funding": [ 1722 + { 1723 + "type": "opencollective", 1724 + "url": "https://opencollective.com/browserslist" 1725 + }, 1726 + { 1727 + "type": "tidelift", 1728 + "url": "https://tidelift.com/funding/github/npm/browserslist" 1729 + }, 1730 + { 1731 + "type": "github", 1732 + "url": "https://github.com/sponsors/ai" 1733 + } 1734 + ], 1735 + "license": "MIT", 1736 + "dependencies": { 1737 + "escalade": "^3.2.0", 1738 + "picocolors": "^1.1.1" 1739 + }, 1740 + "bin": { 1741 + "update-browserslist-db": "cli.js" 1742 + }, 1743 + "peerDependencies": { 1744 + "browserslist": ">= 4.21.0" 1745 + } 1746 + }, 1747 + "node_modules/vite": { 1748 + "version": "6.4.3", 1749 + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", 1750 + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", 1751 + "dev": true, 1752 + "license": "MIT", 1753 + "dependencies": { 1754 + "esbuild": "^0.25.0", 1755 + "fdir": "^6.4.4", 1756 + "picomatch": "^4.0.2", 1757 + "postcss": "^8.5.3", 1758 + "rollup": "^4.34.9", 1759 + "tinyglobby": "^0.2.13" 1760 + }, 1761 + "bin": { 1762 + "vite": "bin/vite.js" 1763 + }, 1764 + "engines": { 1765 + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 1766 + }, 1767 + "funding": { 1768 + "url": "https://github.com/vitejs/vite?sponsor=1" 1769 + }, 1770 + "optionalDependencies": { 1771 + "fsevents": "~2.3.3" 1772 + }, 1773 + "peerDependencies": { 1774 + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", 1775 + "jiti": ">=1.21.0", 1776 + "less": "*", 1777 + "lightningcss": "^1.21.0", 1778 + "sass": "*", 1779 + "sass-embedded": "*", 1780 + "stylus": "*", 1781 + "sugarss": "*", 1782 + "terser": "^5.16.0", 1783 + "tsx": "^4.8.1", 1784 + "yaml": "^2.4.2" 1785 + }, 1786 + "peerDependenciesMeta": { 1787 + "@types/node": { 1788 + "optional": true 1789 + }, 1790 + "jiti": { 1791 + "optional": true 1792 + }, 1793 + "less": { 1794 + "optional": true 1795 + }, 1796 + "lightningcss": { 1797 + "optional": true 1798 + }, 1799 + "sass": { 1800 + "optional": true 1801 + }, 1802 + "sass-embedded": { 1803 + "optional": true 1804 + }, 1805 + "stylus": { 1806 + "optional": true 1807 + }, 1808 + "sugarss": { 1809 + "optional": true 1810 + }, 1811 + "terser": { 1812 + "optional": true 1813 + }, 1814 + "tsx": { 1815 + "optional": true 1816 + }, 1817 + "yaml": { 1818 + "optional": true 1819 + } 1820 + } 1821 + }, 1822 + "node_modules/yallist": { 1823 + "version": "3.1.1", 1824 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 1825 + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", 1826 + "dev": true, 1827 + "license": "ISC" 1828 + } 1829 + } 1830 + }
+23
web/package.json
··· 1 + { 2 + "name": "pds-operator-web", 3 + "version": "0.1.0", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite", 8 + "build": "tsc -b && vite build", 9 + "preview": "vite preview" 10 + }, 11 + "dependencies": { 12 + "@simplewebauthn/browser": "^13.3.0", 13 + "react": "^19.0.0", 14 + "react-dom": "^19.0.0" 15 + }, 16 + "devDependencies": { 17 + "@types/react": "^19.0.10", 18 + "@types/react-dom": "^19.0.4", 19 + "@vitejs/plugin-react": "^4.3.4", 20 + "typescript": "^5.7.3", 21 + "vite": "^6.1.0" 22 + } 23 + }
+311
web/src/AccountsPanel.tsx
··· 1 + import { useEffect, useState, useCallback } from "react"; 2 + import { api, type Account } from "./api.js"; 3 + import { useToast } from "./useToast.js"; 4 + 5 + const PAGE_SIZES = [25, 50, 100, 250]; 6 + 7 + export function AccountsPanel({ appviewUrl }: { appviewUrl: string }) { 8 + const [accounts, setAccounts] = useState<Account[]>([]); 9 + const [total, setTotal] = useState(0); 10 + const [flaggedTotal, setFlaggedTotal] = useState(0); 11 + // DM notifications deep-link here with ?q=<handle> to pull up the flagged account 12 + const [query, setQuery] = useState( 13 + () => new URLSearchParams(window.location.search).get("q") ?? "", 14 + ); 15 + const [hideTakendown, setHideTakendown] = useState(true); 16 + const [hideEmails, setHideEmails] = useState( 17 + () => localStorage.getItem("hideEmails") !== "0", 18 + ); 19 + const [pageSize, setPageSize] = useState(() => { 20 + const saved = Number(localStorage.getItem("accountsPageSize")); 21 + return PAGE_SIZES.includes(saved) ? saved : 25; 22 + }); 23 + const [loading, setLoading] = useState(false); 24 + const [error, setError] = useState<string | null>(null); 25 + const [busyDid, setBusyDid] = useState<string | null>(null); 26 + const [confirmDid, setConfirmDid] = useState<string | null>(null); 27 + const [menuDid, setMenuDid] = useState<string | null>(null); 28 + const [toast, showToast] = useToast(); 29 + 30 + const copy = (text: string, what: string, e: React.MouseEvent) => { 31 + const at = { clientX: e.clientX, clientY: e.clientY }; 32 + setMenuDid(null); 33 + navigator.clipboard 34 + .writeText(text) 35 + .then(() => showToast(`${what} copied`, at)) 36 + .catch(() => showToast("copy failed", at)); 37 + }; 38 + 39 + useEffect(() => { 40 + if (!menuDid) return; 41 + const close = (e: MouseEvent | KeyboardEvent) => { 42 + if (e instanceof KeyboardEvent && e.key !== "Escape") return; 43 + setMenuDid(null); 44 + setConfirmDid(null); 45 + }; 46 + document.addEventListener("click", close); 47 + document.addEventListener("keydown", close); 48 + return () => { 49 + document.removeEventListener("click", close); 50 + document.removeEventListener("keydown", close); 51 + }; 52 + }, [menuDid]); 53 + 54 + const refresh = useCallback(() => { 55 + setLoading(true); 56 + api 57 + .accounts({ q: query.trim() || undefined, limit: pageSize, hideTakendown }) 58 + .then((r) => { 59 + setAccounts(r.accounts); 60 + setTotal(r.total); 61 + setFlaggedTotal(r.flaggedTotal); 62 + setError(null); 63 + }) 64 + .catch((e) => setError(e.message)) 65 + .finally(() => setLoading(false)); 66 + }, [query, hideTakendown, pageSize]); 67 + 68 + useEffect(() => { 69 + const id = setTimeout(refresh, query.trim() ? 300 : 0); 70 + return () => clearTimeout(id); 71 + }, [refresh, query]); 72 + 73 + const loadMore = () => { 74 + setLoading(true); 75 + api 76 + .accounts({ 77 + q: query.trim() || undefined, 78 + offset: accounts.length, 79 + limit: pageSize, 80 + hideTakendown, 81 + }) 82 + .then((r) => { 83 + setAccounts((prev) => [...prev, ...r.accounts]); 84 + setTotal(r.total); 85 + }) 86 + .catch((e) => setError(e.message)) 87 + .finally(() => setLoading(false)); 88 + }; 89 + 90 + const runAction = async (did: string, action: "takedown" | "enable" | "resetPassword") => { 91 + setBusyDid(did); 92 + setConfirmDid(null); 93 + try { 94 + await api[action](did); 95 + refresh(); 96 + } catch (e) { 97 + setError((e as Error).message); 98 + } finally { 99 + setBusyDid(null); 100 + } 101 + }; 102 + 103 + const tableHead = ( 104 + <thead> 105 + <tr> 106 + <th>handle</th> 107 + <th>did</th> 108 + <th>status</th> 109 + <th>indexed</th> 110 + <th></th> 111 + </tr> 112 + </thead> 113 + ); 114 + 115 + const renderRow = (a: Account) => ( 116 + <tr key={a.did}> 117 + <td> 118 + <span className="handle-cell"> 119 + {a.avatar ? ( 120 + <img className="avatar" src={a.avatar} alt="" loading="lazy" /> 121 + ) : ( 122 + <span className="avatar" aria-hidden="true" /> 123 + )} 124 + <a 125 + className="handle-link" 126 + href={`${appviewUrl}/profile/${a.did}`} 127 + target="_blank" 128 + rel="noopener noreferrer" 129 + > 130 + {a.handle} 131 + </a> 132 + </span> 133 + {(a.labels?.length ?? 0) > 0 && <div className="labels">{a.labels!.join(", ")}</div>} 134 + </td> 135 + <td className="mono-dim did-cell"> 136 + <div>{a.did}</div> 137 + {a.email && !hideEmails && <div className="email">{a.email}</div>} 138 + </td> 139 + <td 140 + className={`status-cell ${ 141 + a.status === "takendown" ? "error" : a.status === "deactivated" ? "warn" : "ok" 142 + }`} 143 + > 144 + {a.status ?? "active"} 145 + </td> 146 + <td className="mono-dim">{new Date(a.indexedAt).toLocaleDateString()}</td> 147 + <td> 148 + <div className="menu-wrap" onClick={(e) => e.stopPropagation()}> 149 + <button 150 + className="kebab" 151 + aria-label={`actions for ${a.handle}`} 152 + aria-haspopup="menu" 153 + aria-expanded={menuDid === a.did} 154 + disabled={busyDid === a.did} 155 + onClick={() => { 156 + setMenuDid(menuDid === a.did ? null : a.did); 157 + setConfirmDid(null); 158 + }} 159 + > 160 + 161 + </button> 162 + {menuDid === a.did && ( 163 + <div className="menu" role="menu"> 164 + {confirmDid === a.did ? ( 165 + <> 166 + <button 167 + className="danger" 168 + role="menuitem" 169 + onClick={() => { 170 + setMenuDid(null); 171 + runAction(a.did, a.status === "takendown" ? "enable" : "takedown"); 172 + }} 173 + > 174 + confirm {a.status === "takendown" ? "enable" : "takedown"} 175 + </button> 176 + <button role="menuitem" onClick={() => setConfirmDid(null)}> 177 + cancel 178 + </button> 179 + </> 180 + ) : ( 181 + <> 182 + <button 183 + role="menuitem" 184 + onClick={() => { 185 + setMenuDid(null); 186 + window.open(`${appviewUrl}/profile/${a.did}`, "_blank", "noopener"); 187 + }} 188 + > 189 + visit profile 190 + </button> 191 + <button role="menuitem" onClick={(e) => copy(a.handle, "username", e)}> 192 + copy username 193 + </button> 194 + <button role="menuitem" onClick={(e) => copy(a.did, "did", e)}> 195 + copy did 196 + </button> 197 + {a.email && ( 198 + <button role="menuitem" onClick={(e) => copy(a.email ?? "", "email", e)}> 199 + copy email 200 + </button> 201 + )} 202 + <div className="menu-sep" /> 203 + <button role="menuitem" onClick={() => setConfirmDid(a.did)}> 204 + {a.status === "takendown" ? "enable" : "takedown"} 205 + </button> 206 + <button 207 + role="menuitem" 208 + onClick={() => { 209 + setMenuDid(null); 210 + runAction(a.did, "resetPassword"); 211 + }} 212 + > 213 + reset password 214 + </button> 215 + </> 216 + )} 217 + </div> 218 + )} 219 + </div> 220 + </td> 221 + </tr> 222 + ); 223 + 224 + return ( 225 + <> 226 + <div className="panel"> 227 + <h2> 228 + accounts ({total - flaggedTotal} 229 + {query.trim() ? " matching" : ""} 230 + {flaggedTotal > 0 ? ` + ${flaggedTotal} flagged` : ""}) 231 + </h2> 232 + <div className="accounts-toolbar"> 233 + <span className="search-wrap"> 234 + <input 235 + type="search" 236 + placeholder="search handle, email, or did…" 237 + value={query} 238 + onChange={(e) => setQuery(e.target.value)} 239 + /> 240 + {query && ( 241 + <button 242 + type="button" 243 + className="clear-search" 244 + aria-label="clear search" 245 + onClick={() => setQuery("")} 246 + > 247 + 248 + </button> 249 + )} 250 + </span> 251 + <label> 252 + <input 253 + type="checkbox" 254 + checked={hideTakendown} 255 + onChange={(e) => setHideTakendown(e.target.checked)} 256 + /> 257 + hide taken down 258 + </label> 259 + <label> 260 + <input 261 + type="checkbox" 262 + checked={hideEmails} 263 + onChange={(e) => { 264 + setHideEmails(e.target.checked); 265 + localStorage.setItem("hideEmails", e.target.checked ? "1" : "0"); 266 + }} 267 + /> 268 + hide emails 269 + </label> 270 + </div> 271 + {error && <div className="error-text">{error}</div>} 272 + {!loading && accounts.length === 0 && !error && ( 273 + <p className="mono-dim empty-state"> 274 + {query.trim() ? `no accounts match “${query.trim()}”` : "no accounts to show"} 275 + </p> 276 + )} 277 + <table className="accounts-table"> 278 + {tableHead} 279 + <tbody>{accounts.map(renderRow)}</tbody> 280 + </table> 281 + <div className="accounts-footer"> 282 + {total > accounts.length ? ( 283 + <button onClick={loadMore} disabled={loading}> 284 + load more ({total - accounts.length} more) 285 + </button> 286 + ) : ( 287 + <span /> 288 + )} 289 + <label className="page-size"> 290 + per page 291 + <select 292 + value={pageSize} 293 + onChange={(e) => { 294 + const size = Number(e.target.value); 295 + setPageSize(size); 296 + localStorage.setItem("accountsPageSize", String(size)); 297 + }} 298 + > 299 + {PAGE_SIZES.map((n) => ( 300 + <option key={n} value={n}> 301 + {n} 302 + </option> 303 + ))} 304 + </select> 305 + </label> 306 + </div> 307 + {toast} 308 + </div> 309 + </> 310 + ); 311 + }
+78
web/src/App.tsx
··· 1 + import { useEffect, useState } from "react"; 2 + import { api } from "./api.js"; 3 + import { Login } from "./Login.js"; 4 + import { StatusPanel } from "./StatusPanel.js"; 5 + import { AccountsPanel } from "./AccountsPanel.js"; 6 + import { PasskeysPanel } from "./PasskeysPanel.js"; 7 + import { AuditPanel } from "./AuditPanel.js"; 8 + import { RequestCrawl } from "./RequestCrawl.js"; 9 + import { SyncFooter } from "./SyncFooter.js"; 10 + import { InvitesPanel } from "./InvitesPanel.js"; 11 + import { Enroll } from "./Enroll.js"; 12 + 13 + export function App() { 14 + const [authed, setAuthed] = useState<boolean | null>(null); 15 + const [appviewUrl, setAppviewUrl] = useState("https://bsky.app"); 16 + const [pdsHostname, setPdsHostname] = useState(""); 17 + const [passwordLogin, setPasswordLogin] = useState(true); 18 + const [lastFullSync, setLastFullSync] = useState<string | null>(null); 19 + const [enrollToken] = useState( 20 + () => new URLSearchParams(window.location.search).get("enroll"), 21 + ); 22 + 23 + useEffect(() => { 24 + api 25 + .session() 26 + .then((s) => { 27 + setAuthed(s.authenticated); 28 + if (s.appviewUrl) setAppviewUrl(s.appviewUrl); 29 + if (s.pdsHostname) setPdsHostname(s.pdsHostname); 30 + setPasswordLogin(s.passwordLogin ?? true); 31 + }) 32 + .catch(() => setAuthed(false)); 33 + }, []); 34 + 35 + useEffect(() => { 36 + document.title = pdsHostname ? `${pdsHostname} — operator` : "PDS Operator"; 37 + }, [pdsHostname]); 38 + 39 + if (authed === null) return null; 40 + if (!authed && enrollToken) { 41 + return ( 42 + <Enroll token={enrollToken} pdsHostname={pdsHostname} onEnrolled={() => setAuthed(true)} /> 43 + ); 44 + } 45 + if (!authed) { 46 + return ( 47 + <Login 48 + pdsHostname={pdsHostname} 49 + passwordLogin={passwordLogin} 50 + onLoggedIn={() => setAuthed(true)} 51 + /> 52 + ); 53 + } 54 + 55 + return ( 56 + <div className="app"> 57 + <header className="header"> 58 + <h1>{pdsHostname || "pds"} / operator</h1> 59 + <div className="header-actions"> 60 + <RequestCrawl className="nav-crawl" /> 61 + <button 62 + onClick={() => 63 + api.logout().then(() => setAuthed(false)) 64 + } 65 + > 66 + sign out 67 + </button> 68 + </div> 69 + </header> 70 + <StatusPanel onLastSync={setLastFullSync} /> 71 + <AccountsPanel appviewUrl={appviewUrl} /> 72 + <InvitesPanel /> 73 + <PasskeysPanel /> 74 + <AuditPanel /> 75 + <SyncFooter lastFullSync={lastFullSync} /> 76 + </div> 77 + ); 78 + }
+66
web/src/AuditPanel.tsx
··· 1 + import { useEffect, useState } from "react"; 2 + import { api } from "./api.js"; 3 + 4 + interface AuditEntry { 5 + at: string; 6 + operator: string; 7 + action: string; 8 + target?: string; 9 + } 10 + 11 + const INITIAL = 7; 12 + const STEP = 25; 13 + 14 + export function AuditPanel() { 15 + const [entries, setEntries] = useState<AuditEntry[]>([]); 16 + const [visible, setVisible] = useState(INITIAL); 17 + const [error, setError] = useState<string | null>(null); 18 + 19 + useEffect(() => { 20 + api 21 + .audit() 22 + .then((r) => setEntries(r.entries)) 23 + .catch((e) => setError(e.message)); 24 + }, []); 25 + 26 + return ( 27 + <div className="panel"> 28 + <h2>audit log</h2> 29 + {error && <div className="error-text">{error}</div>} 30 + {entries.length === 0 && !error && ( 31 + <p className="mono-dim empty-state">no actions recorded yet</p> 32 + )} 33 + {entries.length > 0 && ( 34 + <table className="audit-table"> 35 + <thead> 36 + <tr> 37 + <th>when</th> 38 + <th>action</th> 39 + <th>target</th> 40 + <th>operator</th> 41 + </tr> 42 + </thead> 43 + <tbody> 44 + {entries.slice(0, visible).map((e, i) => ( 45 + <tr key={i}> 46 + <td className="mono-dim">{new Date(e.at).toLocaleString()}</td> 47 + <td>{e.action}</td> 48 + <td className="mono-dim">{e.target ?? "—"}</td> 49 + <td className="mono-dim">{e.operator}</td> 50 + </tr> 51 + ))} 52 + </tbody> 53 + </table> 54 + )} 55 + {entries.length > visible && ( 56 + <button 57 + className="wide-mobile" 58 + onClick={() => setVisible((v) => v + STEP)} 59 + style={{ marginTop: 12 }} 60 + > 61 + load more ({entries.length - visible} more) 62 + </button> 63 + )} 64 + </div> 65 + ); 66 + }
+52
web/src/Enroll.tsx
··· 1 + import { useState } from "react"; 2 + import { startRegistration } from "@simplewebauthn/browser"; 3 + import { api } from "./api.js"; 4 + 5 + export function Enroll({ 6 + token, 7 + pdsHostname, 8 + onEnrolled, 9 + }: { 10 + token: string; 11 + pdsHostname: string; 12 + onEnrolled: () => void; 13 + }) { 14 + const [name, setName] = useState(""); 15 + const [error, setError] = useState<string | null>(null); 16 + const [busy, setBusy] = useState(false); 17 + 18 + const enroll = async () => { 19 + setBusy(true); 20 + setError(null); 21 + try { 22 + const options = await api.passkeyRegisterOptions(token); 23 + const response = await startRegistration({ optionsJSON: options }); 24 + await api.passkeyRegister(name.trim(), response, token); 25 + window.history.replaceState(null, "", "/"); 26 + onEnrolled(); 27 + } catch (e) { 28 + const err = e as Error; 29 + if (err.name !== "NotAllowedError") setError(err.message); 30 + } finally { 31 + setBusy(false); 32 + } 33 + }; 34 + 35 + return ( 36 + <div className="login-screen"> 37 + <div className="login-form"> 38 + <span className="mono-dim">{pdsHostname ? `${pdsHostname} — ` : ""}enroll a passkey</span> 39 + <input 40 + placeholder="name (e.g. macbook touch id)" 41 + value={name} 42 + onChange={(e) => setName(e.target.value)} 43 + autoFocus 44 + /> 45 + {error && <span className="error-text">{error}</span>} 46 + <button disabled={busy} onClick={enroll}> 47 + {busy ? "enrolling…" : "create passkey"} 48 + </button> 49 + </div> 50 + </div> 51 + ); 52 + }
+142
web/src/InvitesPanel.tsx
··· 1 + import { useCallback, useEffect, useState } from "react"; 2 + import { api } from "./api.js"; 3 + import { useToast } from "./useToast.js"; 4 + 5 + interface Invite { 6 + code: string; 7 + available: number; 8 + disabled: boolean; 9 + createdAt: string; 10 + uses: { usedBy: string; usedAt: string }[]; 11 + } 12 + 13 + export function InvitesPanel() { 14 + const [invites, setInvites] = useState<Invite[]>([]); 15 + const [hideUsed, setHideUsed] = useState(true); 16 + const [busy, setBusy] = useState(false); 17 + const [error, setError] = useState<string | null>(null); 18 + const [toast, showToast] = useToast(); 19 + 20 + const refresh = useCallback(() => { 21 + api 22 + .invites() 23 + .then((r) => setInvites(r.codes)) 24 + .catch((e) => setError(e.message)); 25 + }, []); 26 + 27 + useEffect(refresh, [refresh]); 28 + 29 + const create = async () => { 30 + setBusy(true); 31 + setError(null); 32 + try { 33 + await api.inviteCreate(1); 34 + refresh(); 35 + } catch (e) { 36 + setError((e as Error).message); 37 + } finally { 38 + setBusy(false); 39 + } 40 + }; 41 + 42 + const disable = async (code: string) => { 43 + setBusy(true); 44 + setError(null); 45 + try { 46 + await api.inviteDisable(code); 47 + refresh(); 48 + } catch (e) { 49 + setError((e as Error).message); 50 + } finally { 51 + setBusy(false); 52 + } 53 + }; 54 + 55 + const copy = (code: string, e: React.MouseEvent) => { 56 + const at = { clientX: e.clientX, clientY: e.clientY }; 57 + navigator.clipboard 58 + .writeText(code) 59 + .then(() => showToast("invite code copied", at)) 60 + .catch(() => showToast("copy failed", at)); 61 + }; 62 + 63 + const isDead = (inv: Invite) => inv.disabled || inv.uses.length >= inv.available; 64 + const shown = hideUsed ? invites.filter((inv) => !isDead(inv)) : invites; 65 + 66 + return ( 67 + <div className="panel"> 68 + <h2 style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}> 69 + invite codes ({shown.length}) 70 + <label 71 + style={{ 72 + display: "flex", 73 + alignItems: "center", 74 + gap: 6, 75 + textTransform: "none", 76 + letterSpacing: "normal", 77 + fontWeight: 400, 78 + color: "var(--text)", 79 + }} 80 + > 81 + <input 82 + type="checkbox" 83 + checked={hideUsed} 84 + onChange={(e) => setHideUsed(e.target.checked)} 85 + /> 86 + hide used 87 + </label> 88 + </h2> 89 + {error && <div className="error-text">{error}</div>} 90 + {shown.length === 0 && !error && ( 91 + <p className="mono-dim empty-state"> 92 + {hideUsed ? "no unused invite codes" : "no invite codes yet"} 93 + </p> 94 + )} 95 + {shown.length > 0 && ( 96 + <table className="invites-table"> 97 + <thead> 98 + <tr> 99 + <th>code</th> 100 + <th>uses</th> 101 + <th>created</th> 102 + <th></th> 103 + </tr> 104 + </thead> 105 + <tbody> 106 + {shown.map((inv) => { 107 + const spent = inv.uses.length >= inv.available; 108 + return ( 109 + <tr key={inv.code}> 110 + <td className={inv.disabled || spent ? "mono-dim" : ""}>{inv.code}</td> 111 + <td className="mono-dim"> 112 + {inv.uses.length}/{inv.available} 113 + </td> 114 + <td className="mono-dim"> 115 + {new Date(inv.createdAt).toLocaleDateString()} 116 + {inv.disabled ? " (disabled)" : ""} 117 + </td> 118 + <td> 119 + <div className="invite-actions"> 120 + <button disabled={busy} onClick={(e) => copy(inv.code, e)}> 121 + copy 122 + </button> 123 + {!inv.disabled && !spent && ( 124 + <button disabled={busy} onClick={() => disable(inv.code)}> 125 + disable 126 + </button> 127 + )} 128 + </div> 129 + </td> 130 + </tr> 131 + ); 132 + })} 133 + </tbody> 134 + </table> 135 + )} 136 + <button className="wide-mobile" disabled={busy} onClick={create} style={{ marginTop: 12 }}> 137 + create invite 138 + </button> 139 + {toast} 140 + </div> 141 + ); 142 + }
+80
web/src/Login.tsx
··· 1 + import { useState } from "react"; 2 + import { startAuthentication } from "@simplewebauthn/browser"; 3 + import { api } from "./api.js"; 4 + 5 + export function Login({ 6 + pdsHostname, 7 + passwordLogin, 8 + onLoggedIn, 9 + }: { 10 + pdsHostname: string; 11 + passwordLogin: boolean; 12 + onLoggedIn: () => void; 13 + }) { 14 + const [password, setPassword] = useState(""); 15 + const [showPassword, setShowPassword] = useState(false); 16 + const [error, setError] = useState<string | null>(null); 17 + const [busy, setBusy] = useState(false); 18 + 19 + const submit = async (e: React.FormEvent) => { 20 + e.preventDefault(); 21 + setBusy(true); 22 + setError(null); 23 + try { 24 + await api.login(password); 25 + onLoggedIn(); 26 + } catch (e) { 27 + setError((e as Error).message); 28 + } finally { 29 + setBusy(false); 30 + } 31 + }; 32 + 33 + const passkeySignIn = async () => { 34 + setBusy(true); 35 + setError(null); 36 + try { 37 + const options = await api.passkeyLoginOptions(); 38 + const response = await startAuthentication({ optionsJSON: options }); 39 + await api.passkeyLogin(response); 40 + onLoggedIn(); 41 + } catch (e) { 42 + const err = e as Error; 43 + // user dismissing the browser's passkey prompt isn't an error worth showing 44 + if (err.name !== "NotAllowedError") setError(err.message); 45 + } finally { 46 + setBusy(false); 47 + } 48 + }; 49 + 50 + return ( 51 + <div className="login-screen"> 52 + <form className="login-form" onSubmit={submit}> 53 + <span className="mono-dim">{pdsHostname ? `${pdsHostname} — operator` : "pds operator"}</span> 54 + <button type="button" disabled={busy} onClick={passkeySignIn} autoFocus> 55 + sign in with passkey 56 + </button> 57 + {passwordLogin && 58 + (showPassword ? ( 59 + <> 60 + <input 61 + type="password" 62 + placeholder="operator password" 63 + value={password} 64 + onChange={(e) => setPassword(e.target.value)} 65 + autoFocus 66 + /> 67 + <button type="submit" disabled={busy}> 68 + {busy ? "checking…" : "sign in"} 69 + </button> 70 + </> 71 + ) : ( 72 + <button type="button" disabled={busy} onClick={() => setShowPassword(true)}> 73 + use password 74 + </button> 75 + ))} 76 + {error && <span className="error-text">{error}</span>} 77 + </form> 78 + </div> 79 + ); 80 + }
+84
web/src/PasskeysPanel.tsx
··· 1 + import { useCallback, useEffect, useState } from "react"; 2 + import { startRegistration } from "@simplewebauthn/browser"; 3 + import { api } from "./api.js"; 4 + 5 + interface Passkey { 6 + id: string; 7 + name: string; 8 + createdAt: number; 9 + } 10 + 11 + export function PasskeysPanel() { 12 + const [passkeys, setPasskeys] = useState<Passkey[]>([]); 13 + const [name, setName] = useState(""); 14 + const [busy, setBusy] = useState(false); 15 + const [error, setError] = useState<string | null>(null); 16 + 17 + const refresh = useCallback(() => { 18 + api 19 + .passkeys() 20 + .then((r) => setPasskeys(r.passkeys)) 21 + .catch((e) => setError(e.message)); 22 + }, []); 23 + 24 + useEffect(refresh, [refresh]); 25 + 26 + const add = async () => { 27 + setBusy(true); 28 + setError(null); 29 + try { 30 + const options = await api.passkeyRegisterOptions(); 31 + const response = await startRegistration({ optionsJSON: options }); 32 + await api.passkeyRegister(name.trim(), response); 33 + setName(""); 34 + refresh(); 35 + } catch (e) { 36 + const err = e as Error; 37 + if (err.name !== "NotAllowedError") setError(err.message); 38 + } finally { 39 + setBusy(false); 40 + } 41 + }; 42 + 43 + const remove = async (id: string) => { 44 + setBusy(true); 45 + setError(null); 46 + try { 47 + await api.passkeyDelete(id); 48 + refresh(); 49 + } catch (e) { 50 + setError((e as Error).message); 51 + } finally { 52 + setBusy(false); 53 + } 54 + }; 55 + 56 + return ( 57 + <div className="panel"> 58 + <h2>passkeys</h2> 59 + {error && <div className="error-text">{error}</div>} 60 + {passkeys.length === 0 && ( 61 + <p className="mono-dim empty-state">no passkeys yet. password sign-in only.</p> 62 + )} 63 + {passkeys.map((p) => ( 64 + <div key={p.id} className="passkey-row"> 65 + <span>{p.name}</span> 66 + <span className="mono-dim">{new Date(p.createdAt).toLocaleDateString()}</span> 67 + <button disabled={busy} onClick={() => remove(p.id)}> 68 + remove 69 + </button> 70 + </div> 71 + ))} 72 + <div className="passkey-add"> 73 + <input 74 + placeholder="name (e.g. macbook touch id)" 75 + value={name} 76 + onChange={(e) => setName(e.target.value)} 77 + /> 78 + <button disabled={busy} onClick={add}> 79 + add passkey 80 + </button> 81 + </div> 82 + </div> 83 + ); 84 + }
+48
web/src/RequestCrawl.tsx
··· 1 + import { useState } from "react"; 2 + import { api } from "./api.js"; 3 + 4 + type CrawlState = 5 + | { state: "idle" } 6 + | { state: "requesting" } 7 + | { state: "done" } 8 + | { state: "failed"; message: string }; 9 + 10 + export function RequestCrawl({ 11 + className = "", 12 + onRequested, 13 + }: { 14 + className?: string; 15 + onRequested?: () => void; 16 + }) { 17 + const [crawl, setCrawl] = useState<CrawlState>({ state: "idle" }); 18 + 19 + const handleRequestCrawl = async () => { 20 + setCrawl({ state: "requesting" }); 21 + try { 22 + await api.requestCrawl(); 23 + setCrawl({ state: "done" }); 24 + onRequested?.(); 25 + setTimeout(() => setCrawl({ state: "idle" }), 8000); 26 + } catch (e) { 27 + setCrawl({ state: "failed", message: (e as Error).message }); 28 + } 29 + }; 30 + 31 + return ( 32 + <div className={`crawl-row ${className}`}> 33 + <button onClick={handleRequestCrawl} disabled={crawl.state === "requesting"}> 34 + {crawl.state === "requesting" ? "requesting…" : "request crawl"} 35 + </button> 36 + {crawl.state === "done" && ( 37 + <span className="value" style={{ fontSize: 12 }}> 38 + crawl requested. the relay status will refresh shortly. 39 + </span> 40 + )} 41 + {crawl.state === "failed" && ( 42 + <span className="error-text" style={{ fontSize: 12 }}> 43 + crawl request failed: {crawl.message} 44 + </span> 45 + )} 46 + </div> 47 + ); 48 + }
+93
web/src/StatusPanel.tsx
··· 1 + import { useEffect, useState, useCallback } from "react"; 2 + import { api, type CursorStatus } from "./api.js"; 3 + import { RequestCrawl } from "./RequestCrawl.js"; 4 + 5 + export function StatusPanel({ 6 + onLastSync, 7 + }: { 8 + onLastSync?: (iso: string | null) => void; 9 + }) { 10 + const [status, setStatus] = useState<CursorStatus | null>(null); 11 + const [error, setError] = useState<string | null>(null); 12 + 13 + const refresh = useCallback(() => { 14 + api 15 + .cursorStatus() 16 + .then((s) => { 17 + setStatus(s); 18 + onLastSync?.(s.lastFullSync); 19 + }) 20 + .catch((e) => setError(e.message)); 21 + }, [onLastSync]); 22 + 23 + useEffect(() => { 24 + refresh(); 25 + const id = setInterval(refresh, 15_000); 26 + return () => clearInterval(id); 27 + }, [refresh]); 28 + 29 + const gap = status?.gap; 30 + const gapClass = gap == null ? "warn" : gap < 500 ? "ok" : gap < 5000 ? "warn" : "error"; 31 + const statusClass = 32 + status == null 33 + ? "" 34 + : status.relayStatus === "active" || status.relayStatus === "idle" 35 + ? "ok" 36 + : status.relayStatus === "throttled" 37 + ? "warn" 38 + : "error"; 39 + 40 + // pulsing skeleton while the first fetch is in flight; static dash once it has failed 41 + const placeholder = error ? ( 42 + <span className="value mono-dim">—</span> 43 + ) : ( 44 + <span className="value skeleton" aria-hidden="true" /> 45 + ); 46 + 47 + return ( 48 + <div className="status-panel"> 49 + {error && ( 50 + <div className="error-text" style={{ marginBottom: 12 }}> 51 + {error} 52 + </div> 53 + )} 54 + <div className="status-grid"> 55 + <div className="stat"> 56 + <span className="label">relay seq</span> 57 + {status ? ( 58 + <span className="value">{status.relaySeq.toLocaleString()}</span> 59 + ) : ( 60 + placeholder 61 + )} 62 + </div> 63 + <div className="stat"> 64 + <span className="label">pds head</span> 65 + {status ? ( 66 + <span className="value">{status.pdsHeadSeqApprox?.toLocaleString() ?? "—"}</span> 67 + ) : ( 68 + placeholder 69 + )} 70 + </div> 71 + <div className="stat"> 72 + <span className="label">gap</span> 73 + {status ? ( 74 + <span className={`value ${gapClass}`}> 75 + {gap != null ? gap.toLocaleString() : "unknown"} 76 + </span> 77 + ) : ( 78 + placeholder 79 + )} 80 + </div> 81 + <div className="stat"> 82 + <span className="label">relay status</span> 83 + {status ? <span className={`value ${statusClass}`}>{status.relayStatus}</span> : placeholder} 84 + </div> 85 + <div className="stat"> 86 + <span className="label">accounts</span> 87 + {status ? <span className="value">{status.accountCount.toLocaleString()}</span> : placeholder} 88 + </div> 89 + </div> 90 + <RequestCrawl className="panel-crawl" onRequested={() => setTimeout(refresh, 2000)} /> 91 + </div> 92 + ); 93 + }
+19
web/src/SyncFooter.tsx
··· 1 + export function SyncFooter({ lastFullSync }: { lastFullSync: string | null }) { 2 + if (!lastFullSync) return null; 3 + const mins = (Date.now() - new Date(lastFullSync).getTime()) / 60_000; 4 + return ( 5 + <footer className="sync-footer"> 6 + {mins > 40 ? "⚠ " : ""} 7 + db synced {formatAgo(mins)} 8 + </footer> 9 + ); 10 + } 11 + 12 + function formatAgo(mins: number): string { 13 + const m = Math.floor(mins); 14 + if (m < 1) return "just now"; 15 + if (m < 60) return `${m}m ago`; 16 + const hours = Math.floor(m / 60); 17 + if (hours < 24) return `${hours}h ago`; 18 + return `${Math.floor(hours / 24)}d ago`; 19 + }
+93
web/src/api.ts
··· 1 + async function req<T>(path: string, opts: RequestInit = {}): Promise<T> { 2 + const res = await fetch(path, { 3 + ...opts, 4 + headers: { 5 + ...(opts.body != null ? { "Content-Type": "application/json" } : {}), 6 + ...opts.headers, 7 + }, 8 + credentials: "include", 9 + }); 10 + if (!res.ok) { 11 + const body = await res.json().catch(() => ({})); 12 + throw new Error(body.error ?? `request failed: ${res.status}`); 13 + } 14 + return res.json(); 15 + } 16 + 17 + export interface Account { 18 + did: string; 19 + handle: string; 20 + email?: string; 21 + indexedAt: string; 22 + status?: "active" | "takendown" | "deactivated"; 23 + avatar?: string; 24 + labels?: string[]; 25 + } 26 + 27 + export interface CursorStatus { 28 + relaySeq: number; 29 + relayStatus: string; 30 + accountCount: number; 31 + pdsHeadSeqApprox: number | null; 32 + gap: number | null; 33 + lastFullSync: string | null; 34 + } 35 + 36 + export const api = { 37 + session: () => 38 + req<{ 39 + authenticated: boolean; 40 + appviewUrl: string; 41 + pdsHostname: string; 42 + passwordLogin: boolean; 43 + }>("/api/session"), 44 + login: (password: string) => req<{ ok: true }>("/api/login", { method: "POST", body: JSON.stringify({ password }) }), 45 + logout: () => req<{ ok: true }>("/api/logout", { method: "POST" }), 46 + accounts: (opts: { q?: string; offset?: number; limit?: number; hideTakendown?: boolean } = {}) => { 47 + const params = new URLSearchParams(); 48 + if (opts.q) params.set("q", opts.q); 49 + if (opts.offset) params.set("offset", String(opts.offset)); 50 + if (opts.limit) params.set("limit", String(opts.limit)); 51 + if (opts.hideTakendown) params.set("hideTakendown", "1"); 52 + const qs = params.toString(); 53 + return req<{ accounts: Account[]; total: number; flaggedTotal: number }>( 54 + `/api/accounts${qs ? `?${qs}` : ""}`, 55 + ); 56 + }, 57 + takedown: (did: string) => req<{ ok: true }>(`/api/accounts/${encodeURIComponent(did)}/takedown`, { method: "POST" }), 58 + enable: (did: string) => req<{ ok: true }>(`/api/accounts/${encodeURIComponent(did)}/enable`, { method: "POST" }), 59 + resetPassword: (did: string) => req<{ ok: true }>(`/api/accounts/${encodeURIComponent(did)}/reset-password`, { method: "POST" }), 60 + cursorStatus: () => req<CursorStatus>("/api/status/cursor"), 61 + requestCrawl: () => req<{ ok: true }>("/api/status/request-crawl", { method: "POST" }), 62 + passkeyRegisterOptions: (enrollToken?: string) => 63 + req<any>("/api/passkeys/options", { method: "POST", body: JSON.stringify({ enrollToken }) }), 64 + passkeyRegister: (name: string, response: unknown, enrollToken?: string) => 65 + req<{ ok: true }>("/api/passkeys", { 66 + method: "POST", 67 + body: JSON.stringify({ name, response, enrollToken }), 68 + }), 69 + passkeys: () => req<{ passkeys: { id: string; name: string; createdAt: number }[] }>("/api/passkeys"), 70 + passkeyDelete: (id: string) => 71 + req<{ ok: true }>(`/api/passkeys/${encodeURIComponent(id)}`, { method: "DELETE" }), 72 + invites: () => 73 + req<{ 74 + codes: { 75 + code: string; 76 + available: number; 77 + disabled: boolean; 78 + createdAt: string; 79 + uses: { usedBy: string; usedAt: string }[]; 80 + }[]; 81 + }>("/api/invites"), 82 + inviteCreate: (useCount = 1) => 83 + req<{ code: string }>("/api/invites", { method: "POST", body: JSON.stringify({ useCount }) }), 84 + inviteDisable: (code: string) => 85 + req<{ ok: true }>("/api/invites/disable", { method: "POST", body: JSON.stringify({ code }) }), 86 + audit: () => 87 + req<{ entries: { at: string; operator: string; action: string; target?: string }[] }>( 88 + "/api/audit?limit=500", 89 + ), 90 + passkeyLoginOptions: () => req<any>("/api/login/passkey/options", { method: "POST" }), 91 + passkeyLogin: (response: unknown) => 92 + req<{ ok: true }>("/api/login/passkey", { method: "POST", body: JSON.stringify(response) }), 93 + };
+9
web/src/main.tsx
··· 1 + import { StrictMode } from "react"; 2 + import { createRoot } from "react-dom/client"; 3 + import { App } from "./App.js"; 4 + 5 + createRoot(document.getElementById("root")!).render( 6 + <StrictMode> 7 + <App /> 8 + </StrictMode>, 9 + );
+715
web/src/style.css
··· 1 + :root { 2 + --bg: #fafafa; 3 + --surface: #ffffff; 4 + --border: #d8d8d8; 5 + --text: #161616; 6 + --text-dim: #6b6b6b; 7 + --ok: #1a7a4c; 8 + --warn: #a5690a; 9 + --error: #b0281f; 10 + --accent: #161616; 11 + --radius: 2px; 12 + --shadow: rgba(0, 0, 0, 0.18); 13 + } 14 + 15 + @media (prefers-color-scheme: dark) { 16 + :root { 17 + --bg: #101112; 18 + --surface: #16181a; 19 + --border: #2c2f33; 20 + --text: #e7e7e7; 21 + --text-dim: #8a8f94; 22 + --ok: #4fbf85; 23 + --warn: #d9a441; 24 + --error: #e2685c; 25 + --accent: #e7e7e7; 26 + --shadow: rgba(0, 0, 0, 0.5); 27 + } 28 + } 29 + 30 + * { 31 + box-sizing: border-box; 32 + } 33 + 34 + html, 35 + body, 36 + #root { 37 + height: 100%; 38 + } 39 + 40 + body { 41 + margin: 0; 42 + background: var(--bg); 43 + color: var(--text); 44 + font-family: ui-monospace, "SF Mono", "Cascadia Code", "Consolas", "Menlo", monospace; 45 + font-size: 14px; 46 + line-height: 1.5; 47 + } 48 + 49 + a { 50 + color: var(--text); 51 + } 52 + 53 + button, 54 + input, 55 + select { 56 + font-family: inherit; 57 + font-size: inherit; 58 + color: var(--text); 59 + background: var(--surface); 60 + border: 1px solid var(--border); 61 + border-radius: var(--radius); 62 + padding: 6px 10px; 63 + } 64 + 65 + input { 66 + font-size: 16px; 67 + } 68 + 69 + button { 70 + cursor: pointer; 71 + transition: background-color 120ms ease, border-color 120ms ease; 72 + } 73 + 74 + button:hover:not(:disabled) { 75 + border-color: var(--text); 76 + } 77 + 78 + button:focus-visible, 79 + input:focus-visible { 80 + outline: 2px solid var(--accent); 81 + outline-offset: 1px; 82 + } 83 + 84 + button:disabled { 85 + opacity: 0.5; 86 + cursor: not-allowed; 87 + } 88 + 89 + button.danger:hover:not(:disabled) { 90 + border-color: var(--error); 91 + color: var(--error); 92 + } 93 + 94 + .app { 95 + max-width: 1000px; 96 + margin: 0 auto; 97 + padding: 24px 16px 64px; 98 + } 99 + 100 + .header { 101 + display: flex; 102 + align-items: baseline; 103 + justify-content: space-between; 104 + padding-bottom: 12px; 105 + margin-bottom: 24px; 106 + } 107 + 108 + .header h1 { 109 + font-size: 14px; 110 + font-weight: 600; 111 + letter-spacing: 0.02em; 112 + margin: 0; 113 + text-transform: lowercase; 114 + } 115 + 116 + .header .sub { 117 + color: var(--text-dim); 118 + } 119 + 120 + .panel { 121 + border: 1px solid var(--border); 122 + border-radius: var(--radius); 123 + background: var(--surface); 124 + padding: 16px; 125 + margin-bottom: 16px; 126 + } 127 + 128 + /* relay stats sit directly on the page, no panel chrome; side padding lines the 129 + stats up with the panels' inner content (16px panel + 10px cell padding) */ 130 + .status-panel { 131 + padding: 8px 16px; 132 + margin-bottom: 24px; 133 + } 134 + 135 + .header-actions { 136 + display: flex; 137 + align-items: center; 138 + gap: 10px; 139 + } 140 + 141 + /* request crawl lives in the nav on desktop, above the stats on mobile */ 142 + .crawl-row.nav-crawl { 143 + margin-top: 0; 144 + flex-direction: row-reverse; /* feedback message to the button's left */ 145 + white-space: nowrap; 146 + } 147 + 148 + .crawl-row.panel-crawl { 149 + display: none; 150 + margin-top: 16px; 151 + } 152 + 153 + @media (max-width: 640px) { 154 + .crawl-row.nav-crawl { 155 + display: none; 156 + } 157 + 158 + .crawl-row.panel-crawl { 159 + display: flex; /* full-width button; message wraps below */ 160 + } 161 + } 162 + 163 + .panel h2 { 164 + font-size: 12px; 165 + text-transform: uppercase; 166 + letter-spacing: 0.08em; 167 + color: var(--text-dim); 168 + margin: 0 0 12px; 169 + font-weight: 600; 170 + } 171 + 172 + .status-grid { 173 + display: grid; 174 + grid-template-columns: repeat(5, minmax(0, 1fr)); 175 + gap: 16px; 176 + padding-left: 10px; 177 + } 178 + 179 + .skeleton { 180 + display: inline-block; 181 + width: 4em; 182 + height: 1.2em; 183 + margin: 0.15em 0; /* fill out the line box so nothing shifts when real values land */ 184 + border-radius: var(--radius); 185 + background: color-mix(in srgb, var(--text) 8%, transparent); 186 + animation: skeleton-pulse 1.2s ease-in-out infinite; 187 + } 188 + 189 + @keyframes skeleton-pulse { 190 + 0%, 191 + 100% { 192 + opacity: 0.5; 193 + } 194 + 50% { 195 + opacity: 1; 196 + } 197 + } 198 + 199 + .sync-footer { 200 + margin-top: 24px; 201 + text-align: center; 202 + color: var(--text-dim); 203 + font-size: 12px; 204 + } 205 + 206 + .crawl-row { 207 + display: flex; 208 + flex-wrap: wrap; 209 + align-items: center; 210 + gap: 10px; 211 + margin-top: 12px; 212 + } 213 + 214 + .stat { 215 + display: flex; 216 + flex-direction: column; 217 + gap: 4px; 218 + } 219 + 220 + .stat .label { 221 + color: var(--text-dim); 222 + font-size: 12px; 223 + text-transform: uppercase; 224 + letter-spacing: 0.06em; 225 + } 226 + 227 + .stat .value { 228 + font-size: 20px; 229 + font-variant-numeric: tabular-nums; 230 + } 231 + 232 + .stat .value.ok { 233 + color: var(--ok); 234 + } 235 + 236 + .stat .value.warn { 237 + color: var(--warn); 238 + } 239 + 240 + .stat .value.error { 241 + color: var(--error); 242 + } 243 + 244 + table { 245 + width: 100%; 246 + border-collapse: collapse; 247 + } 248 + 249 + th, 250 + td { 251 + text-align: left; 252 + padding: 8px 10px; 253 + font-variant-numeric: tabular-nums; 254 + } 255 + 256 + th { 257 + color: var(--text-dim); 258 + font-weight: 500; 259 + font-size: 12px; 260 + text-transform: uppercase; 261 + letter-spacing: 0.06em; 262 + } 263 + 264 + tr:hover td { 265 + background: color-mix(in srgb, var(--text) 4%, transparent); 266 + } 267 + 268 + .login-screen { 269 + height: 100%; 270 + display: flex; 271 + align-items: center; 272 + justify-content: center; 273 + } 274 + 275 + .login-form { 276 + display: flex; 277 + flex-direction: column; 278 + gap: 10px; 279 + width: 240px; 280 + } 281 + 282 + .error-text { 283 + color: var(--error); 284 + font-size: 12px; 285 + } 286 + 287 + .mono-dim { 288 + color: var(--text-dim); 289 + } 290 + 291 + /* line empty-state copy up with table cell text (th/td have 10px side padding) */ 292 + .empty-state { 293 + padding-left: 10px; 294 + } 295 + 296 + @media (max-width: 640px) { 297 + .empty-state { 298 + padding-left: 0; /* mobile cards are flush left */ 299 + } 300 + } 301 + 302 + .handle-cell { 303 + display: flex; 304 + align-items: center; 305 + gap: 8px; 306 + } 307 + 308 + .handle-link { 309 + text-decoration: none; 310 + } 311 + 312 + .handle-link:hover { 313 + text-decoration: underline; 314 + } 315 + 316 + .labels { 317 + color: var(--error); 318 + font-size: 12px; 319 + margin-left: 28px; /* align under the handle text, past the avatar */ 320 + overflow-wrap: anywhere; 321 + } 322 + 323 + .avatar { 324 + width: 20px; 325 + height: 20px; 326 + border-radius: 50%; 327 + object-fit: cover; 328 + background: var(--border); 329 + flex-shrink: 0; 330 + } 331 + 332 + td.status-cell { 333 + max-width: 110px; 334 + white-space: nowrap; 335 + overflow: hidden; 336 + text-overflow: ellipsis; 337 + } 338 + 339 + td.status-cell.ok { 340 + color: var(--ok); 341 + } 342 + 343 + td.status-cell.warn { 344 + color: var(--warn); 345 + } 346 + 347 + td.status-cell.error { 348 + color: var(--error); 349 + } 350 + 351 + .menu-wrap { 352 + position: relative; 353 + display: inline-block; 354 + } 355 + 356 + button.kebab { 357 + padding: 2px 8px; 358 + line-height: 1.4; 359 + } 360 + 361 + .menu { 362 + position: absolute; 363 + right: 0; 364 + top: calc(100% + 4px); 365 + min-width: 160px; 366 + z-index: 20; 367 + display: flex; 368 + flex-direction: column; 369 + padding: 4px; 370 + background: var(--surface); 371 + border: 1px solid var(--border); 372 + border-radius: var(--radius); 373 + box-shadow: 0 4px 14px var(--shadow); 374 + } 375 + 376 + .menu button { 377 + border: none; 378 + background: none; 379 + text-align: left; 380 + padding: 6px 10px; 381 + white-space: nowrap; 382 + } 383 + 384 + .menu button:hover:not(:disabled) { 385 + background: color-mix(in srgb, var(--text) 6%, transparent); 386 + } 387 + 388 + .menu button.danger { 389 + color: var(--error); 390 + } 391 + 392 + .menu-sep { 393 + height: 1px; 394 + background: var(--border); 395 + margin: 4px 2px; 396 + } 397 + 398 + .accounts-footer { 399 + display: flex; 400 + justify-content: space-between; 401 + align-items: center; 402 + margin-top: 12px; 403 + } 404 + 405 + .page-size { 406 + display: flex; 407 + align-items: center; 408 + gap: 8px; 409 + color: var(--text-dim); 410 + } 411 + 412 + .toast { 413 + position: fixed; 414 + transform: translateX(-50%); 415 + padding: 6px 14px; 416 + background: var(--surface); 417 + border: 1px solid var(--border); 418 + border-radius: var(--radius); 419 + box-shadow: 0 4px 14px var(--shadow); 420 + z-index: 30; 421 + max-width: calc(100vw - 16px); 422 + white-space: nowrap; 423 + overflow: hidden; 424 + text-overflow: ellipsis; 425 + animation: toast-in 150ms ease-out; 426 + transition: transform 450ms ease, opacity 450ms ease, top 300ms ease; 427 + pointer-events: none; 428 + } 429 + 430 + .toast.leaving { 431 + transform: translateX(-50%) translateY(-16px); 432 + opacity: 0; 433 + } 434 + 435 + @keyframes toast-in { 436 + from { 437 + opacity: 0; 438 + transform: translateX(-50%) translateY(4px); 439 + } 440 + to { 441 + opacity: 1; 442 + transform: translateX(-50%) translateY(0); 443 + } 444 + } 445 + 446 + .invite-actions { 447 + display: flex; 448 + gap: 6px; 449 + justify-content: flex-end; 450 + } 451 + 452 + .passkey-row { 453 + display: flex; 454 + align-items: center; 455 + gap: 16px; 456 + padding: 6px 10px 6px 10px; 457 + } 458 + 459 + @media (max-width: 640px) { 460 + .passkey-row { 461 + padding-left: 0; 462 + padding-right: 0; 463 + } 464 + } 465 + 466 + .passkey-row span:first-child { 467 + flex: 1; 468 + } 469 + 470 + .passkey-add { 471 + display: flex; 472 + gap: 10px; 473 + margin-top: 12px; 474 + } 475 + 476 + .passkey-add input { 477 + flex: 1; 478 + } 479 + 480 + .did-cell .email { 481 + overflow-wrap: anywhere; 482 + color: var(--text); 483 + } 484 + 485 + .accounts-toolbar { 486 + display: flex; 487 + align-items: center; 488 + gap: 12px; 489 + margin-bottom: 12px; 490 + } 491 + 492 + .search-wrap { 493 + position: relative; 494 + display: flex; 495 + flex: 1; 496 + } 497 + 498 + .search-wrap input { 499 + width: 100%; 500 + } 501 + 502 + /* custom clear button replaces the browser's built-in one */ 503 + .search-wrap input::-webkit-search-cancel-button { 504 + -webkit-appearance: none; 505 + appearance: none; 506 + } 507 + 508 + .clear-search { 509 + position: absolute; 510 + right: 4px; 511 + top: 50%; 512 + transform: translateY(-50%); 513 + border: none; 514 + background: none; 515 + padding: 2px 6px; 516 + color: var(--text-dim); 517 + } 518 + 519 + .clear-search:hover:not(:disabled) { 520 + border: none; 521 + color: var(--text); 522 + } 523 + 524 + .accounts-toolbar label { 525 + display: flex; 526 + align-items: center; 527 + gap: 6px; 528 + white-space: nowrap; 529 + } 530 + 531 + /* stack account rows into cards: handle / did / status+indexed+menu */ 532 + @media (max-width: 640px) { 533 + /* key-value list: label left, value right */ 534 + .status-grid { 535 + display: flex; 536 + flex-direction: column; 537 + gap: 0; 538 + padding-left: 0; 539 + } 540 + 541 + .status-grid .stat { 542 + flex-direction: row; 543 + align-items: center; 544 + padding: 4px 0; 545 + } 546 + 547 + /* leader line from label to value */ 548 + .status-grid .stat::after { 549 + content: ""; 550 + flex: 1; 551 + height: 1px; 552 + background: var(--border); 553 + margin: 0 8px; 554 + order: 1; 555 + } 556 + 557 + .status-grid .stat .value { 558 + order: 0; 559 + } 560 + 561 + .status-grid .stat .label { 562 + order: 2; 563 + } 564 + 565 + .stat .value { 566 + font-size: 14px; 567 + } 568 + 569 + .crawl-row button { 570 + width: 100%; 571 + } 572 + 573 + .accounts-toolbar { 574 + flex-direction: column; 575 + align-items: stretch; 576 + gap: 8px; 577 + } 578 + 579 + .accounts-footer { 580 + flex-direction: column; 581 + align-items: stretch; 582 + gap: 10px; 583 + } 584 + 585 + .accounts-footer button { 586 + width: 100%; 587 + } 588 + 589 + .accounts-footer .page-size { 590 + justify-content: flex-end; 591 + } 592 + 593 + button.wide-mobile { 594 + width: 100%; 595 + } 596 + 597 + thead { 598 + display: none; 599 + } 600 + 601 + table, 602 + tbody { 603 + display: block; 604 + } 605 + 606 + tbody tr { 607 + display: grid; 608 + column-gap: 12px; 609 + row-gap: 2px; 610 + align-items: center; 611 + padding: 10px 0; 612 + } 613 + 614 + tbody td { 615 + display: block; 616 + border-left: none; 617 + padding: 0; 618 + } 619 + 620 + tbody tr:hover td { 621 + background: none; 622 + } 623 + 624 + /* accounts: handle / did+email / status+indexed+menu */ 625 + .accounts-table tbody tr { 626 + grid-template-columns: max-content 1fr max-content; 627 + grid-template-areas: 628 + "handle handle handle" 629 + "did did did" 630 + "status indexed menu"; 631 + } 632 + 633 + .accounts-table tbody td:nth-child(1) { 634 + grid-area: handle; 635 + } 636 + 637 + /* indent under the handle text: avatar 20px + 8px gap */ 638 + .accounts-table tbody td:nth-child(2) { 639 + grid-area: did; 640 + overflow-wrap: anywhere; 641 + font-size: 12px; 642 + margin-left: 28px; 643 + } 644 + 645 + .accounts-table tbody td:nth-child(3) { 646 + grid-area: status; 647 + max-width: none; 648 + margin-left: 28px; 649 + } 650 + 651 + .accounts-table tbody td:nth-child(4) { 652 + grid-area: indexed; 653 + } 654 + 655 + .accounts-table tbody td:nth-child(5) { 656 + grid-area: menu; 657 + justify-self: end; 658 + } 659 + 660 + /* invites: code / uses+created+actions — actions at the right edge like the 661 + accounts context menu */ 662 + .invites-table tbody tr { 663 + grid-template-columns: max-content 1fr max-content; 664 + grid-template-areas: 665 + "code code code" 666 + "uses created actions"; 667 + } 668 + 669 + .invites-table tbody td:nth-child(1) { 670 + grid-area: code; 671 + overflow-wrap: anywhere; 672 + } 673 + 674 + .invites-table tbody td:nth-child(2) { 675 + grid-area: uses; 676 + } 677 + 678 + .invites-table tbody td:nth-child(3) { 679 + grid-area: created; 680 + } 681 + 682 + .invites-table tbody td:nth-child(4) { 683 + grid-area: actions; 684 + justify-self: end; 685 + } 686 + 687 + /* audit log: action+when / target / operator */ 688 + .audit-table tbody tr { 689 + grid-template-columns: 1fr max-content; 690 + grid-template-areas: 691 + "action when" 692 + "target target" 693 + "operator operator"; 694 + } 695 + 696 + .audit-table tbody td:nth-child(1) { 697 + grid-area: when; 698 + justify-self: end; 699 + } 700 + 701 + .audit-table tbody td:nth-child(2) { 702 + grid-area: action; 703 + } 704 + 705 + .audit-table tbody td:nth-child(3) { 706 + grid-area: target; 707 + overflow-wrap: anywhere; 708 + font-size: 12px; 709 + } 710 + 711 + .audit-table tbody td:nth-child(4) { 712 + grid-area: operator; 713 + font-size: 12px; 714 + } 715 + }
+68
web/src/useToast.tsx
··· 1 + import { useState, type ReactNode } from "react"; 2 + 3 + interface Toast { 4 + id: number; 5 + message: string; 6 + x: number; 7 + y: number; 8 + leaving: boolean; 9 + } 10 + 11 + let nextId = 0; 12 + 13 + const VISIBLE_MS = 1200; 14 + const LEAVE_MS = 450; 15 + const STACK_GAP = 36; 16 + const EDGE = 8; 17 + 18 + /** 19 + * Transient notices anchored to the click point, clamped to the viewport. 20 + * Each toast holds, then slides up and fades; concurrent toasts stack upward. 21 + */ 22 + export function useToast(): [ 23 + ReactNode, 24 + (message: string, at?: { clientX: number; clientY: number }) => void, 25 + ] { 26 + const [toasts, setToasts] = useState<Toast[]>([]); 27 + 28 + const show = (message: string, at?: { clientX: number; clientY: number }) => { 29 + const id = nextId++; 30 + setToasts((prev) => { 31 + // rough monospace width estimate, good enough to keep it on-screen 32 + const halfWidth = Math.min( 33 + (message.length * 8.4 + 28) / 2, 34 + (window.innerWidth - EDGE * 2) / 2, 35 + ); 36 + const baseX = at?.clientX ?? window.innerWidth / 2; 37 + const baseY = (at?.clientY ?? window.innerHeight - 20) - 44; 38 + const x = Math.min(Math.max(baseX, halfWidth + EDGE), window.innerWidth - halfWidth - EDGE); 39 + const y = Math.max(baseY, EDGE); 40 + // newest sits at the click point; everything already showing shifts up a slot 41 + const shifted = prev.map((t) => ({ ...t, y: Math.max(t.y - STACK_GAP, EDGE) })); 42 + return [...shifted, { id, message, x, y, leaving: false }]; 43 + }); 44 + window.setTimeout(() => { 45 + setToasts((prev) => prev.map((t) => (t.id === id ? { ...t, leaving: true } : t))); 46 + }, VISIBLE_MS); 47 + window.setTimeout(() => { 48 + setToasts((prev) => prev.filter((t) => t.id !== id)); 49 + }, VISIBLE_MS + LEAVE_MS); 50 + }; 51 + 52 + const node = 53 + toasts.length > 0 ? ( 54 + <> 55 + {toasts.map((t) => ( 56 + <div 57 + key={t.id} 58 + className={`toast${t.leaving ? " leaving" : ""}`} 59 + style={{ left: t.x, top: t.y }} 60 + > 61 + {t.message} 62 + </div> 63 + ))} 64 + </> 65 + ) : null; 66 + 67 + return [node, show]; 68 + }
+14
web/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "lib": ["ES2022", "DOM"], 5 + "module": "ESNext", 6 + "moduleResolution": "Bundler", 7 + "jsx": "react-jsx", 8 + "strict": true, 9 + "skipLibCheck": true, 10 + "esModuleInterop": true, 11 + "noEmit": true 12 + }, 13 + "include": ["src"] 14 + }
+11
web/vite.config.ts
··· 1 + import { defineConfig } from "vite"; 2 + import react from "@vitejs/plugin-react"; 3 + 4 + export default defineConfig({ 5 + plugins: [react()], 6 + server: { 7 + proxy: { 8 + "/api": "http://localhost:8788", 9 + }, 10 + }, 11 + });