SPB Git

spb/drive Public

SPB Drive — self-hosted personal cloud drive (files, previews, sharing) on the MacLustr cluster.

JavaScript 82.7% CSS 10.6% Nunjucks 3.6% Shell 1.8% SQL 1.3%

feat: skeleton, auth, storage core — config, sqlite schema, blob CAS, tree ops, chunked uploads, sessions, lockout, shares, search, preview backend

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed yesterday (Aug 9, 2026)

Showing 33 changed files with +11,616 and −0

added .gitignore +18 −0
@@ -0,0 +1,18 @@
1 +# ─────────────────────────────────────────────
2 +# SPB Drive — Personal Cloud Drive
3 +# ─────────────────────────────────────────────
4 +# Author : Simon-Pierre Boucher
5 +# Contact : contact@spboucher.ai
6 +# File : .gitignore
7 +# Purpose : Git ignore rules (deps, runtime data, logs, secrets)
8 +# License : MIT © Simon-Pierre Boucher
9 +# ─────────────────────────────────────────────
10 +
11 +node_modules/
12 +data/
13 +*.log
14 +.DS_Store
15 +.env
16 +coverage/
17 +dist/
18 +test/.tmp/
added CLAUDE.md +369 −0
@@ -0,0 +1,369 @@
1 +# CLAUDE.md — **SPB Drive** · Personal Cloud Drive for Simon-Pierre Boucher
2 +
3 +> **Read this entire document before writing a single line of code.**
4 +> This file is the single source of truth. Every architectural decision, naming convention, UI detail, and non-negotiable rule lives here. When in doubt, re-read this file. When this file conflicts with your instinct, this file wins.
5 +
6 +---
7 +
8 +## 0. Identity & Non-Negotiables
9 +
10 +| Key | Value |
11 +|---|---|
12 +| **Product name** | SPB Drive |
13 +| **Owner / sole user** | Simon-Pierre Boucher |
14 +| **Contact** | [contact@spboucher.ai](mailto:contact@spboucher.ai) |
15 +| **Public domain** | `https://drive.spboucher.ai` (ngrok custom domain) |
16 +| **Deployment host** | Node **m3u96b** |
17 +| **Runtime** | Node.js ≥ 20, ESM only |
18 +| **Access model** | **Private by default.** The entire drive is behind a password login. The ONLY public surfaces are explicitly created **share links**. |
19 +| **Initial password** | `sn18brady` — seeded at first boot, **stored argon2-hashed, never in code, never in the repo, never logged**. Changeable from Settings. |
20 +
21 +### 0.1 THE GOLDEN RULE — Mandatory Author Header
22 +
23 +**Every single file you generate — source, config, script, style, test, migration — MUST begin with an author header.** No exceptions. A file without this header is a bug.
24 +
25 +Canonical template (adapt comment syntax per language):
26 +
27 +```js
28 +/**
29 + * ─────────────────────────────────────────────
30 + * SPB Drive — Personal Cloud Drive
31 + * ─────────────────────────────────────────────
32 + * Author : Simon-Pierre Boucher
33 + * Contact : contact@spboucher.ai
34 + * File : <relative/path/filename.ext>
35 + * Purpose : <one-line description>
36 + * License : MIT © Simon-Pierre Boucher
37 + * ─────────────────────────────────────────────
38 + */
39 +```
40 +
41 +| Language | Syntax |
42 +|---|---|
43 +| JS / TS / CSS / SCSS | `/** ... */` block |
44 +| Python / Bash / YAML / Dockerfile / TOML | `#` lines |
45 +| HTML / Nunjucks | `<!-- ... -->` / `{# ... #}` |
46 +| SQL | `-- ` lines |
47 +
48 +Provide `scripts/inject-headers.mjs` and `scripts/check-headers.mjs`; `npm run check:headers` must fail CI if any tracked file lacks the header.
49 +
50 +### 0.2 Password / secret handling rules (absolute)
51 +
52 +- The literal string `sn18brady` must appear **nowhere** in the codebase, config files, tests, fixtures, or logs. It is provided once via the `SPBDRIVE_BOOTSTRAP_PASSWORD` env var (or interactive prompt) on first boot, hashed with **argon2id**, stored in `data/auth.json`, and the env var is then ignored forever.
53 +- Password change flow in Settings (requires current password). Also a break-glass CLI on the server: `node scripts/reset-password.mjs` (interactive, local only).
54 +- Session secret, share-link signing key: generated randomly at first boot into `data/keys.json` (chmod 600).
55 +
56 +---
57 +
58 +## 1. Product Vision
59 +
60 +SPB Drive is a **self-hosted Google Drive / Dropbox replacement** for one person. Three purposes:
61 +
62 +1. **Vault**: all of Simon-Pierre's files, organized in folders, uploadable from any browser, safe on m3u96b.
63 +2. **Universal previewer**: click any file → beautiful in-browser preview. Images, video, audio, PDF, Office docs, code, Markdown, CSV, archives, fonts — *everything imaginable* previews without downloading.
64 +3. **Sharing machine**: any file or folder → one click → clean public URL (`https://drive.spboucher.ai/s/<token>`) with optional expiry, password, and download limits. Recipients need no account.
65 +
66 +Quality bar: *"If a stranger receives a share link, the preview page should look like a polished commercial product."*
67 +
68 +---
69 +
70 +## 2. High-Level Architecture
71 +
72 +```
73 +┌──────────────────────────── node m3u96b ─────────────────────────────┐
74 +│ │
75 +│ ┌─────────────┐ ┌────────────────────────────────────────────────┐ │
76 +│ │ ngrok │ │ SPB Drive Server (Node 20) │ │
77 +│ │ tunnel │──▶│ Fastify app :7430 │ │
78 +│ │ drive. │ │ ├─ /login, /app/* Web UI (SSR + JS) │ │
79 +│ │ spboucher.ai │ │ ├─ /api/v1/* JSON API (session auth) │ │
80 +│ └─────────────┘ │ ├─ /s/:token[/...] PUBLIC share pages │ │
81 +│ │ ├─ /dl/*, /stream/* Auth'd download/stream │ │
82 +│ │ └─ /thumb/* Thumbnails │ │
83 +│ └────────────────┬───────────────────────────────┘ │
84 +│ │ │
85 +│ ┌──────────────────────────────┼──────────────────────────────┐ │
86 +│ │ │ │ │
87 +│ ┌────▼─────────────┐ ┌────────────▼───────────┐ ┌──────────────▼─┐ │
88 +│ │ /srv/drive/files │ │ /srv/drive/db │ │ /srv/drive/ │ │
89 +│ │ content store: │ │ drive.sqlite (better- │ │ cache/ │ │
90 +│ │ blobs by sha256 │ │ sqlite3, WAL): │ │ thumbnails, │ │
91 +│ │ /ab/cd/abcd... │ │ nodes, shares, tags, │ │ transcodes, │ │
92 +│ │ (dedup natural) │ │ sessions, activity, FTS│ │ office→pdf │ │
93 +│ └──────────────────┘ └────────────────────────┘ └────────────────┘ │
94 +└────────────────────────────────────────────────────────────────────────┘
95 +```
96 +
97 +**Principles:**
98 +- **Content-addressed blob store** (`/srv/drive/files/<sha256[0:2]>/<sha256[2:4]>/<sha256>`): identical files stored once (free dedup); DB `nodes` table maps the virtual folder tree onto blobs. Deleting a node only deletes the blob when its refcount hits zero.
99 +- **SQLite is the metadata brain** (`better-sqlite3`, WAL mode): tables `nodes` (id, parent_id, name, type file/folder, blob_sha, size, mime, created, modified, starred, color, trashed_at), `shares`, `tags`, `node_tags`, `sessions`, `activity`, plus an **FTS5** virtual table for search.
100 +- **Everything streams.** Uploads and downloads never buffer whole files in memory. Range requests supported everywhere (video seeking!).
101 +- **Preview/transcode work is queued** (tiny in-process job queue, concurrency 2) and cached in `/srv/drive/cache/` keyed by blob sha — thumbnails and conversions are computed once per unique file, ever.
102 +
103 +### 2.1 Repository layout of *this* project
104 +
105 +```
106 +spbdrive/
107 +├── CLAUDE.md
108 +├── README.md # with badges
109 +├── package.json
110 +├── src/
111 +│ ├── server.mjs # Fastify bootstrap
112 +│ ├── config.mjs # zod-validated env/config
113 +│ ├── db/
114 +│ │ ├── schema.sql # full schema + FTS5 + indexes
115 +│ │ └── db.mjs # migrations, prepared statements
116 +│ ├── auth/
117 +│ │ ├── password.mjs # argon2id verify/change, lockout
118 +│ │ └── session.mjs # cookie sessions (httpOnly, SameSite=Lax)
119 +│ ├── storage/
120 +│ │ ├── blobs.mjs # CAS store: put(stream)→sha, get, refcount GC
121 +│ │ ├── nodes.mjs # tree ops: mkdir, move, copy, rename, trash
122 +│ │ └── upload.mjs # chunked/resumable upload endpoint
123 +│ ├── preview/
124 +│ │ ├── router.mjs # mime → preview strategy dispatcher
125 +│ │ ├── thumbs.mjs # sharp: image thumbs; ffmpeg: video poster
126 +│ │ ├── transcode.mjs # ffmpeg audio/video web-safe transcodes
127 +│ │ ├── office.mjs # libreoffice --headless → PDF conversion
128 +│ │ ├── code.mjs # shiki highlighting + markdown-it (GFM)
129 +│ │ └── archive.mjs # zip/tar listing (and inner-file preview)
130 +│ ├── shares/
131 +│ │ └── shares.mjs # tokens, expiry, passwords, limits, zip-of-folder
132 +│ ├── search/
133 +│ │ └── search.mjs # FTS5 queries + filters
134 +│ ├── api/v1.mjs
135 +│ └── web/
136 +│ ├── routes.mjs
137 +│ ├── views/ # Nunjucks templates (login, app shell, share)
138 +│ └── assets/ # tokens.css, app.css, app.js, fonts, icons
139 +├── cli/
140 +│ ├── spbdrive.mjs # bin entry
141 +│ └── commands/
142 +├── deploy/
143 +│ ├── ngrok.yml
144 +│ ├── ecosystem.config.cjs # pm2: server + tunnel
145 +│ ├── spbdrive.service # systemd alternative
146 +│ ├── setup-m3u96b.sh # idempotent bootstrap (installs ffmpeg, libreoffice)
147 +│ └── backup.sh
148 +├── scripts/
149 +│ ├── inject-headers.mjs
150 +│ ├── check-headers.mjs
151 +│ └── reset-password.mjs
152 +└── test/ # vitest unit + e2e (upload→preview→share round-trip)
153 +```
154 +
155 +---
156 +
157 +## 3. Authentication & Sessions
158 +
159 +- **Login page** at `/login`: centered card, SPB monogram, single password field, "Remember me for 30 days" checkbox. Sharp, minimal, dark.
160 +- Verify with argon2id. **Brute-force protection**: 5 failed attempts → 15-minute lockout per IP + global exponential backoff; every failure logged to `activity`.
161 +- Session = random 256-bit id in `sessions` table; cookie `spbdrive_sid`, `httpOnly`, `Secure`, `SameSite=Lax`; 24 h TTL (30 d with remember-me), sliding renewal.
162 +- Every route except `/login`, `/s/*` (shares), `/healthz`, and static assets requires a valid session → otherwise 302 to `/login?next=...`.
163 +- Settings page: change password, view/revoke active sessions (device + last seen), toggle theme default.
164 +
165 +---
166 +
167 +## 4. File Management Core
168 +
169 +### 4.1 Uploads (must feel instant and bulletproof)
170 +
171 +- **Drag & drop anywhere** in the app (full-window drop overlay), plus an Upload button (files *and* folders — use `webkitdirectory` and DataTransferItem tree walking so entire folder structures upload with hierarchy preserved).
172 +- **Chunked + resumable**: client slices files into 8 MB chunks, `POST /api/v1/upload/init` → uploadId, `PUT /api/v1/upload/:id/chunk/:n`, `POST /api/v1/upload/:id/complete` (server assembles, sha256-streams into blob store, creates node). Interrupted uploads resume by asking the server which chunks it has.
173 +- Upload panel (bottom-right, Google-Drive-style): per-file progress bars, speed, cancel, retry, aggregate progress, minimize.
174 +- Paste-to-upload (Ctrl+V an image/screenshot into the app → lands in the current folder as `pasted-YYYYMMDD-HHmmss.png`).
175 +- No file-size limit by design; test with a multi-GB file.
176 +
177 +### 4.2 Folder tree & organization ("groupe folder etc.")
178 +
179 +- Unlimited nested folders. Left sidebar: collapsible folder tree + quick sections **Recent**, **Starred**, **Shared**, **Trash**, per-tag views, and a storage usage meter (used space, per-type breakdown donut).
180 +- **Folder colors** (8 palette choices) and **emoji/icon** per folder.
181 +- **Tags/labels**: create colored tags, assign to any file/folder, filter by tag.
182 +- **Starred/favorites** (toggle with `s`).
183 +- Operations, all with multi-select (click, shift-click ranges, ctrl-click, drag-rectangle select): **move (drag & drop onto folders or via dialog with tree picker), copy, rename (F2, inline), delete → Trash, restore, delete forever, download (multi-select → server-zips on the fly), duplicate**.
184 +- **Trash**: soft delete with original-path memory; auto-purge after 30 days (daily job); "Empty trash" with typed confirmation.
185 +- Breadcrumb path with drag-onto-crumb to move; right-click **context menu** everywhere (custom-rendered, keyboard accessible).
186 +- Conflict handling on move/upload: "Keep both (name-2)", "Replace", "Skip" — batch-applicable.
187 +
188 +### 4.3 Views
189 +
190 +- **Grid view** (thumbnail cards, 5 sizes via slider) and **List view** (name, size, type, modified, tags) — toggle persisted per folder.
191 +- Sort: name / size / modified / type, asc-desc. Folders always first.
192 +- **Keyboard-first**: arrows navigate, Enter opens, Space = quick-look preview overlay, Del = trash, Ctrl+A select all, `/` focuses search, `?` shows a shortcuts cheat-sheet modal.
193 +
194 +---
195 +
196 +## 5. Universal Preview Engine — "all types imaginable" (Critical)
197 +
198 +Clicking a file opens the **Preview overlay**: full-screen modal, dark scrim, filename + size header, actions (Download · Share · Star · Info · Delete), ←/→ arrows to flip through siblings, Esc closes. Every strategy below is dispatched by `preview/router.mjs` from MIME + extension:
199 +
200 +| Category | Formats | Preview behavior |
201 +|---|---|---|
202 +| **Images** | jpg, png, gif, webp, avif, svg, bmp, ico, heic* | Zoom (wheel/pinch), pan, rotate, 1:1 toggle, EXIF panel (dimensions, camera, date, GPS→"open map" link). HEIC converted to jpg via sharp if supported, else download card. Animated gif/webp play. SVG sandboxed (served with strict CSP, no scripts). |
203 +| **Video** | mp4, webm, mov, mkv, avi | Native `<video>` with **Range streaming** (instant seek). Poster frame + duration badge on thumbnails (ffmpeg). Non-web-safe codecs (mkv/avi/hevc) → background ffmpeg transcode to H.264/AAC mp4, cached by blob sha; UI shows "Optimizing for playback…" with progress, then plays. Playback speed control, PiP, keyboard (space, ←→ 5 s, ↑↓ volume, f fullscreen). |
204 +| **Audio** | mp3, wav, flac, ogg, m4a, aac | Custom player: **waveform** (wavesurfer.js or pre-computed peaks via ffmpeg), ID3 tags + embedded cover art displayed, loop, speed. |
205 +| **PDF** | | Rendered with **pdf.js**: page thumbnails rail, page nav, zoom, text selection + in-document search, print. |
206 +| **Office** | docx, xlsx, pptx, odt, ods, odp, rtf | Converted to PDF via **libreoffice --headless** (queued, cached by sha) then shown in the pdf.js viewer with a note "Converted preview — download for original". xlsx additionally offers a native fast path: SheetJS → styled HTML table with sheet tabs. |
207 +| **Code** | 100+ extensions (js, ts, py, go, rs, c, cpp, java, sh, sql, …) | **Shiki** highlighting, line numbers, wrap toggle, copy button, language auto-detect fallback. |
208 +| **Markdown** | md | Full **GFM render** (same pipeline standard as SPB Git: tables, task lists, badges inline, heading anchors, mermaid blocks) with rendered/source toggle. |
209 +| **Data** | csv, tsv | Virtualized table (fast on 100k rows), sticky header, column sort, cell search, "detected delimiter" smartness. |
210 +| **Structured text** | json, yaml, toml, xml | Pretty-printed, syntax-highlighted, JSON gets a collapsible tree explorer toggle. |
211 +| **Notebooks** | ipynb | Cells rendered: markdown cells via GFM, code cells highlighted, outputs (text/images) shown. |
212 +| **Archives** | zip, tar, tar.gz, 7z*, rar* | **Browse inside the archive**: file tree with sizes, preview text/image members directly (streamed extraction of single member), "Extract to folder…" action. 7z/rar listing via `7z` binary if installed, else download card. |
213 +| **Fonts** | ttf, otf, woff, woff2 | Specimen page: alphabet, pangram, size slider, weight info. |
214 +| **Email** | eml | Parsed headers + HTML/plain body (sanitized) + attachment list (each previewable). |
215 +| **Ebooks** | epub | epub.js reader with chapters. |
216 +| **3D** *(nice-to-have)* | stl, glb | three.js orbit viewer. |
217 +| **Everything else** | * | Elegant fallback card: big type icon, filename, size, mime, sha256, "Download" button — never an ugly error. |
218 +
219 +**Thumbnails** (`/thumb/:nodeId?size=`): images via sharp (256/512, webp), videos via ffmpeg frame @10%, PDFs via pdf render of page 1, office via converted-PDF page 1, code/text via generic type icons (crisp custom SVG icon set per extension family, color-coded). All cached by `(sha, size)`.
220 +
221 +---
222 +
223 +## 6. Sharing System — clean public URLs
224 +
225 +Any file **or folder** → "Share" → modal:
226 +
227 +- Generates `https://drive.spboucher.ai/s/<token>` (token = 10-char base58, unguessable, signed).
228 +- Options per share: **expiry** (1 h / 1 d / 7 d / 30 d / never / custom date), **password** (argon2-hashed; public visitor gets a minimal password gate page), **max downloads** (counter-enforced), **allow download** toggle (off = preview-only, download endpoints refuse), note-to-self label.
229 +- **File share page** (public, no session): centered card with the full preview engine (same viewer as the app), file name/size, Download button, "Shared by Simon-Pierre Boucher · contact@spboucher.ai" footer. Proper Open Graph tags (thumbnail as og:image) so links unfurl nicely in iMessage/Slack/Twitter.
230 +- **Folder share page**: read-only file browser of that subtree (grid/list, previews work, per-file download) + "Download all as ZIP" (server streams a zip built on the fly, `archiver`).
231 +- **Share manager** (app section): table of all active shares — target, URL (copy button), visits, downloads, expiry countdown, revoke button, edit options. Expired/revoked links show a clean "This link has expired" page.
232 +- Every share visit/download logged to `activity` (timestamp, IP, user-agent) and visible in the share's detail drawer.
233 +- QR code button next to every share URL (generated server-side, SVG).
234 +
235 +---
236 +
237 +## 7. Search, Recents & Activity
238 +
239 +- **Global search** (`/` shortcut): FTS5 over name + tags + extracted text. **Text extraction pipeline** (queued, cached): plain/code/md/csv indexed directly; PDF via `pdftotext`; docx/xlsx/pptx via converted text; results ranked, with snippet highlights.
240 +- Filter chips in search: type (image/video/audio/doc/archive), tag, folder scope, date range, size range, starred, shared.
241 +- **Recent** view: last 50 touched files. **Activity** page: chronological log (uploads, renames, moves, shares created, share visits, logins) with icons — this doubles as a security audit trail.
242 +
243 +---
244 +
245 +## 8. Web UI Design Spec — must be sharp
246 +
247 +- **Same design DNA as SPB Git** (they're siblings): dark default `#0b0e14` background, surface `#11151c`, border `#1f2530`, text `#e6e9ef`, muted `#8b93a3`, accent `#4f8cff`, accent-2 `#22d3aa`, danger `#ff5d5d`; light theme toggle; `Inter` + `JetBrains Mono`, self-hosted woff2; radius 10px; borders over shadows.
248 +- **App shell**: top bar (logo "SPB Drive", global search, upload button, view toggle, theme, settings) · left sidebar (New button with dropdown: folder/upload/paste, tree, sections, tags, storage meter) · main pane (breadcrumb, toolbar, content) · right **Info panel** (slides in: preview thumb, metadata, tags editor, share list, activity for that node).
249 +- Micro-interactions: 150 ms ease transitions, skeleton loaders, optimistic UI on rename/move/star with rollback on error, toast notifications (undo on trash: "Moved to trash · Undo").
250 +- Empty states illustrated (custom minimal SVG art): empty folder, empty trash, no search results.
251 +- Fully responsive: on mobile, sidebar becomes a drawer, grid adapts, upload via native picker, previews go full-screen. Touch: long-press = context menu.
252 +- Custom 404/500/expired-share pages in the design system. Favicon + PWA manifest (installable, standalone display) — offline is out of scope, but the icon on a phone home screen must look pro.
253 +- Lighthouse ≥ 90/95 (app) and ≥ 95/95 (share pages). No third-party CDN requests anywhere.
254 +
255 +---
256 +
257 +## 9. `spbdrive` CLI
258 +
259 +Companion CLI (`npm i -g` / `npm link`), config in `~/.spbdrive/config.json` (server URL + an **API token** created in Settings — the CLI never stores the login password). Chmod 600.
260 +
261 +| Command | Behavior |
262 +|---|---|
263 +| `spbdrive init` | Wizard: server URL + API token paste, verifies with `/api/v1/me`. |
264 +| `spbdrive ls [remote-path]` | List a folder (aligned, colorized; `--json`). |
265 +| `spbdrive up <files...> [-d /remote/folder]` | Chunked upload with progress bars; directories recurse. |
266 +| `spbdrive down <remote-path> [local]` | Download file, or folder as zip. |
267 +| `spbdrive mkdir / mv / rm / restore` | Tree operations (rm → trash). |
268 +| `spbdrive share <remote-path> [--expires 7d] [--password] [--max-dl N]` | Create share, print URL (and `--qr` renders an ANSI QR in the terminal). |
269 +| `spbdrive shares` / `spbdrive revoke <token>` | Manage shares. |
270 +| `spbdrive search "query"` | FTS search from terminal. |
271 +| `spbdrive push <local-dir> <remote-dir>` | One-way sync mirror (hash-compare, upload changed, `--delete` optional). |
272 +| `spbdrive doctor` | Config/token/server/ffmpeg-on-server diagnostics. |
273 +
274 +Exit codes 0/1/2/3 and `NO_COLOR` respected, same conventions as spbgit.
275 +
276 +---
277 +
278 +## 10. JSON API (`/api/v1`) — session or Bearer API-token auth
279 +
280 +`GET /me` · `GET /nodes/:id` · `GET /nodes/:id/children?sort&view` · `POST /nodes` (mkdir) · `PATCH /nodes/:id` (rename/move/star/color/tags) · `DELETE /nodes/:id` (trash) + `/restore` + `?force=true` · upload trio (`/upload/init|chunk|complete`) · `GET /dl/:id` + `GET /stream/:id` (Range) · `GET /thumb/:id` · shares CRUD (`/shares`) · `GET /search?q&filters` · `GET /activity` · `GET /stats` (storage totals) · `POST /auth/login|logout`, `POST /auth/password`, `GET|DELETE /auth/sessions`, `POST /auth/api-tokens`.
281 +
282 +Errors as `{ "error": { "code", "message" } }`. Rate limit `/auth/login` hard (see §3) and public `/s/*` endpoints (100 req/min/IP).
283 +
284 +---
285 +
286 +## 11. Deployment — node m3u96b + ngrok + `drive.spboucher.ai`
287 +
288 +### 11.1 `deploy/setup-m3u96b.sh` (idempotent)
289 +
290 +1. Install Node 20, **ffmpeg**, **libreoffice** (headless), **poppler-utils** (pdftotext), `7z`, ngrok.
291 +2. Create `/srv/drive/{files,db,cache,backups,logs}` with correct ownership/permissions (700).
292 +3. Clone/pull repo to `/srv/drive/app`, `npm ci --omit=dev`, run migrations.
293 +4. First-boot: prompt for bootstrap password (or read `SPBDRIVE_BOOTSTRAP_PASSWORD`), hash, store; generate session/share keys.
294 +5. Install pm2 config, start, `pm2 save`.
295 +
296 +### 11.2 ngrok (`deploy/ngrok.yml`)
297 +
298 +```yaml
299 +version: 3
300 +agent:
301 + authtoken: ${NGROK_AUTHTOKEN}
302 +endpoints:
303 + - name: spbdrive
304 + url: https://drive.spboucher.ai
305 + upstream:
306 + url: http://127.0.0.1:7430
307 +```
308 +
309 +Document: register `drive.spboucher.ai` in the ngrok dashboard, add the CNAME at the DNS provider, TLS terminates at ngrok, app runs `trustProxy: true` and reads client IP from `x-forwarded-for` (used by lockout + share logs).
310 +
311 +### 11.3 Process management & ops
312 +
313 +- pm2 apps `spbdrive-server` + `spbdrive-tunnel`, autorestart, logs to `/srv/drive/logs/`, `pm2 startup` documented (survives m3u96b reboots). systemd unit provided as alternative.
314 +- `GET /healthz`: uptime, node count, storage used, queue depth.
315 +- **Backups** (`deploy/backup.sh`, nightly cron): sqlite `.backup` snapshot + incremental rsync-style hardlink copy of `/srv/drive/files` into `backups/YYYY-MM-DD/`, keep 14 dailies + 8 weeklies; verify + log. Restore procedure documented in README.
316 +- Daily maintenance job: purge 30-day trash, GC orphaned blobs (refcount 0), prune expired shares/sessions, vacuum FTS.
317 +
318 +---
319 +
320 +## 12. Security Checklist (verify each before "done")
321 +
322 +- [ ] `sn18brady` appears nowhere in the repo; grep in CI (`check:secrets` script) proves it.
323 +- [ ] argon2id everywhere (login, share passwords); constant-time token compares; lockout works (test it).
324 +- [ ] All cookies `httpOnly` + `Secure` + `SameSite=Lax`; CSRF token on state-changing form posts.
325 +- [ ] Share tokens ≥ 58 bits entropy; revocation immediate; expired links leak nothing (no filename in error page title).
326 +- [ ] Path traversal impossible (nodes are DB ids, blobs are hashes — never trust client paths; validate names against `^[^/\\\0]{1,255}$`, reject `.`/`..`).
327 +- [ ] Uploaded HTML/SVG never served same-origin as executable content: previews sandboxed (`Content-Security-Policy: sandbox`, `X-Content-Type-Options: nosniff`; raw HTML downloads as attachment).
328 +- [ ] Strict CSP on the app (`default-src 'self'`); share pages likewise.
329 +- [ ] ffmpeg/libreoffice invoked with `execFile` (no shell), timeouts, and memory/size guards; conversion of hostile files can't take the server down.
330 +- [ ] Session revocation page works; logout everywhere button.
331 +- [ ] No secrets/passwords in logs (pino redaction paths configured).
332 +
333 +---
334 +
335 +## 13. Quality Bar & Definition of Done
336 +
337 +**Engineering standards:** ESM, small modules, JSDoc on exports, prepared statements only (no string-built SQL), vitest coverage on blob store + tree ops + share logic, and an e2e test that boots the server, logs in, chunk-uploads a file, fetches its thumbnail, creates a password-protected share, and downloads through it. `npm run lint` + `check:headers` + `check:secrets` all green.
338 +
339 +**The platform is DONE when every box is checked:**
340 +
341 +- [ ] `bash deploy/setup-m3u96b.sh` on a fresh m3u96b brings `https://drive.spboucher.ai` live and it survives a reboot.
342 +- [ ] Wrong password 5× → locked out; right password → in; password changeable in Settings.
343 +- [ ] Drag-dropping a **folder** with 500 nested files uploads with hierarchy intact, resumable, with a live progress panel.
344 +- [ ] A 2 GB video uploads, gets a poster thumbnail, and **seeks instantly** in the player; an mkv transcodes then plays.
345 +- [ ] docx, xlsx, pptx, pdf, epub, ipynb, zip (browse inside!), csv (100k rows), json, md (with badges), mp3 (waveform), heic, svg, and an unknown `.xyz` file all preview correctly or fall back elegantly.
346 +- [ ] Any file/folder → share URL with expiry + password + max-downloads; folder share offers ZIP-all; link unfurls with a thumbnail in social apps; revoke kills it instantly.
347 +- [ ] Multi-select drag-move, rename inline, trash + undo toast, restore, tags, folder colors, starred — all functional with keyboard shortcuts.
348 +- [ ] Search finds a word **inside** an uploaded PDF.
349 +- [ ] Storage meter, activity log, share visit logs all accurate.
350 +- [ ] `spbdrive up ~/photos -d /Photos` and `spbdrive share /Photos --expires 7d` work end-to-end.
351 +- [ ] Every generated file carries the **Simon-Pierre Boucher / contact@spboucher.ai** header (`check:headers` passes).
352 +- [ ] Nightly backup ran and the documented restore procedure was tested once.
353 +
354 +---
355 +
356 +## 14. Build Order (follow this sequence)
357 +
358 +1. **Skeleton**: config, Fastify, sqlite schema + migrations, logging, healthz, header/secret check scripts.
359 +2. **Auth**: password bootstrap, sessions, login page, lockout, settings (change password, sessions list).
360 +3. **Storage core**: blob store, nodes tree ops, chunked resumable upload, download/stream with Range, trash + GC.
361 +4. **Web app shell**: design tokens, layout, folder browsing (grid+list), drag-drop upload panel, multi-select ops, context menus, keyboard nav.
362 +5. **Preview engine**: thumbnails → images/video/audio → pdf.js → code/md/csv/json → office conversion → archives → fallbacks.
363 +6. **Sharing**: tokens, options, public pages (file + folder + zip-all), share manager, OG unfurls, QR.
364 +7. **Search & activity**: FTS, extraction pipeline, recents, activity log.
365 +8. **CLI**: init → ls/up/down → share → push sync → doctor.
366 +9. **Polish**: info panel, tags/colors/stars, empty states, PWA manifest, a11y, Lighthouse.
367 +10. **Deploy**: setup script, ngrok, pm2, backups + restore drill on m3u96b; run the full DoD checklist.
368 +
369 +Work in small conventional commits (`feat:`, `fix:`, `chore:`). At the end of each phase, state which DoD boxes are now satisfied.
added package-lock.json +7109 −0
@@ -0,0 +1,7109 @@
1 +{
2 + "name": "spbdrive",
3 + "version": "1.0.0",
4 + "lockfileVersion": 3,
5 + "requires": true,
6 + "packages": {
7 + "": {
8 + "name": "spbdrive",
9 + "version": "1.0.0",
10 + "license": "MIT",
11 + "dependencies": {
12 + "@fastify/cookie": "^11.0.2",
13 + "@fastify/formbody": "^8.0.2",
14 + "@fastify/static": "^8.1.1",
15 + "@fontsource/inter": "^5.2.5",
16 + "@fontsource/jetbrains-mono": "^5.2.5",
17 + "archiver": "^7.0.1",
18 + "argon2": "^0.44.0",
19 + "better-sqlite3": "^12.2.0",
20 + "commander": "^14.0.0",
21 + "exif-reader": "^2.0.2",
22 + "fastify": "^5.4.0",
23 + "markdown-it": "^14.1.0",
24 + "markdown-it-anchor": "^9.2.0",
25 + "markdown-it-task-lists": "^2.1.1",
26 + "mime": "^4.0.7",
27 + "nunjucks": "^3.2.4",
28 + "pdfjs-dist": "^4.10.38",
29 + "pino": "^9.7.0",
30 + "postal-mime": "^2.4.3",
31 + "qrcode": "^1.5.4",
32 + "sharp": "^0.34.2",
33 + "shiki": "^3.7.0",
34 + "tar-stream": "^3.1.7",
35 + "wavesurfer.js": "^7.9.5",
36 + "xlsx": "^0.18.5",
37 + "yauzl": "^3.2.0",
38 + "zod": "^3.25.67"
39 + },
40 + "bin": {
41 + "spbdrive": "cli/spbdrive.mjs"
42 + },
43 + "devDependencies": {
44 + "eslint": "^9.30.0",
45 + "pino-pretty": "^13.0.0",
46 + "vitest": "^3.2.4"
47 + },
48 + "engines": {
49 + "node": ">=20"
50 + }
51 + },
52 + "node_modules/@emnapi/runtime": {
53 + "version": "1.11.3",
54 + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
55 + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
56 + "license": "MIT",
57 + "optional": true,
58 + "dependencies": {
59 + "tslib": "^2.4.0"
60 + }
61 + },
62 + "node_modules/@epic-web/invariant": {
63 + "version": "1.0.0",
64 + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
65 + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
66 + "license": "MIT"
67 + },
68 + "node_modules/@esbuild/aix-ppc64": {
69 + "version": "0.28.2",
70 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
71 + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
72 + "cpu": [
73 + "ppc64"
74 + ],
75 + "dev": true,
76 + "license": "MIT",
77 + "optional": true,
78 + "os": [
79 + "aix"
80 + ],
81 + "engines": {
82 + "node": ">=18"
83 + }
84 + },
85 + "node_modules/@esbuild/android-arm": {
86 + "version": "0.28.2",
87 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
88 + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
89 + "cpu": [
90 + "arm"
91 + ],
92 + "dev": true,
93 + "license": "MIT",
94 + "optional": true,
95 + "os": [
96 + "android"
97 + ],
98 + "engines": {
99 + "node": ">=18"
100 + }
101 + },
102 + "node_modules/@esbuild/android-arm64": {
103 + "version": "0.28.2",
104 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
105 + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
106 + "cpu": [
107 + "arm64"
108 + ],
109 + "dev": true,
110 + "license": "MIT",
111 + "optional": true,
112 + "os": [
113 + "android"
114 + ],
115 + "engines": {
116 + "node": ">=18"
117 + }
118 + },
119 + "node_modules/@esbuild/android-x64": {
120 + "version": "0.28.2",
121 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
122 + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
123 + "cpu": [
124 + "x64"
125 + ],
126 + "dev": true,
127 + "license": "MIT",
128 + "optional": true,
129 + "os": [
130 + "android"
131 + ],
132 + "engines": {
133 + "node": ">=18"
134 + }
135 + },
136 + "node_modules/@esbuild/darwin-arm64": {
137 + "version": "0.28.2",
138 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
139 + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
140 + "cpu": [
141 + "arm64"
142 + ],
143 + "dev": true,
144 + "license": "MIT",
145 + "optional": true,
146 + "os": [
147 + "darwin"
148 + ],
149 + "engines": {
150 + "node": ">=18"
151 + }
152 + },
153 + "node_modules/@esbuild/darwin-x64": {
154 + "version": "0.28.2",
155 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
156 + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
157 + "cpu": [
158 + "x64"
159 + ],
160 + "dev": true,
161 + "license": "MIT",
162 + "optional": true,
163 + "os": [
164 + "darwin"
165 + ],
166 + "engines": {
167 + "node": ">=18"
168 + }
169 + },
170 + "node_modules/@esbuild/freebsd-arm64": {
171 + "version": "0.28.2",
172 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
173 + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
174 + "cpu": [
175 + "arm64"
176 + ],
177 + "dev": true,
178 + "license": "MIT",
179 + "optional": true,
180 + "os": [
181 + "freebsd"
182 + ],
183 + "engines": {
184 + "node": ">=18"
185 + }
186 + },
187 + "node_modules/@esbuild/freebsd-x64": {
188 + "version": "0.28.2",
189 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
190 + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
191 + "cpu": [
192 + "x64"
193 + ],
194 + "dev": true,
195 + "license": "MIT",
196 + "optional": true,
197 + "os": [
198 + "freebsd"
199 + ],
200 + "engines": {
201 + "node": ">=18"
202 + }
203 + },
204 + "node_modules/@esbuild/linux-arm": {
205 + "version": "0.28.2",
206 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
207 + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
208 + "cpu": [
209 + "arm"
210 + ],
211 + "dev": true,
212 + "license": "MIT",
213 + "optional": true,
214 + "os": [
215 + "linux"
216 + ],
217 + "engines": {
218 + "node": ">=18"
219 + }
220 + },
221 + "node_modules/@esbuild/linux-arm64": {
222 + "version": "0.28.2",
223 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
224 + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
225 + "cpu": [
226 + "arm64"
227 + ],
228 + "dev": true,
229 + "license": "MIT",
230 + "optional": true,
231 + "os": [
232 + "linux"
233 + ],
234 + "engines": {
235 + "node": ">=18"
236 + }
237 + },
238 + "node_modules/@esbuild/linux-ia32": {
239 + "version": "0.28.2",
240 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
241 + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
242 + "cpu": [
243 + "ia32"
244 + ],
245 + "dev": true,
246 + "license": "MIT",
247 + "optional": true,
248 + "os": [
249 + "linux"
250 + ],
251 + "engines": {
252 + "node": ">=18"
253 + }
254 + },
255 + "node_modules/@esbuild/linux-loong64": {
256 + "version": "0.28.2",
257 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
258 + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
259 + "cpu": [
260 + "loong64"
261 + ],
262 + "dev": true,
263 + "license": "MIT",
264 + "optional": true,
265 + "os": [
266 + "linux"
267 + ],
268 + "engines": {
269 + "node": ">=18"
270 + }
271 + },
272 + "node_modules/@esbuild/linux-mips64el": {
273 + "version": "0.28.2",
274 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
275 + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
276 + "cpu": [
277 + "mips64el"
278 + ],
279 + "dev": true,
280 + "license": "MIT",
281 + "optional": true,
282 + "os": [
283 + "linux"
284 + ],
285 + "engines": {
286 + "node": ">=18"
287 + }
288 + },
289 + "node_modules/@esbuild/linux-ppc64": {
290 + "version": "0.28.2",
291 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
292 + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
293 + "cpu": [
294 + "ppc64"
295 + ],
296 + "dev": true,
297 + "license": "MIT",
298 + "optional": true,
299 + "os": [
300 + "linux"
301 + ],
302 + "engines": {
303 + "node": ">=18"
304 + }
305 + },
306 + "node_modules/@esbuild/linux-riscv64": {
307 + "version": "0.28.2",
308 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
309 + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
310 + "cpu": [
311 + "riscv64"
312 + ],
313 + "dev": true,
314 + "license": "MIT",
315 + "optional": true,
316 + "os": [
317 + "linux"
318 + ],
319 + "engines": {
320 + "node": ">=18"
321 + }
322 + },
323 + "node_modules/@esbuild/linux-s390x": {
324 + "version": "0.28.2",
325 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
326 + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
327 + "cpu": [
328 + "s390x"
329 + ],
330 + "dev": true,
331 + "license": "MIT",
332 + "optional": true,
333 + "os": [
334 + "linux"
335 + ],
336 + "engines": {
337 + "node": ">=18"
338 + }
339 + },
340 + "node_modules/@esbuild/linux-x64": {
341 + "version": "0.28.2",
342 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
343 + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
344 + "cpu": [
345 + "x64"
346 + ],
347 + "dev": true,
348 + "license": "MIT",
349 + "optional": true,
350 + "os": [
351 + "linux"
352 + ],
353 + "engines": {
354 + "node": ">=18"
355 + }
356 + },
357 + "node_modules/@esbuild/netbsd-arm64": {
358 + "version": "0.28.2",
359 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
360 + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
361 + "cpu": [
362 + "arm64"
363 + ],
364 + "dev": true,
365 + "license": "MIT",
366 + "optional": true,
367 + "os": [
368 + "netbsd"
369 + ],
370 + "engines": {
371 + "node": ">=18"
372 + }
373 + },
374 + "node_modules/@esbuild/netbsd-x64": {
375 + "version": "0.28.2",
376 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
377 + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
378 + "cpu": [
379 + "x64"
380 + ],
381 + "dev": true,
382 + "license": "MIT",
383 + "optional": true,
384 + "os": [
385 + "netbsd"
386 + ],
387 + "engines": {
388 + "node": ">=18"
389 + }
390 + },
391 + "node_modules/@esbuild/openbsd-arm64": {
392 + "version": "0.28.2",
393 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
394 + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
395 + "cpu": [
396 + "arm64"
397 + ],
398 + "dev": true,
399 + "license": "MIT",
400 + "optional": true,
401 + "os": [
402 + "openbsd"
403 + ],
404 + "engines": {
405 + "node": ">=18"
406 + }
407 + },
408 + "node_modules/@esbuild/openbsd-x64": {
409 + "version": "0.28.2",
410 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
411 + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
412 + "cpu": [
413 + "x64"
414 + ],
415 + "dev": true,
416 + "license": "MIT",
417 + "optional": true,
418 + "os": [
419 + "openbsd"
420 + ],
421 + "engines": {
422 + "node": ">=18"
423 + }
424 + },
425 + "node_modules/@esbuild/openharmony-arm64": {
426 + "version": "0.28.2",
427 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
428 + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
429 + "cpu": [
430 + "arm64"
431 + ],
432 + "dev": true,
433 + "license": "MIT",
434 + "optional": true,
435 + "os": [
436 + "openharmony"
437 + ],
438 + "engines": {
439 + "node": ">=18"
440 + }
441 + },
442 + "node_modules/@esbuild/sunos-x64": {
443 + "version": "0.28.2",
444 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
445 + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
446 + "cpu": [
447 + "x64"
448 + ],
449 + "dev": true,
450 + "license": "MIT",
451 + "optional": true,
452 + "os": [
453 + "sunos"
454 + ],
455 + "engines": {
456 + "node": ">=18"
457 + }
458 + },
459 + "node_modules/@esbuild/win32-arm64": {
460 + "version": "0.28.2",
461 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
462 + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
463 + "cpu": [
464 + "arm64"
465 + ],
466 + "dev": true,
467 + "license": "MIT",
468 + "optional": true,
469 + "os": [
470 + "win32"
471 + ],
472 + "engines": {
473 + "node": ">=18"
474 + }
475 + },
476 + "node_modules/@esbuild/win32-ia32": {
477 + "version": "0.28.2",
478 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
479 + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
480 + "cpu": [
481 + "ia32"
482 + ],
483 + "dev": true,
484 + "license": "MIT",
485 + "optional": true,
486 + "os": [
487 + "win32"
488 + ],
489 + "engines": {
490 + "node": ">=18"
491 + }
492 + },
493 + "node_modules/@esbuild/win32-x64": {
494 + "version": "0.28.2",
495 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
496 + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
497 + "cpu": [
498 + "x64"
499 + ],
500 + "dev": true,
501 + "license": "MIT",
502 + "optional": true,
503 + "os": [
504 + "win32"
505 + ],
506 + "engines": {
507 + "node": ">=18"
508 + }
509 + },
510 + "node_modules/@eslint-community/eslint-utils": {
511 + "version": "4.10.1",
512 + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
513 + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
514 + "dev": true,
515 + "license": "MIT",
516 + "dependencies": {
517 + "eslint-visitor-keys": "^3.4.3"
518 + },
519 + "engines": {
520 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
521 + },
522 + "funding": {
523 + "url": "https://opencollective.com/eslint"
524 + },
525 + "peerDependencies": {
526 + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
527 + }
528 + },
529 + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
530 + "version": "3.4.3",
531 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
532 + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
533 + "dev": true,
534 + "license": "Apache-2.0",
535 + "engines": {
536 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
537 + },
538 + "funding": {
539 + "url": "https://opencollective.com/eslint"
540 + }
541 + },
542 + "node_modules/@eslint-community/regexpp": {
543 + "version": "4.12.2",
544 + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
545 + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
546 + "dev": true,
547 + "license": "MIT",
548 + "engines": {
549 + "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
550 + }
551 + },
552 + "node_modules/@eslint/config-array": {
553 + "version": "0.21.2",
554 + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
555 + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
556 + "dev": true,
557 + "license": "Apache-2.0",
558 + "dependencies": {
559 + "@eslint/object-schema": "^2.1.7",
560 + "debug": "^4.3.1",
561 + "minimatch": "^3.1.5"
562 + },
563 + "engines": {
564 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
565 + }
566 + },
567 + "node_modules/@eslint/config-helpers": {
568 + "version": "0.4.2",
569 + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
570 + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
571 + "dev": true,
572 + "license": "Apache-2.0",
573 + "dependencies": {
574 + "@eslint/core": "^0.17.0"
575 + },
576 + "engines": {
577 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
578 + }
579 + },
580 + "node_modules/@eslint/core": {
581 + "version": "0.17.0",
582 + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
583 + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
584 + "dev": true,
585 + "license": "Apache-2.0",
586 + "dependencies": {
587 + "@types/json-schema": "^7.0.15"
588 + },
589 + "engines": {
590 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
591 + }
592 + },
593 + "node_modules/@eslint/eslintrc": {
594 + "version": "3.3.6",
595 + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz",
596 + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==",
597 + "dev": true,
598 + "license": "MIT",
599 + "dependencies": {
600 + "ajv": "^6.14.0",
601 + "debug": "^4.3.2",
602 + "espree": "^10.0.1",
603 + "globals": "^14.0.0",
604 + "ignore": "^5.2.0",
605 + "import-fresh": "^3.2.1",
606 + "js-yaml": "^4.3.0",
607 + "minimatch": "^3.1.5",
608 + "strip-json-comments": "^3.1.1"
609 + },
610 + "engines": {
611 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
612 + },
613 + "funding": {
614 + "url": "https://opencollective.com/eslint"
615 + }
616 + },
617 + "node_modules/@eslint/js": {
618 + "version": "9.39.5",
619 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz",
620 + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==",
621 + "dev": true,
622 + "license": "MIT",
623 + "engines": {
624 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
625 + },
626 + "funding": {
627 + "url": "https://eslint.org/donate"
628 + }
629 + },
630 + "node_modules/@eslint/object-schema": {
631 + "version": "2.1.7",
632 + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
633 + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
634 + "dev": true,
635 + "license": "Apache-2.0",
636 + "engines": {
637 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
638 + }
639 + },
640 + "node_modules/@eslint/plugin-kit": {
641 + "version": "0.4.1",
642 + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
643 + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
644 + "dev": true,
645 + "license": "Apache-2.0",
646 + "dependencies": {
647 + "@eslint/core": "^0.17.0",
648 + "levn": "^0.4.1"
649 + },
650 + "engines": {
651 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
652 + }
653 + },
654 + "node_modules/@fastify/accept-negotiator": {
655 + "version": "2.1.0",
656 + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.1.0.tgz",
657 + "integrity": "sha512-F3EVbzWt+xcnVaOHmWyIlpuFtbxOln7HDZQsh09MtMmMm/CipMayNt8hnIL8VQi54u2ZociDbf+iluGYkf7B1A==",
658 + "funding": [
659 + {
660 + "type": "github",
661 + "url": "https://github.com/sponsors/fastify"
662 + },
663 + {
664 + "type": "opencollective",
665 + "url": "https://opencollective.com/fastify"
666 + }
667 + ],
668 + "license": "MIT"
669 + },
670 + "node_modules/@fastify/ajv-compiler": {
671 + "version": "4.0.6",
672 + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.6.tgz",
673 + "integrity": "sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==",
674 + "funding": [
675 + {
676 + "type": "github",
677 + "url": "https://github.com/sponsors/fastify"
678 + },
679 + {
680 + "type": "opencollective",
681 + "url": "https://opencollective.com/fastify"
682 + }
683 + ],
684 + "license": "MIT",
685 + "dependencies": {
686 + "ajv": "^8.12.0",
687 + "ajv-formats": "^3.0.1",
688 + "fast-uri": "^4.0.0"
689 + }
690 + },
691 + "node_modules/@fastify/ajv-compiler/node_modules/ajv": {
692 + "version": "8.20.0",
693 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
694 + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
695 + "license": "MIT",
696 + "dependencies": {
697 + "fast-deep-equal": "^3.1.3",
698 + "fast-uri": "^3.0.1",
699 + "json-schema-traverse": "^1.0.0",
700 + "require-from-string": "^2.0.2"
701 + },
702 + "funding": {
703 + "type": "github",
704 + "url": "https://github.com/sponsors/epoberezkin"
705 + }
706 + },
707 + "node_modules/@fastify/ajv-compiler/node_modules/ajv/node_modules/fast-uri": {
708 + "version": "3.1.5",
709 + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
710 + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
711 + "funding": [
712 + {
713 + "type": "github",
714 + "url": "https://github.com/sponsors/fastify"
715 + },
716 + {
717 + "type": "opencollective",
718 + "url": "https://opencollective.com/fastify"
719 + }
720 + ],
721 + "license": "BSD-3-Clause"
722 + },
723 + "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": {
724 + "version": "1.0.0",
725 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
726 + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
727 + "license": "MIT"
728 + },
729 + "node_modules/@fastify/cookie": {
730 + "version": "11.1.2",
731 + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.1.2.tgz",
732 + "integrity": "sha512-Dtrpk/YOGUsbRMvP/8ZqPpwnMRv0qSqodFdoQ2B589Obc7jw4s4Qla+cV72Bsm7WsZJnqlYFX/i7uSBq0xzg6g==",
733 + "funding": [
734 + {
735 + "type": "github",
736 + "url": "https://github.com/sponsors/fastify"
737 + },
738 + {
739 + "type": "opencollective",
740 + "url": "https://opencollective.com/fastify"
741 + }
742 + ],
743 + "license": "MIT",
744 + "dependencies": {
745 + "cookie": "^2.0.0",
746 + "fastify-plugin": "^6.0.0"
747 + }
748 + },
749 + "node_modules/@fastify/error": {
750 + "version": "4.2.0",
751 + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
752 + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==",
753 + "funding": [
754 + {
755 + "type": "github",
756 + "url": "https://github.com/sponsors/fastify"
757 + },
758 + {
759 + "type": "opencollective",
760 + "url": "https://opencollective.com/fastify"
761 + }
762 + ],
763 + "license": "MIT"
764 + },
765 + "node_modules/@fastify/fast-json-stringify-compiler": {
766 + "version": "5.1.0",
767 + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz",
768 + "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==",
769 + "funding": [
770 + {
771 + "type": "github",
772 + "url": "https://github.com/sponsors/fastify"
773 + },
774 + {
775 + "type": "opencollective",
776 + "url": "https://opencollective.com/fastify"
777 + }
778 + ],
779 + "license": "MIT",
780 + "dependencies": {
781 + "fast-json-stringify": "^7.0.0"
782 + }
783 + },
784 + "node_modules/@fastify/formbody": {
785 + "version": "8.0.2",
786 + "resolved": "https://registry.npmjs.org/@fastify/formbody/-/formbody-8.0.2.tgz",
787 + "integrity": "sha512-84v5J2KrkXzjgBpYnaNRPqwgMsmY7ZDjuj0YVuMR3NXCJRCgKEZy/taSP1wUYGn0onfxJpLyRGDLa+NMaDJtnA==",
788 + "funding": [
789 + {
790 + "type": "github",
791 + "url": "https://github.com/sponsors/fastify"
792 + },
793 + {
794 + "type": "opencollective",
795 + "url": "https://opencollective.com/fastify"
796 + }
797 + ],
798 + "license": "MIT",
799 + "dependencies": {
800 + "fast-querystring": "^1.1.2",
801 + "fastify-plugin": "^5.0.0"
802 + }
803 + },
804 + "node_modules/@fastify/formbody/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/@fastify/forwarded": {
821 + "version": "3.0.2",
822 + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.2.tgz",
823 + "integrity": "sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==",
824 + "funding": [
825 + {
826 + "type": "github",
827 + "url": "https://github.com/sponsors/fastify"
828 + },
829 + {
830 + "type": "opencollective",
831 + "url": "https://opencollective.com/fastify"
832 + }
833 + ],
834 + "license": "MIT"
835 + },
836 + "node_modules/@fastify/merge-json-schemas": {
837 + "version": "0.2.1",
838 + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz",
839 + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==",
840 + "funding": [
841 + {
842 + "type": "github",
843 + "url": "https://github.com/sponsors/fastify"
844 + },
845 + {
846 + "type": "opencollective",
847 + "url": "https://opencollective.com/fastify"
848 + }
849 + ],
850 + "license": "MIT",
851 + "dependencies": {
852 + "dequal": "^2.0.3"
853 + }
854 + },
855 + "node_modules/@fastify/proxy-addr": {
856 + "version": "5.1.0",
857 + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz",
858 + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==",
859 + "funding": [
860 + {
861 + "type": "github",
862 + "url": "https://github.com/sponsors/fastify"
863 + },
864 + {
865 + "type": "opencollective",
866 + "url": "https://opencollective.com/fastify"
867 + }
868 + ],
869 + "license": "MIT",
870 + "dependencies": {
871 + "@fastify/forwarded": "^3.0.0",
872 + "ipaddr.js": "^2.1.0"
873 + }
874 + },
875 + "node_modules/@fastify/send": {
876 + "version": "4.1.1",
877 + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.1.tgz",
878 + "integrity": "sha512-BYo+EiaKwlxH+WetGk6hAs1d39iP0y1gqB8lGF/qwkJ9ZZ/cBY1vx5NvExb9Sc3yRMFjD5X4Eyh4e4+TzRkzdw==",
879 + "funding": [
880 + {
881 + "type": "github",
882 + "url": "https://github.com/sponsors/fastify"
883 + },
884 + {
885 + "type": "opencollective",
886 + "url": "https://opencollective.com/fastify"
887 + }
888 + ],
889 + "license": "MIT",
890 + "dependencies": {
891 + "@lukeed/ms": "^2.0.2",
892 + "escape-html": "~1.0.3",
893 + "fast-decode-uri-component": "^1.0.1",
894 + "http-errors": "^2.0.0",
895 + "mime": "^3"
896 + }
897 + },
898 + "node_modules/@fastify/send/node_modules/mime": {
899 + "version": "3.0.0",
900 + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
901 + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
902 + "license": "MIT",
903 + "bin": {
904 + "mime": "cli.js"
905 + },
906 + "engines": {
907 + "node": ">=10.0.0"
908 + }
909 + },
910 + "node_modules/@fastify/static": {
911 + "version": "8.3.0",
912 + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.3.0.tgz",
913 + "integrity": "sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==",
914 + "funding": [
915 + {
916 + "type": "github",
917 + "url": "https://github.com/sponsors/fastify"
918 + },
919 + {
920 + "type": "opencollective",
921 + "url": "https://opencollective.com/fastify"
922 + }
923 + ],
924 + "license": "MIT",
925 + "dependencies": {
926 + "@fastify/accept-negotiator": "^2.0.0",
927 + "@fastify/send": "^4.0.0",
928 + "content-disposition": "^0.5.4",
929 + "fastify-plugin": "^5.0.0",
930 + "fastq": "^1.17.1",
931 + "glob": "^11.0.0"
932 + }
933 + },
934 + "node_modules/@fastify/static/node_modules/fastify-plugin": {
935 + "version": "5.1.0",
936 + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
937 + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
938 + "funding": [
939 + {
940 + "type": "github",
941 + "url": "https://github.com/sponsors/fastify"
942 + },
943 + {
944 + "type": "opencollective",
945 + "url": "https://opencollective.com/fastify"
946 + }
947 + ],
948 + "license": "MIT"
949 + },
950 + "node_modules/@fontsource/inter": {
951 + "version": "5.3.0",
952 + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.3.0.tgz",
953 + "integrity": "sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g==",
954 + "license": "OFL-1.1",
955 + "funding": {
956 + "url": "https://github.com/sponsors/ayuhito"
957 + }
958 + },
959 + "node_modules/@fontsource/jetbrains-mono": {
960 + "version": "5.3.0",
961 + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz",
962 + "integrity": "sha512-fqDfB5I9f1p1TV486aUgB9t8zP84P0O1FtQR5Ol9vjwPy+S+EIGlVYm1cvj2W5shcZMTg2nZFdVMoH5wFu8a1A==",
963 + "license": "OFL-1.1",
964 + "funding": {
965 + "url": "https://github.com/sponsors/ayuhito"
966 + }
967 + },
968 + "node_modules/@humanfs/core": {
969 + "version": "0.19.2",
970 + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
971 + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
972 + "dev": true,
973 + "license": "Apache-2.0",
974 + "dependencies": {
975 + "@humanfs/types": "^0.15.0"
976 + },
977 + "engines": {
978 + "node": ">=18.18.0"
979 + }
980 + },
981 + "node_modules/@humanfs/node": {
982 + "version": "0.16.8",
983 + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
984 + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
985 + "dev": true,
986 + "license": "Apache-2.0",
987 + "dependencies": {
988 + "@humanfs/core": "^0.19.2",
989 + "@humanfs/types": "^0.15.0",
990 + "@humanwhocodes/retry": "^0.4.0"
991 + },
992 + "engines": {
993 + "node": ">=18.18.0"
994 + }
995 + },
996 + "node_modules/@humanfs/types": {
997 + "version": "0.15.0",
998 + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
999 + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
1000 + "dev": true,
1001 + "license": "Apache-2.0",
1002 + "engines": {
1003 + "node": ">=18.18.0"
1004 + }
1005 + },
1006 + "node_modules/@humanwhocodes/module-importer": {
1007 + "version": "1.0.1",
1008 + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
1009 + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
1010 + "dev": true,
1011 + "license": "Apache-2.0",
1012 + "engines": {
1013 + "node": ">=12.22"
1014 + },
1015 + "funding": {
1016 + "type": "github",
1017 + "url": "https://github.com/sponsors/nzakas"
1018 + }
1019 + },
1020 + "node_modules/@humanwhocodes/retry": {
1021 + "version": "0.4.3",
1022 + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
1023 + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
1024 + "dev": true,
1025 + "license": "Apache-2.0",
1026 + "engines": {
1027 + "node": ">=18.18"
1028 + },
1029 + "funding": {
1030 + "type": "github",
1031 + "url": "https://github.com/sponsors/nzakas"
1032 + }
1033 + },
1034 + "node_modules/@img/colour": {
1035 + "version": "1.1.0",
1036 + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
1037 + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
1038 + "license": "MIT",
1039 + "engines": {
1040 + "node": ">=18"
1041 + }
1042 + },
1043 + "node_modules/@img/sharp-darwin-arm64": {
1044 + "version": "0.34.5",
1045 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
1046 + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
1047 + "cpu": [
1048 + "arm64"
1049 + ],
1050 + "license": "Apache-2.0",
1051 + "optional": true,
1052 + "os": [
1053 + "darwin"
1054 + ],
1055 + "engines": {
1056 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1057 + },
1058 + "funding": {
1059 + "url": "https://opencollective.com/libvips"
1060 + },
1061 + "optionalDependencies": {
1062 + "@img/sharp-libvips-darwin-arm64": "1.2.4"
1063 + }
1064 + },
1065 + "node_modules/@img/sharp-darwin-x64": {
1066 + "version": "0.34.5",
1067 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
1068 + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
1069 + "cpu": [
1070 + "x64"
1071 + ],
1072 + "license": "Apache-2.0",
1073 + "optional": true,
1074 + "os": [
1075 + "darwin"
1076 + ],
1077 + "engines": {
1078 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1079 + },
1080 + "funding": {
1081 + "url": "https://opencollective.com/libvips"
1082 + },
1083 + "optionalDependencies": {
1084 + "@img/sharp-libvips-darwin-x64": "1.2.4"
1085 + }
1086 + },
1087 + "node_modules/@img/sharp-libvips-darwin-arm64": {
1088 + "version": "1.2.4",
1089 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
1090 + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
1091 + "cpu": [
1092 + "arm64"
1093 + ],
1094 + "license": "LGPL-3.0-or-later",
1095 + "optional": true,
1096 + "os": [
1097 + "darwin"
1098 + ],
1099 + "funding": {
1100 + "url": "https://opencollective.com/libvips"
1101 + }
1102 + },
1103 + "node_modules/@img/sharp-libvips-darwin-x64": {
1104 + "version": "1.2.4",
1105 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
1106 + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
1107 + "cpu": [
1108 + "x64"
1109 + ],
1110 + "license": "LGPL-3.0-or-later",
1111 + "optional": true,
1112 + "os": [
1113 + "darwin"
1114 + ],
1115 + "funding": {
1116 + "url": "https://opencollective.com/libvips"
1117 + }
1118 + },
1119 + "node_modules/@img/sharp-libvips-linux-arm": {
1120 + "version": "1.2.4",
1121 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
1122 + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
1123 + "cpu": [
1124 + "arm"
1125 + ],
1126 + "libc": [
1127 + "glibc"
1128 + ],
1129 + "license": "LGPL-3.0-or-later",
1130 + "optional": true,
1131 + "os": [
1132 + "linux"
1133 + ],
1134 + "funding": {
1135 + "url": "https://opencollective.com/libvips"
1136 + }
1137 + },
1138 + "node_modules/@img/sharp-libvips-linux-arm64": {
1139 + "version": "1.2.4",
1140 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
1141 + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
1142 + "cpu": [
1143 + "arm64"
1144 + ],
1145 + "libc": [
1146 + "glibc"
1147 + ],
1148 + "license": "LGPL-3.0-or-later",
1149 + "optional": true,
1150 + "os": [
1151 + "linux"
1152 + ],
1153 + "funding": {
1154 + "url": "https://opencollective.com/libvips"
1155 + }
1156 + },
1157 + "node_modules/@img/sharp-libvips-linux-ppc64": {
1158 + "version": "1.2.4",
1159 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
1160 + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
1161 + "cpu": [
1162 + "ppc64"
1163 + ],
1164 + "libc": [
1165 + "glibc"
1166 + ],
1167 + "license": "LGPL-3.0-or-later",
1168 + "optional": true,
1169 + "os": [
1170 + "linux"
1171 + ],
1172 + "funding": {
1173 + "url": "https://opencollective.com/libvips"
1174 + }
1175 + },
1176 + "node_modules/@img/sharp-libvips-linux-riscv64": {
1177 + "version": "1.2.4",
1178 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
1179 + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
1180 + "cpu": [
1181 + "riscv64"
1182 + ],
1183 + "libc": [
1184 + "glibc"
1185 + ],
1186 + "license": "LGPL-3.0-or-later",
1187 + "optional": true,
1188 + "os": [
1189 + "linux"
1190 + ],
1191 + "funding": {
1192 + "url": "https://opencollective.com/libvips"
1193 + }
1194 + },
1195 + "node_modules/@img/sharp-libvips-linux-s390x": {
1196 + "version": "1.2.4",
1197 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
1198 + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
1199 + "cpu": [
1200 + "s390x"
1201 + ],
1202 + "libc": [
1203 + "glibc"
1204 + ],
1205 + "license": "LGPL-3.0-or-later",
1206 + "optional": true,
1207 + "os": [
1208 + "linux"
1209 + ],
1210 + "funding": {
1211 + "url": "https://opencollective.com/libvips"
1212 + }
1213 + },
1214 + "node_modules/@img/sharp-libvips-linux-x64": {
1215 + "version": "1.2.4",
1216 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
1217 + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
1218 + "cpu": [
1219 + "x64"
1220 + ],
1221 + "libc": [
1222 + "glibc"
1223 + ],
1224 + "license": "LGPL-3.0-or-later",
1225 + "optional": true,
1226 + "os": [
1227 + "linux"
1228 + ],
1229 + "funding": {
1230 + "url": "https://opencollective.com/libvips"
1231 + }
1232 + },
1233 + "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
1234 + "version": "1.2.4",
1235 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
1236 + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
1237 + "cpu": [
1238 + "arm64"
1239 + ],
1240 + "libc": [
1241 + "musl"
1242 + ],
1243 + "license": "LGPL-3.0-or-later",
1244 + "optional": true,
1245 + "os": [
1246 + "linux"
1247 + ],
1248 + "funding": {
1249 + "url": "https://opencollective.com/libvips"
1250 + }
1251 + },
1252 + "node_modules/@img/sharp-libvips-linuxmusl-x64": {
1253 + "version": "1.2.4",
1254 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
1255 + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
1256 + "cpu": [
1257 + "x64"
1258 + ],
1259 + "libc": [
1260 + "musl"
1261 + ],
1262 + "license": "LGPL-3.0-or-later",
1263 + "optional": true,
1264 + "os": [
1265 + "linux"
1266 + ],
1267 + "funding": {
1268 + "url": "https://opencollective.com/libvips"
1269 + }
1270 + },
1271 + "node_modules/@img/sharp-linux-arm": {
1272 + "version": "0.34.5",
1273 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
1274 + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
1275 + "cpu": [
1276 + "arm"
1277 + ],
1278 + "libc": [
1279 + "glibc"
1280 + ],
1281 + "license": "Apache-2.0",
1282 + "optional": true,
1283 + "os": [
1284 + "linux"
1285 + ],
1286 + "engines": {
1287 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1288 + },
1289 + "funding": {
1290 + "url": "https://opencollective.com/libvips"
1291 + },
1292 + "optionalDependencies": {
1293 + "@img/sharp-libvips-linux-arm": "1.2.4"
1294 + }
1295 + },
1296 + "node_modules/@img/sharp-linux-arm64": {
1297 + "version": "0.34.5",
1298 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
1299 + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
1300 + "cpu": [
1301 + "arm64"
1302 + ],
1303 + "libc": [
1304 + "glibc"
1305 + ],
1306 + "license": "Apache-2.0",
1307 + "optional": true,
1308 + "os": [
1309 + "linux"
1310 + ],
1311 + "engines": {
1312 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1313 + },
1314 + "funding": {
1315 + "url": "https://opencollective.com/libvips"
1316 + },
1317 + "optionalDependencies": {
1318 + "@img/sharp-libvips-linux-arm64": "1.2.4"
1319 + }
1320 + },
1321 + "node_modules/@img/sharp-linux-ppc64": {
1322 + "version": "0.34.5",
1323 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
1324 + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
1325 + "cpu": [
1326 + "ppc64"
1327 + ],
1328 + "libc": [
1329 + "glibc"
1330 + ],
1331 + "license": "Apache-2.0",
1332 + "optional": true,
1333 + "os": [
1334 + "linux"
1335 + ],
1336 + "engines": {
1337 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1338 + },
1339 + "funding": {
1340 + "url": "https://opencollective.com/libvips"
1341 + },
1342 + "optionalDependencies": {
1343 + "@img/sharp-libvips-linux-ppc64": "1.2.4"
1344 + }
1345 + },
1346 + "node_modules/@img/sharp-linux-riscv64": {
1347 + "version": "0.34.5",
1348 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
1349 + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
1350 + "cpu": [
1351 + "riscv64"
1352 + ],
1353 + "libc": [
1354 + "glibc"
1355 + ],
1356 + "license": "Apache-2.0",
1357 + "optional": true,
1358 + "os": [
1359 + "linux"
1360 + ],
1361 + "engines": {
1362 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1363 + },
1364 + "funding": {
1365 + "url": "https://opencollective.com/libvips"
1366 + },
1367 + "optionalDependencies": {
1368 + "@img/sharp-libvips-linux-riscv64": "1.2.4"
1369 + }
1370 + },
1371 + "node_modules/@img/sharp-linux-s390x": {
1372 + "version": "0.34.5",
1373 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
1374 + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
1375 + "cpu": [
1376 + "s390x"
1377 + ],
1378 + "libc": [
1379 + "glibc"
1380 + ],
1381 + "license": "Apache-2.0",
1382 + "optional": true,
1383 + "os": [
1384 + "linux"
1385 + ],
1386 + "engines": {
1387 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1388 + },
1389 + "funding": {
1390 + "url": "https://opencollective.com/libvips"
1391 + },
1392 + "optionalDependencies": {
1393 + "@img/sharp-libvips-linux-s390x": "1.2.4"
1394 + }
1395 + },
1396 + "node_modules/@img/sharp-linux-x64": {
1397 + "version": "0.34.5",
1398 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
1399 + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
1400 + "cpu": [
1401 + "x64"
1402 + ],
1403 + "libc": [
1404 + "glibc"
1405 + ],
1406 + "license": "Apache-2.0",
1407 + "optional": true,
1408 + "os": [
1409 + "linux"
1410 + ],
1411 + "engines": {
1412 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1413 + },
1414 + "funding": {
1415 + "url": "https://opencollective.com/libvips"
1416 + },
1417 + "optionalDependencies": {
1418 + "@img/sharp-libvips-linux-x64": "1.2.4"
1419 + }
1420 + },
1421 + "node_modules/@img/sharp-linuxmusl-arm64": {
1422 + "version": "0.34.5",
1423 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
1424 + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
1425 + "cpu": [
1426 + "arm64"
1427 + ],
1428 + "libc": [
1429 + "musl"
1430 + ],
1431 + "license": "Apache-2.0",
1432 + "optional": true,
1433 + "os": [
1434 + "linux"
1435 + ],
1436 + "engines": {
1437 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1438 + },
1439 + "funding": {
1440 + "url": "https://opencollective.com/libvips"
1441 + },
1442 + "optionalDependencies": {
1443 + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
1444 + }
1445 + },
1446 + "node_modules/@img/sharp-linuxmusl-x64": {
1447 + "version": "0.34.5",
1448 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
1449 + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
1450 + "cpu": [
1451 + "x64"
1452 + ],
1453 + "libc": [
1454 + "musl"
1455 + ],
1456 + "license": "Apache-2.0",
1457 + "optional": true,
1458 + "os": [
1459 + "linux"
1460 + ],
1461 + "engines": {
1462 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1463 + },
1464 + "funding": {
1465 + "url": "https://opencollective.com/libvips"
1466 + },
1467 + "optionalDependencies": {
1468 + "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
1469 + }
1470 + },
1471 + "node_modules/@img/sharp-wasm32": {
1472 + "version": "0.34.5",
1473 + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
1474 + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
1475 + "cpu": [
1476 + "wasm32"
1477 + ],
1478 + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
1479 + "optional": true,
1480 + "dependencies": {
1481 + "@emnapi/runtime": "^1.7.0"
1482 + },
1483 + "engines": {
1484 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1485 + },
1486 + "funding": {
1487 + "url": "https://opencollective.com/libvips"
1488 + }
1489 + },
1490 + "node_modules/@img/sharp-win32-arm64": {
1491 + "version": "0.34.5",
1492 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
1493 + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
1494 + "cpu": [
1495 + "arm64"
1496 + ],
1497 + "license": "Apache-2.0 AND LGPL-3.0-or-later",
1498 + "optional": true,
1499 + "os": [
1500 + "win32"
1501 + ],
1502 + "engines": {
1503 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1504 + },
1505 + "funding": {
1506 + "url": "https://opencollective.com/libvips"
1507 + }
1508 + },
1509 + "node_modules/@img/sharp-win32-ia32": {
1510 + "version": "0.34.5",
1511 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
1512 + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
1513 + "cpu": [
1514 + "ia32"
1515 + ],
1516 + "license": "Apache-2.0 AND LGPL-3.0-or-later",
1517 + "optional": true,
1518 + "os": [
1519 + "win32"
1520 + ],
1521 + "engines": {
1522 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1523 + },
1524 + "funding": {
1525 + "url": "https://opencollective.com/libvips"
1526 + }
1527 + },
1528 + "node_modules/@img/sharp-win32-x64": {
1529 + "version": "0.34.5",
1530 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
1531 + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
1532 + "cpu": [
1533 + "x64"
1534 + ],
1535 + "license": "Apache-2.0 AND LGPL-3.0-or-later",
1536 + "optional": true,
1537 + "os": [
1538 + "win32"
1539 + ],
1540 + "engines": {
1541 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1542 + },
1543 + "funding": {
1544 + "url": "https://opencollective.com/libvips"
1545 + }
1546 + },
1547 + "node_modules/@isaacs/cliui": {
1548 + "version": "9.0.0",
1549 + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
1550 + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
1551 + "license": "BlueOak-1.0.0",
1552 + "engines": {
1553 + "node": ">=18"
1554 + }
1555 + },
1556 + "node_modules/@jridgewell/sourcemap-codec": {
1557 + "version": "1.5.5",
1558 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
1559 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
1560 + "dev": true,
1561 + "license": "MIT"
1562 + },
1563 + "node_modules/@lukeed/ms": {
1564 + "version": "2.0.2",
1565 + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
1566 + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==",
1567 + "license": "MIT",
1568 + "engines": {
1569 + "node": ">=8"
1570 + }
1571 + },
1572 + "node_modules/@napi-rs/canvas": {
1573 + "version": "0.1.100",
1574 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz",
1575 + "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==",
1576 + "license": "MIT",
1577 + "optional": true,
1578 + "workspaces": [
1579 + "e2e/*"
1580 + ],
1581 + "engines": {
1582 + "node": ">= 10"
1583 + },
1584 + "funding": {
1585 + "type": "github",
1586 + "url": "https://github.com/sponsors/Brooooooklyn"
1587 + },
1588 + "optionalDependencies": {
1589 + "@napi-rs/canvas-android-arm64": "0.1.100",
1590 + "@napi-rs/canvas-darwin-arm64": "0.1.100",
1591 + "@napi-rs/canvas-darwin-x64": "0.1.100",
1592 + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100",
1593 + "@napi-rs/canvas-linux-arm64-gnu": "0.1.100",
1594 + "@napi-rs/canvas-linux-arm64-musl": "0.1.100",
1595 + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100",
1596 + "@napi-rs/canvas-linux-x64-gnu": "0.1.100",
1597 + "@napi-rs/canvas-linux-x64-musl": "0.1.100",
1598 + "@napi-rs/canvas-win32-arm64-msvc": "0.1.100",
1599 + "@napi-rs/canvas-win32-x64-msvc": "0.1.100"
1600 + }
1601 + },
1602 + "node_modules/@napi-rs/canvas-android-arm64": {
1603 + "version": "0.1.100",
1604 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz",
1605 + "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==",
1606 + "cpu": [
1607 + "arm64"
1608 + ],
1609 + "license": "MIT",
1610 + "optional": true,
1611 + "os": [
1612 + "android"
1613 + ],
1614 + "engines": {
1615 + "node": ">= 10"
1616 + },
1617 + "funding": {
1618 + "type": "github",
1619 + "url": "https://github.com/sponsors/Brooooooklyn"
1620 + }
1621 + },
1622 + "node_modules/@napi-rs/canvas-darwin-arm64": {
1623 + "version": "0.1.100",
1624 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz",
1625 + "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==",
1626 + "cpu": [
1627 + "arm64"
1628 + ],
1629 + "license": "MIT",
1630 + "optional": true,
1631 + "os": [
1632 + "darwin"
1633 + ],
1634 + "engines": {
1635 + "node": ">= 10"
1636 + },
1637 + "funding": {
1638 + "type": "github",
1639 + "url": "https://github.com/sponsors/Brooooooklyn"
1640 + }
1641 + },
1642 + "node_modules/@napi-rs/canvas-darwin-x64": {
1643 + "version": "0.1.100",
1644 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz",
1645 + "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==",
1646 + "cpu": [
1647 + "x64"
1648 + ],
1649 + "license": "MIT",
1650 + "optional": true,
1651 + "os": [
1652 + "darwin"
1653 + ],
1654 + "engines": {
1655 + "node": ">= 10"
1656 + },
1657 + "funding": {
1658 + "type": "github",
1659 + "url": "https://github.com/sponsors/Brooooooklyn"
1660 + }
1661 + },
1662 + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
1663 + "version": "0.1.100",
1664 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz",
1665 + "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==",
1666 + "cpu": [
1667 + "arm"
1668 + ],
1669 + "license": "MIT",
1670 + "optional": true,
1671 + "os": [
1672 + "linux"
1673 + ],
1674 + "engines": {
1675 + "node": ">= 10"
1676 + },
1677 + "funding": {
1678 + "type": "github",
1679 + "url": "https://github.com/sponsors/Brooooooklyn"
1680 + }
1681 + },
1682 + "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
1683 + "version": "0.1.100",
1684 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz",
1685 + "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==",
1686 + "cpu": [
1687 + "arm64"
1688 + ],
1689 + "libc": [
1690 + "glibc"
1691 + ],
1692 + "license": "MIT",
1693 + "optional": true,
1694 + "os": [
1695 + "linux"
1696 + ],
1697 + "engines": {
1698 + "node": ">= 10"
1699 + },
1700 + "funding": {
1701 + "type": "github",
1702 + "url": "https://github.com/sponsors/Brooooooklyn"
1703 + }
1704 + },
1705 + "node_modules/@napi-rs/canvas-linux-arm64-musl": {
1706 + "version": "0.1.100",
1707 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz",
1708 + "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==",
1709 + "cpu": [
1710 + "arm64"
1711 + ],
1712 + "libc": [
1713 + "musl"
1714 + ],
1715 + "license": "MIT",
1716 + "optional": true,
1717 + "os": [
1718 + "linux"
1719 + ],
1720 + "engines": {
1721 + "node": ">= 10"
1722 + },
1723 + "funding": {
1724 + "type": "github",
1725 + "url": "https://github.com/sponsors/Brooooooklyn"
1726 + }
1727 + },
1728 + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
1729 + "version": "0.1.100",
1730 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz",
1731 + "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==",
1732 + "cpu": [
1733 + "riscv64"
1734 + ],
1735 + "libc": [
1736 + "glibc"
1737 + ],
1738 + "license": "MIT",
1739 + "optional": true,
1740 + "os": [
1741 + "linux"
1742 + ],
1743 + "engines": {
1744 + "node": ">= 10"
1745 + },
1746 + "funding": {
1747 + "type": "github",
1748 + "url": "https://github.com/sponsors/Brooooooklyn"
1749 + }
1750 + },
1751 + "node_modules/@napi-rs/canvas-linux-x64-gnu": {
1752 + "version": "0.1.100",
1753 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz",
1754 + "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==",
1755 + "cpu": [
1756 + "x64"
1757 + ],
1758 + "libc": [
1759 + "glibc"
1760 + ],
1761 + "license": "MIT",
1762 + "optional": true,
1763 + "os": [
1764 + "linux"
1765 + ],
1766 + "engines": {
1767 + "node": ">= 10"
1768 + },
1769 + "funding": {
1770 + "type": "github",
1771 + "url": "https://github.com/sponsors/Brooooooklyn"
1772 + }
1773 + },
1774 + "node_modules/@napi-rs/canvas-linux-x64-musl": {
1775 + "version": "0.1.100",
1776 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz",
1777 + "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==",
1778 + "cpu": [
1779 + "x64"
1780 + ],
1781 + "libc": [
1782 + "musl"
1783 + ],
1784 + "license": "MIT",
1785 + "optional": true,
1786 + "os": [
1787 + "linux"
1788 + ],
1789 + "engines": {
1790 + "node": ">= 10"
1791 + },
1792 + "funding": {
1793 + "type": "github",
1794 + "url": "https://github.com/sponsors/Brooooooklyn"
1795 + }
1796 + },
1797 + "node_modules/@napi-rs/canvas-win32-arm64-msvc": {
1798 + "version": "0.1.100",
1799 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz",
1800 + "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==",
1801 + "cpu": [
1802 + "arm64"
1803 + ],
1804 + "license": "MIT",
1805 + "optional": true,
1806 + "os": [
1807 + "win32"
1808 + ],
1809 + "engines": {
1810 + "node": ">= 10"
1811 + },
1812 + "funding": {
1813 + "type": "github",
1814 + "url": "https://github.com/sponsors/Brooooooklyn"
1815 + }
1816 + },
1817 + "node_modules/@napi-rs/canvas-win32-x64-msvc": {
1818 + "version": "0.1.100",
1819 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz",
1820 + "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==",
1821 + "cpu": [
1822 + "x64"
1823 + ],
1824 + "license": "MIT",
1825 + "optional": true,
1826 + "os": [
1827 + "win32"
1828 + ],
1829 + "engines": {
1830 + "node": ">= 10"
1831 + },
1832 + "funding": {
1833 + "type": "github",
1834 + "url": "https://github.com/sponsors/Brooooooklyn"
1835 + }
1836 + },
1837 + "node_modules/@napi-rs/lzma-linux-x64-gnu": {
1838 + "version": "1.5.1",
1839 + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
1840 + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
1841 + "cpu": [
1842 + "x64"
1843 + ],
1844 + "dev": true,
1845 + "libc": [
1846 + "glibc"
1847 + ],
1848 + "license": "MIT",
1849 + "optional": true,
1850 + "os": [
1851 + "linux"
1852 + ],
1853 + "engines": {
1854 + "node": "^22.20 || ^24.12 || >=25"
1855 + }
1856 + },
1857 + "node_modules/@phc/format": {
1858 + "version": "1.0.0",
1859 + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
1860 + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==",
1861 + "license": "MIT",
1862 + "engines": {
1863 + "node": ">=10"
1864 + }
1865 + },
1866 + "node_modules/@pinojs/redact": {
1867 + "version": "0.4.0",
1868 + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
1869 + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
1870 + "license": "MIT"
1871 + },
1872 + "node_modules/@pkgjs/parseargs": {
1873 + "version": "0.11.0",
1874 + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
1875 + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
1876 + "license": "MIT",
1877 + "optional": true,
1878 + "engines": {
1879 + "node": ">=14"
1880 + }
1881 + },
1882 + "node_modules/@rollup/rollup-android-arm-eabi": {
1883 + "version": "4.62.4",
1884 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz",
1885 + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==",
1886 + "cpu": [
1887 + "arm"
1888 + ],
1889 + "dev": true,
1890 + "license": "MIT",
1891 + "optional": true,
1892 + "os": [
1893 + "android"
1894 + ]
1895 + },
1896 + "node_modules/@rollup/rollup-android-arm64": {
1897 + "version": "4.62.4",
1898 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz",
1899 + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==",
1900 + "cpu": [
1901 + "arm64"
1902 + ],
1903 + "dev": true,
1904 + "license": "MIT",
1905 + "optional": true,
1906 + "os": [
1907 + "android"
1908 + ]
1909 + },
1910 + "node_modules/@rollup/rollup-darwin-arm64": {
1911 + "version": "4.62.4",
1912 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz",
1913 + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==",
1914 + "cpu": [
1915 + "arm64"
1916 + ],
1917 + "dev": true,
1918 + "license": "MIT",
1919 + "optional": true,
1920 + "os": [
1921 + "darwin"
1922 + ]
1923 + },
1924 + "node_modules/@rollup/rollup-darwin-x64": {
1925 + "version": "4.62.4",
1926 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz",
1927 + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==",
1928 + "cpu": [
1929 + "x64"
1930 + ],
1931 + "dev": true,
1932 + "license": "MIT",
1933 + "optional": true,
1934 + "os": [
1935 + "darwin"
1936 + ]
1937 + },
1938 + "node_modules/@rollup/rollup-freebsd-arm64": {
1939 + "version": "4.62.4",
1940 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz",
1941 + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==",
1942 + "cpu": [
1943 + "arm64"
1944 + ],
1945 + "dev": true,
1946 + "license": "MIT",
1947 + "optional": true,
1948 + "os": [
1949 + "freebsd"
1950 + ]
1951 + },
1952 + "node_modules/@rollup/rollup-freebsd-x64": {
1953 + "version": "4.62.4",
1954 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz",
1955 + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==",
1956 + "cpu": [
1957 + "x64"
1958 + ],
1959 + "dev": true,
1960 + "license": "MIT",
1961 + "optional": true,
1962 + "os": [
1963 + "freebsd"
1964 + ]
1965 + },
1966 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
1967 + "version": "4.62.4",
1968 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz",
1969 + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==",
1970 + "cpu": [
1971 + "arm"
1972 + ],
1973 + "dev": true,
1974 + "libc": [
1975 + "glibc"
1976 + ],
1977 + "license": "MIT",
1978 + "optional": true,
1979 + "os": [
1980 + "linux"
1981 + ]
1982 + },
1983 + "node_modules/@rollup/rollup-linux-arm-musleabihf": {
1984 + "version": "4.62.4",
1985 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz",
1986 + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==",
1987 + "cpu": [
1988 + "arm"
1989 + ],
1990 + "dev": true,
1991 + "libc": [
1992 + "musl"
1993 + ],
1994 + "license": "MIT",
1995 + "optional": true,
1996 + "os": [
1997 + "linux"
1998 + ]
1999 + },
2000 + "node_modules/@rollup/rollup-linux-arm64-gnu": {
2001 + "version": "4.62.4",
2002 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz",
2003 + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==",
2004 + "cpu": [
2005 + "arm64"
2006 + ],
2007 + "dev": true,
2008 + "libc": [
2009 + "glibc"
2010 + ],
2011 + "license": "MIT",
2012 + "optional": true,
2013 + "os": [
2014 + "linux"
2015 + ]
2016 + },
2017 + "node_modules/@rollup/rollup-linux-arm64-musl": {
2018 + "version": "4.62.4",
2019 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz",
2020 + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==",
2021 + "cpu": [
2022 + "arm64"
2023 + ],
2024 + "dev": true,
2025 + "libc": [
2026 + "musl"
2027 + ],
2028 + "license": "MIT",
2029 + "optional": true,
2030 + "os": [
2031 + "linux"
2032 + ]
2033 + },
2034 + "node_modules/@rollup/rollup-linux-loong64-gnu": {
2035 + "version": "4.62.4",
2036 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz",
2037 + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==",
2038 + "cpu": [
2039 + "loong64"
2040 + ],
2041 + "dev": true,
2042 + "libc": [
2043 + "glibc"
2044 + ],
2045 + "license": "MIT",
2046 + "optional": true,
2047 + "os": [
2048 + "linux"
2049 + ]
2050 + },
2051 + "node_modules/@rollup/rollup-linux-loong64-musl": {
2052 + "version": "4.62.4",
2053 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz",
2054 + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==",
2055 + "cpu": [
2056 + "loong64"
2057 + ],
2058 + "dev": true,
2059 + "libc": [
2060 + "musl"
2061 + ],
2062 + "license": "MIT",
2063 + "optional": true,
2064 + "os": [
2065 + "linux"
2066 + ]
2067 + },
2068 + "node_modules/@rollup/rollup-linux-ppc64-gnu": {
2069 + "version": "4.62.4",
2070 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz",
2071 + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==",
2072 + "cpu": [
2073 + "ppc64"
2074 + ],
2075 + "dev": true,
2076 + "libc": [
2077 + "glibc"
2078 + ],
2079 + "license": "MIT",
2080 + "optional": true,
2081 + "os": [
2082 + "linux"
2083 + ]
2084 + },
2085 + "node_modules/@rollup/rollup-linux-ppc64-musl": {
2086 + "version": "4.62.4",
2087 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz",
2088 + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==",
2089 + "cpu": [
2090 + "ppc64"
2091 + ],
2092 + "dev": true,
2093 + "libc": [
2094 + "musl"
2095 + ],
2096 + "license": "MIT",
2097 + "optional": true,
2098 + "os": [
2099 + "linux"
2100 + ]
2101 + },
2102 + "node_modules/@rollup/rollup-linux-riscv64-gnu": {
2103 + "version": "4.62.4",
2104 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz",
2105 + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==",
2106 + "cpu": [
2107 + "riscv64"
2108 + ],
2109 + "dev": true,
2110 + "libc": [
2111 + "glibc"
2112 + ],
2113 + "license": "MIT",
2114 + "optional": true,
2115 + "os": [
2116 + "linux"
2117 + ]
2118 + },
2119 + "node_modules/@rollup/rollup-linux-riscv64-musl": {
2120 + "version": "4.62.4",
2121 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz",
2122 + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==",
2123 + "cpu": [
2124 + "riscv64"
2125 + ],
2126 + "dev": true,
2127 + "libc": [
2128 + "musl"
2129 + ],
2130 + "license": "MIT",
2131 + "optional": true,
2132 + "os": [
2133 + "linux"
2134 + ]
2135 + },
2136 + "node_modules/@rollup/rollup-linux-s390x-gnu": {
2137 + "version": "4.62.4",
2138 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz",
2139 + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==",
2140 + "cpu": [
2141 + "s390x"
2142 + ],
2143 + "dev": true,
2144 + "libc": [
2145 + "glibc"
2146 + ],
2147 + "license": "MIT",
2148 + "optional": true,
2149 + "os": [
2150 + "linux"
2151 + ]
2152 + },
2153 + "node_modules/@rollup/rollup-linux-x64-gnu": {
2154 + "version": "4.62.4",
2155 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz",
2156 + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==",
2157 + "cpu": [
2158 + "x64"
2159 + ],
2160 + "dev": true,
2161 + "libc": [
2162 + "glibc"
2163 + ],
2164 + "license": "MIT",
2165 + "optional": true,
2166 + "os": [
2167 + "linux"
2168 + ]
2169 + },
2170 + "node_modules/@rollup/rollup-linux-x64-musl": {
2171 + "version": "4.62.4",
2172 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz",
2173 + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==",
2174 + "cpu": [
2175 + "x64"
2176 + ],
2177 + "dev": true,
2178 + "libc": [
2179 + "musl"
2180 + ],
2181 + "license": "MIT",
2182 + "optional": true,
2183 + "os": [
2184 + "linux"
2185 + ]
2186 + },
2187 + "node_modules/@rollup/rollup-openbsd-x64": {
2188 + "version": "4.62.4",
2189 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz",
2190 + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==",
2191 + "cpu": [
2192 + "x64"
2193 + ],
2194 + "dev": true,
2195 + "license": "MIT",
2196 + "optional": true,
2197 + "os": [
2198 + "openbsd"
2199 + ]
2200 + },
2201 + "node_modules/@rollup/rollup-openharmony-arm64": {
2202 + "version": "4.62.4",
2203 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz",
2204 + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==",
2205 + "cpu": [
2206 + "arm64"
2207 + ],
2208 + "dev": true,
2209 + "license": "MIT",
2210 + "optional": true,
2211 + "os": [
2212 + "openharmony"
2213 + ]
2214 + },
2215 + "node_modules/@rollup/rollup-win32-arm64-msvc": {
2216 + "version": "4.62.4",
2217 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz",
2218 + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==",
2219 + "cpu": [
2220 + "arm64"
2221 + ],
2222 + "dev": true,
2223 + "license": "MIT",
2224 + "optional": true,
2225 + "os": [
2226 + "win32"
2227 + ]
2228 + },
2229 + "node_modules/@rollup/rollup-win32-ia32-msvc": {
2230 + "version": "4.62.4",
2231 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz",
2232 + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==",
2233 + "cpu": [
2234 + "ia32"
2235 + ],
2236 + "dev": true,
2237 + "license": "MIT",
2238 + "optional": true,
2239 + "os": [
2240 + "win32"
2241 + ]
2242 + },
2243 + "node_modules/@rollup/rollup-win32-x64-gnu": {
2244 + "version": "4.62.4",
2245 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz",
2246 + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==",
2247 + "cpu": [
2248 + "x64"
2249 + ],
2250 + "dev": true,
2251 + "license": "MIT",
2252 + "optional": true,
2253 + "os": [
2254 + "win32"
2255 + ]
2256 + },
2257 + "node_modules/@rollup/rollup-win32-x64-msvc": {
2258 + "version": "4.62.4",
2259 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz",
2260 + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==",
2261 + "cpu": [
2262 + "x64"
2263 + ],
2264 + "dev": true,
2265 + "license": "MIT",
2266 + "optional": true,
2267 + "os": [
2268 + "win32"
2269 + ]
2270 + },
2271 + "node_modules/@shikijs/core": {
2272 + "version": "3.23.0",
2273 + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz",
2274 + "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==",
2275 + "license": "MIT",
2276 + "dependencies": {
2277 + "@shikijs/types": "3.23.0",
2278 + "@shikijs/vscode-textmate": "^10.0.2",
2279 + "@types/hast": "^3.0.4",
2280 + "hast-util-to-html": "^9.0.5"
2281 + }
2282 + },
2283 + "node_modules/@shikijs/engine-javascript": {
2284 + "version": "3.23.0",
2285 + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz",
2286 + "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==",
2287 + "license": "MIT",
2288 + "dependencies": {
2289 + "@shikijs/types": "3.23.0",
2290 + "@shikijs/vscode-textmate": "^10.0.2",
2291 + "oniguruma-to-es": "^4.3.4"
2292 + }
2293 + },
2294 + "node_modules/@shikijs/engine-oniguruma": {
2295 + "version": "3.23.0",
2296 + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz",
2297 + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==",
2298 + "license": "MIT",
2299 + "dependencies": {
2300 + "@shikijs/types": "3.23.0",
2301 + "@shikijs/vscode-textmate": "^10.0.2"
2302 + }
2303 + },
2304 + "node_modules/@shikijs/langs": {
2305 + "version": "3.23.0",
2306 + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz",
2307 + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==",
2308 + "license": "MIT",
2309 + "dependencies": {
2310 + "@shikijs/types": "3.23.0"
2311 + }
2312 + },
2313 + "node_modules/@shikijs/themes": {
2314 + "version": "3.23.0",
2315 + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz",
2316 + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==",
2317 + "license": "MIT",
2318 + "dependencies": {
2319 + "@shikijs/types": "3.23.0"
2320 + }
2321 + },
2322 + "node_modules/@shikijs/types": {
2323 + "version": "3.23.0",
2324 + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz",
2325 + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==",
2326 + "license": "MIT",
2327 + "dependencies": {
2328 + "@shikijs/vscode-textmate": "^10.0.2",
2329 + "@types/hast": "^3.0.4"
2330 + }
2331 + },
2332 + "node_modules/@shikijs/vscode-textmate": {
2333 + "version": "10.0.2",
2334 + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
2335 + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
2336 + "license": "MIT"
2337 + },
2338 + "node_modules/@types/chai": {
2339 + "version": "5.2.3",
2340 + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
2341 + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
2342 + "dev": true,
2343 + "license": "MIT",
2344 + "dependencies": {
2345 + "@types/deep-eql": "*",
2346 + "assertion-error": "^2.0.1"
2347 + }
2348 + },
2349 + "node_modules/@types/deep-eql": {
2350 + "version": "4.0.2",
2351 + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
2352 + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
2353 + "dev": true,
2354 + "license": "MIT"
2355 + },
2356 + "node_modules/@types/estree": {
2357 + "version": "1.0.9",
2358 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
2359 + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
2360 + "dev": true,
2361 + "license": "MIT"
2362 + },
2363 + "node_modules/@types/hast": {
2364 + "version": "3.0.5",
2365 + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
2366 + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
2367 + "license": "MIT",
2368 + "dependencies": {
2369 + "@types/unist": "*"
2370 + }
2371 + },
2372 + "node_modules/@types/json-schema": {
2373 + "version": "7.0.15",
2374 + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
2375 + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
2376 + "dev": true,
2377 + "license": "MIT"
2378 + },
2379 + "node_modules/@types/linkify-it": {
2380 + "version": "5.0.0",
2381 + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
2382 + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
2383 + "license": "MIT",
2384 + "peer": true
2385 + },
2386 + "node_modules/@types/markdown-it": {
2387 + "version": "14.1.2",
2388 + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
2389 + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
2390 + "license": "MIT",
2391 + "peer": true,
2392 + "dependencies": {
2393 + "@types/linkify-it": "^5",
2394 + "@types/mdurl": "^2"
2395 + }
2396 + },
2397 + "node_modules/@types/mdast": {
2398 + "version": "4.0.4",
2399 + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
2400 + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
2401 + "license": "MIT",
2402 + "dependencies": {
2403 + "@types/unist": "*"
2404 + }
2405 + },
2406 + "node_modules/@types/mdurl": {
2407 + "version": "2.0.0",
2408 + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
2409 + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
2410 + "license": "MIT",
2411 + "peer": true
2412 + },
2413 + "node_modules/@types/unist": {
2414 + "version": "3.0.3",
2415 + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
2416 + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
2417 + "license": "MIT"
2418 + },
2419 + "node_modules/@ungap/structured-clone": {
2420 + "version": "1.3.3",
2421 + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
2422 + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==",
2423 + "license": "ISC"
2424 + },
2425 + "node_modules/@vitest/expect": {
2426 + "version": "3.2.7",
2427 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
2428 + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
2429 + "dev": true,
2430 + "license": "MIT",
2431 + "dependencies": {
2432 + "@types/chai": "^5.2.2",
2433 + "@vitest/spy": "3.2.7",
2434 + "@vitest/utils": "3.2.7",
2435 + "chai": "^5.2.0",
2436 + "tinyrainbow": "^2.0.0"
2437 + },
2438 + "funding": {
2439 + "url": "https://opencollective.com/vitest"
2440 + }
2441 + },
2442 + "node_modules/@vitest/mocker": {
2443 + "version": "3.2.7",
2444 + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
2445 + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
2446 + "dev": true,
2447 + "license": "MIT",
2448 + "dependencies": {
2449 + "@vitest/spy": "3.2.7",
2450 + "estree-walker": "^3.0.3",
2451 + "magic-string": "^0.30.17"
2452 + },
2453 + "funding": {
2454 + "url": "https://opencollective.com/vitest"
2455 + },
2456 + "peerDependencies": {
2457 + "msw": "^2.4.9",
2458 + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
2459 + },
2460 + "peerDependenciesMeta": {
2461 + "msw": {
2462 + "optional": true
2463 + },
2464 + "vite": {
2465 + "optional": true
2466 + }
2467 + }
2468 + },
2469 + "node_modules/@vitest/pretty-format": {
2470 + "version": "3.2.7",
2471 + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
2472 + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
2473 + "dev": true,
2474 + "license": "MIT",
2475 + "dependencies": {
2476 + "tinyrainbow": "^2.0.0"
2477 + },
2478 + "funding": {
2479 + "url": "https://opencollective.com/vitest"
2480 + }
2481 + },
2482 + "node_modules/@vitest/runner": {
2483 + "version": "3.2.7",
2484 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
2485 + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
2486 + "dev": true,
2487 + "license": "MIT",
2488 + "dependencies": {
2489 + "@vitest/utils": "3.2.7",
2490 + "pathe": "^2.0.3",
2491 + "strip-literal": "^3.0.0"
2492 + },
2493 + "funding": {
2494 + "url": "https://opencollective.com/vitest"
2495 + }
2496 + },
2497 + "node_modules/@vitest/snapshot": {
2498 + "version": "3.2.7",
2499 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
2500 + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
2501 + "dev": true,
2502 + "license": "MIT",
2503 + "dependencies": {
2504 + "@vitest/pretty-format": "3.2.7",
2505 + "magic-string": "^0.30.17",
2506 + "pathe": "^2.0.3"
2507 + },
2508 + "funding": {
2509 + "url": "https://opencollective.com/vitest"
2510 + }
2511 + },
2512 + "node_modules/@vitest/spy": {
2513 + "version": "3.2.7",
2514 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
2515 + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
2516 + "dev": true,
2517 + "license": "MIT",
2518 + "dependencies": {
2519 + "tinyspy": "^4.0.3"
2520 + },
2521 + "funding": {
2522 + "url": "https://opencollective.com/vitest"
2523 + }
2524 + },
2525 + "node_modules/@vitest/utils": {
2526 + "version": "3.2.7",
2527 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
2528 + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
2529 + "dev": true,
2530 + "license": "MIT",
2531 + "dependencies": {
2532 + "@vitest/pretty-format": "3.2.7",
2533 + "loupe": "^3.1.4",
2534 + "tinyrainbow": "^2.0.0"
2535 + },
2536 + "funding": {
2537 + "url": "https://opencollective.com/vitest"
2538 + }
2539 + },
2540 + "node_modules/a-sync-waterfall": {
2541 + "version": "1.0.1",
2542 + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz",
2543 + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==",
2544 + "license": "MIT"
2545 + },
2546 + "node_modules/abort-controller": {
2547 + "version": "3.0.0",
2548 + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
2549 + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
2550 + "license": "MIT",
2551 + "dependencies": {
2552 + "event-target-shim": "^5.0.0"
2553 + },
2554 + "engines": {
2555 + "node": ">=6.5"
2556 + }
2557 + },
2558 + "node_modules/abstract-logging": {
2559 + "version": "2.0.1",
2560 + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
2561 + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==",
2562 + "license": "MIT"
2563 + },
2564 + "node_modules/acorn": {
2565 + "version": "8.18.0",
2566 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
2567 + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
2568 + "dev": true,
2569 + "license": "MIT",
2570 + "bin": {
2571 + "acorn": "bin/acorn"
2572 + },
2573 + "engines": {
2574 + "node": ">=0.4.0"
2575 + }
2576 + },
2577 + "node_modules/acorn-jsx": {
2578 + "version": "5.3.2",
2579 + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
2580 + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
2581 + "dev": true,
2582 + "license": "MIT",
2583 + "peerDependencies": {
2584 + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
2585 + }
2586 + },
2587 + "node_modules/adler-32": {
2588 + "version": "1.3.1",
2589 + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
2590 + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
2591 + "license": "Apache-2.0",
2592 + "engines": {
2593 + "node": ">=0.8"
2594 + }
2595 + },
2596 + "node_modules/ajv": {
2597 + "version": "6.15.0",
2598 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
2599 + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
2600 + "dev": true,
2601 + "license": "MIT",
2602 + "dependencies": {
2603 + "fast-deep-equal": "^3.1.1",
2604 + "fast-json-stable-stringify": "^2.0.0",
2605 + "json-schema-traverse": "^0.4.1",
2606 + "uri-js": "^4.2.2"
2607 + },
2608 + "funding": {
2609 + "type": "github",
2610 + "url": "https://github.com/sponsors/epoberezkin"
2611 + }
2612 + },
2613 + "node_modules/ajv-formats": {
2614 + "version": "3.0.1",
2615 + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
2616 + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
2617 + "license": "MIT",
2618 + "dependencies": {
2619 + "ajv": "^8.0.0"
2620 + },
2621 + "peerDependencies": {
2622 + "ajv": "^8.0.0"
2623 + },
2624 + "peerDependenciesMeta": {
2625 + "ajv": {
2626 + "optional": true
2627 + }
2628 + }
2629 + },
2630 + "node_modules/ajv-formats/node_modules/ajv": {
2631 + "version": "8.20.0",
2632 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
2633 + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
2634 + "license": "MIT",
2635 + "dependencies": {
2636 + "fast-deep-equal": "^3.1.3",
2637 + "fast-uri": "^3.0.1",
2638 + "json-schema-traverse": "^1.0.0",
2639 + "require-from-string": "^2.0.2"
2640 + },
2641 + "funding": {
2642 + "type": "github",
2643 + "url": "https://github.com/sponsors/epoberezkin"
2644 + }
2645 + },
2646 + "node_modules/ajv-formats/node_modules/fast-uri": {
2647 + "version": "3.1.5",
2648 + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
2649 + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
2650 + "funding": [
2651 + {
2652 + "type": "github",
2653 + "url": "https://github.com/sponsors/fastify"
2654 + },
2655 + {
2656 + "type": "opencollective",
2657 + "url": "https://opencollective.com/fastify"
2658 + }
2659 + ],
2660 + "license": "BSD-3-Clause"
2661 + },
2662 + "node_modules/ajv-formats/node_modules/json-schema-traverse": {
2663 + "version": "1.0.0",
2664 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
2665 + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
2666 + "license": "MIT"
2667 + },
2668 + "node_modules/ansi-regex": {
2669 + "version": "5.0.1",
2670 + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
2671 + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
2672 + "license": "MIT",
2673 + "engines": {
2674 + "node": ">=8"
2675 + }
2676 + },
2677 + "node_modules/ansi-styles": {
2678 + "version": "4.3.0",
2679 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
2680 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
2681 + "license": "MIT",
2682 + "dependencies": {
2683 + "color-convert": "^2.0.1"
2684 + },
2685 + "engines": {
2686 + "node": ">=8"
2687 + },
2688 + "funding": {
2689 + "url": "https://github.com/chalk/ansi-styles?sponsor=1"
2690 + }
2691 + },
2692 + "node_modules/archiver": {
2693 + "version": "7.0.1",
2694 + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz",
2695 + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==",
2696 + "license": "MIT",
2697 + "dependencies": {
2698 + "archiver-utils": "^5.0.2",
2699 + "async": "^3.2.4",
2700 + "buffer-crc32": "^1.0.0",
2701 + "readable-stream": "^4.0.0",
2702 + "readdir-glob": "^1.1.2",
2703 + "tar-stream": "^3.0.0",
2704 + "zip-stream": "^6.0.1"
2705 + },
2706 + "engines": {
2707 + "node": ">= 14"
2708 + }
2709 + },
2710 + "node_modules/archiver-utils": {
2711 + "version": "5.0.2",
2712 + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz",
2713 + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==",
2714 + "license": "MIT",
2715 + "dependencies": {
2716 + "glob": "^10.0.0",
2717 + "graceful-fs": "^4.2.0",
2718 + "is-stream": "^2.0.1",
2719 + "lazystream": "^1.0.0",
2720 + "lodash": "^4.17.15",
2721 + "normalize-path": "^3.0.0",
2722 + "readable-stream": "^4.0.0"
2723 + },
2724 + "engines": {
2725 + "node": ">= 14"
2726 + }
2727 + },
2728 + "node_modules/archiver-utils/node_modules/@isaacs/cliui": {
2729 + "version": "8.0.2",
2730 + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
2731 + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
2732 + "license": "ISC",
2733 + "dependencies": {
2734 + "string-width": "^5.1.2",
2735 + "string-width-cjs": "npm:string-width@^4.2.0",
2736 + "strip-ansi": "^7.0.1",
2737 + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
2738 + "wrap-ansi": "^8.1.0",
2739 + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
2740 + },
2741 + "engines": {
2742 + "node": ">=12"
2743 + }
2744 + },
2745 + "node_modules/archiver-utils/node_modules/ansi-regex": {
2746 + "version": "6.2.2",
2747 + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
2748 + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
2749 + "license": "MIT",
2750 + "engines": {
2751 + "node": ">=12"
2752 + },
2753 + "funding": {
2754 + "url": "https://github.com/chalk/ansi-regex?sponsor=1"
2755 + }
2756 + },
2757 + "node_modules/archiver-utils/node_modules/ansi-styles": {
2758 + "version": "6.2.3",
2759 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
2760 + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
2761 + "license": "MIT",
2762 + "engines": {
2763 + "node": ">=12"
2764 + },
2765 + "funding": {
2766 + "url": "https://github.com/chalk/ansi-styles?sponsor=1"
2767 + }
2768 + },
2769 + "node_modules/archiver-utils/node_modules/brace-expansion": {
2770 + "version": "2.1.4",
2771 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
2772 + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
2773 + "license": "MIT",
2774 + "dependencies": {
2775 + "balanced-match": "^1.0.0"
2776 + }
2777 + },
2778 + "node_modules/archiver-utils/node_modules/emoji-regex": {
2779 + "version": "9.2.2",
2780 + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
2781 + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
2782 + "license": "MIT"
2783 + },
2784 + "node_modules/archiver-utils/node_modules/glob": {
2785 + "version": "10.5.0",
2786 + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
2787 + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
2788 + "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",
2789 + "license": "ISC",
2790 + "dependencies": {
2791 + "foreground-child": "^3.1.0",
2792 + "jackspeak": "^3.1.2",
2793 + "minimatch": "^9.0.4",
2794 + "minipass": "^7.1.2",
2795 + "package-json-from-dist": "^1.0.0",
2796 + "path-scurry": "^1.11.1"
2797 + },
2798 + "bin": {
2799 + "glob": "dist/esm/bin.mjs"
2800 + },
2801 + "funding": {
2802 + "url": "https://github.com/sponsors/isaacs"
2803 + }
2804 + },
2805 + "node_modules/archiver-utils/node_modules/jackspeak": {
2806 + "version": "3.4.3",
2807 + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
2808 + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
2809 + "license": "BlueOak-1.0.0",
2810 + "dependencies": {
2811 + "@isaacs/cliui": "^8.0.2"
2812 + },
2813 + "funding": {
2814 + "url": "https://github.com/sponsors/isaacs"
2815 + },
2816 + "optionalDependencies": {
2817 + "@pkgjs/parseargs": "^0.11.0"
2818 + }
2819 + },
2820 + "node_modules/archiver-utils/node_modules/lru-cache": {
2821 + "version": "10.4.3",
2822 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
2823 + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
2824 + "license": "ISC"
2825 + },
2826 + "node_modules/archiver-utils/node_modules/minimatch": {
2827 + "version": "9.0.9",
2828 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
2829 + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
2830 + "license": "ISC",
2831 + "dependencies": {
2832 + "brace-expansion": "^2.0.2"
2833 + },
2834 + "engines": {
2835 + "node": ">=16 || 14 >=14.17"
2836 + },
2837 + "funding": {
2838 + "url": "https://github.com/sponsors/isaacs"
2839 + }
2840 + },
2841 + "node_modules/archiver-utils/node_modules/path-scurry": {
2842 + "version": "1.11.1",
2843 + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
2844 + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
2845 + "license": "BlueOak-1.0.0",
2846 + "dependencies": {
2847 + "lru-cache": "^10.2.0",
2848 + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
2849 + },
2850 + "engines": {
2851 + "node": ">=16 || 14 >=14.18"
2852 + },
2853 + "funding": {
2854 + "url": "https://github.com/sponsors/isaacs"
2855 + }
2856 + },
2857 + "node_modules/archiver-utils/node_modules/string-width": {
2858 + "version": "5.1.2",
2859 + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
2860 + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
2861 + "license": "MIT",
2862 + "dependencies": {
2863 + "eastasianwidth": "^0.2.0",
2864 + "emoji-regex": "^9.2.2",
2865 + "strip-ansi": "^7.0.1"
2866 + },
2867 + "engines": {
2868 + "node": ">=12"
2869 + },
2870 + "funding": {
2871 + "url": "https://github.com/sponsors/sindresorhus"
2872 + }
2873 + },
2874 + "node_modules/archiver-utils/node_modules/strip-ansi": {
2875 + "version": "7.2.0",
2876 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
2877 + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
2878 + "license": "MIT",
2879 + "dependencies": {
2880 + "ansi-regex": "^6.2.2"
2881 + },
2882 + "engines": {
2883 + "node": ">=12"
2884 + },
2885 + "funding": {
2886 + "url": "https://github.com/chalk/strip-ansi?sponsor=1"
2887 + }
2888 + },
2889 + "node_modules/archiver-utils/node_modules/wrap-ansi": {
2890 + "version": "8.1.0",
2891 + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
2892 + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
2893 + "license": "MIT",
2894 + "dependencies": {
2895 + "ansi-styles": "^6.1.0",
2896 + "string-width": "^5.0.1",
2897 + "strip-ansi": "^7.0.1"
2898 + },
2899 + "engines": {
2900 + "node": ">=12"
2901 + },
2902 + "funding": {
2903 + "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
2904 + }
2905 + },
2906 + "node_modules/argon2": {
2907 + "version": "0.44.0",
2908 + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.44.0.tgz",
2909 + "integrity": "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==",
2910 + "hasInstallScript": true,
2911 + "license": "MIT",
2912 + "dependencies": {
2913 + "@phc/format": "^1.0.0",
2914 + "cross-env": "^10.0.0",
2915 + "node-addon-api": "^8.5.0",
2916 + "node-gyp-build": "^4.8.4"
2917 + },
2918 + "engines": {
2919 + "node": ">=16.17.0"
2920 + }
2921 + },
2922 + "node_modules/argparse": {
2923 + "version": "2.0.1",
2924 + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
2925 + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
2926 + "license": "Python-2.0"
2927 + },
2928 + "node_modules/asap": {
2929 + "version": "2.0.6",
2930 + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
2931 + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
2932 + "license": "MIT"
2933 + },
2934 + "node_modules/assertion-error": {
2935 + "version": "2.0.1",
2936 + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
2937 + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
2938 + "dev": true,
2939 + "license": "MIT",
2940 + "engines": {
2941 + "node": ">=12"
2942 + }
2943 + },
2944 + "node_modules/async": {
2945 + "version": "3.2.6",
2946 + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
2947 + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
2948 + "license": "MIT"
2949 + },
2950 + "node_modules/atomic-sleep": {
2951 + "version": "1.0.0",
2952 + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
2953 + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
2954 + "license": "MIT",
2955 + "engines": {
2956 + "node": ">=8.0.0"
2957 + }
2958 + },
2959 + "node_modules/avvio": {
2960 + "version": "9.3.0",
2961 + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz",
2962 + "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==",
2963 + "funding": [
2964 + {
2965 + "type": "github",
2966 + "url": "https://github.com/sponsors/fastify"
2967 + },
2968 + {
2969 + "type": "opencollective",
2970 + "url": "https://opencollective.com/fastify"
2971 + }
2972 + ],
2973 + "license": "MIT",
2974 + "dependencies": {
2975 + "@fastify/error": "^4.0.0",
2976 + "fastq": "^1.17.1"
2977 + }
2978 + },
2979 + "node_modules/b4a": {
2980 + "version": "1.8.1",
2981 + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
2982 + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
2983 + "license": "Apache-2.0",
2984 + "peerDependencies": {
2985 + "react-native-b4a": "*"
2986 + },
2987 + "peerDependenciesMeta": {
2988 + "react-native-b4a": {
2989 + "optional": true
2990 + }
2991 + }
2992 + },
2993 + "node_modules/balanced-match": {
2994 + "version": "1.0.2",
2995 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
2996 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
2997 + "license": "MIT"
2998 + },
2999 + "node_modules/bare-events": {
3000 + "version": "2.9.1",
3001 + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz",
3002 + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==",
3003 + "license": "Apache-2.0",
3004 + "peerDependencies": {
3005 + "bare-abort-controller": "*"
3006 + },
3007 + "peerDependenciesMeta": {
3008 + "bare-abort-controller": {
3009 + "optional": true
3010 + }
3011 + }
3012 + },
3013 + "node_modules/bare-fs": {
3014 + "version": "4.8.0",
3015 + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.0.tgz",
3016 + "integrity": "sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==",
3017 + "license": "Apache-2.0",
3018 + "dependencies": {
3019 + "bare-events": "^2.5.4",
3020 + "bare-path": "^3.0.0",
3021 + "bare-stream": "^2.6.4",
3022 + "bare-url": "^2.2.2",
3023 + "fast-fifo": "^1.3.2"
3024 + },
3025 + "engines": {
3026 + "bare": ">=1.28.0"
3027 + },
3028 + "peerDependencies": {
3029 + "bare-buffer": "*"
3030 + },
3031 + "peerDependenciesMeta": {
3032 + "bare-buffer": {
3033 + "optional": true
3034 + }
3035 + }
3036 + },
3037 + "node_modules/bare-path": {
3038 + "version": "3.1.1",
3039 + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz",
3040 + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==",
3041 + "license": "Apache-2.0"
3042 + },
3043 + "node_modules/bare-stream": {
3044 + "version": "2.13.3",
3045 + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz",
3046 + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==",
3047 + "license": "Apache-2.0",
3048 + "dependencies": {
3049 + "b4a": "^1.8.1",
3050 + "streamx": "^2.25.0",
3051 + "teex": "^1.0.1"
3052 + },
3053 + "peerDependencies": {
3054 + "bare-abort-controller": "*",
3055 + "bare-buffer": "*",
3056 + "bare-events": "*"
3057 + },
3058 + "peerDependenciesMeta": {
3059 + "bare-abort-controller": {
3060 + "optional": true
3061 + },
3062 + "bare-buffer": {
3063 + "optional": true
3064 + },
3065 + "bare-events": {
3066 + "optional": true
3067 + }
3068 + }
3069 + },
3070 + "node_modules/bare-url": {
3071 + "version": "2.5.1",
3072 + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.1.tgz",
3073 + "integrity": "sha512-cD5ciQuKlx+eumTCfqbfiL+fhQm+dHbVNB/cX4+d+I/nx4JsVop9VoFEPAs5RJ6I84QR6bZIBjpd2kjDOYeWcg==",
3074 + "license": "Apache-2.0",
3075 + "dependencies": {
3076 + "bare-path": "^3.0.0"
3077 + }
3078 + },
3079 + "node_modules/base64-js": {
3080 + "version": "1.5.1",
3081 + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
3082 + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
3083 + "funding": [
3084 + {
3085 + "type": "github",
3086 + "url": "https://github.com/sponsors/feross"
3087 + },
3088 + {
3089 + "type": "patreon",
3090 + "url": "https://www.patreon.com/feross"
3091 + },
3092 + {
3093 + "type": "consulting",
3094 + "url": "https://feross.org/support"
3095 + }
3096 + ],
3097 + "license": "MIT"
3098 + },
3099 + "node_modules/better-sqlite3": {
3100 + "version": "12.11.1",
3101 + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz",
3102 + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==",
3103 + "hasInstallScript": true,
3104 + "license": "MIT",
3105 + "dependencies": {
3106 + "bindings": "^1.5.0",
3107 + "prebuild-install": "^7.1.1"
3108 + },
3109 + "engines": {
3110 + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x"
3111 + }
3112 + },
3113 + "node_modules/bindings": {
3114 + "version": "1.5.0",
3115 + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
3116 + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
3117 + "license": "MIT",
3118 + "dependencies": {
3119 + "file-uri-to-path": "1.0.0"
3120 + }
3121 + },
3122 + "node_modules/bl": {
3123 + "version": "4.1.0",
3124 + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
3125 + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
3126 + "license": "MIT",
3127 + "dependencies": {
3128 + "buffer": "^5.5.0",
3129 + "inherits": "^2.0.4",
3130 + "readable-stream": "^3.4.0"
3131 + }
3132 + },
3133 + "node_modules/bl/node_modules/buffer": {
3134 + "version": "5.7.1",
3135 + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
3136 + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
3137 + "funding": [
3138 + {
3139 + "type": "github",
3140 + "url": "https://github.com/sponsors/feross"
3141 + },
3142 + {
3143 + "type": "patreon",
3144 + "url": "https://www.patreon.com/feross"
3145 + },
3146 + {
3147 + "type": "consulting",
3148 + "url": "https://feross.org/support"
3149 + }
3150 + ],
3151 + "license": "MIT",
3152 + "dependencies": {
3153 + "base64-js": "^1.3.1",
3154 + "ieee754": "^1.1.13"
3155 + }
3156 + },
3157 + "node_modules/bl/node_modules/readable-stream": {
3158 + "version": "3.6.2",
3159 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
3160 + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
3161 + "license": "MIT",
3162 + "dependencies": {
3163 + "inherits": "^2.0.3",
3164 + "string_decoder": "^1.1.1",
3165 + "util-deprecate": "^1.0.1"
3166 + },
3167 + "engines": {
3168 + "node": ">= 6"
3169 + }
3170 + },
3171 + "node_modules/brace-expansion": {
3172 + "version": "1.1.18",
3173 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
3174 + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
3175 + "dev": true,
3176 + "license": "MIT",
3177 + "dependencies": {
3178 + "balanced-match": "^1.0.0",
3179 + "concat-map": "0.0.1"
3180 + }
3181 + },
3182 + "node_modules/buffer": {
3183 + "version": "6.0.3",
3184 + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
3185 + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
3186 + "funding": [
3187 + {
3188 + "type": "github",
3189 + "url": "https://github.com/sponsors/feross"
3190 + },
3191 + {
3192 + "type": "patreon",
3193 + "url": "https://www.patreon.com/feross"
3194 + },
3195 + {
3196 + "type": "consulting",
3197 + "url": "https://feross.org/support"
3198 + }
3199 + ],
3200 + "license": "MIT",
3201 + "dependencies": {
3202 + "base64-js": "^1.3.1",
3203 + "ieee754": "^1.2.1"
3204 + }
3205 + },
3206 + "node_modules/buffer-crc32": {
3207 + "version": "1.0.0",
3208 + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
3209 + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
3210 + "license": "MIT",
3211 + "engines": {
3212 + "node": ">=8.0.0"
3213 + }
3214 + },
3215 + "node_modules/cac": {
3216 + "version": "6.7.14",
3217 + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
3218 + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
3219 + "dev": true,
3220 + "license": "MIT",
3221 + "engines": {
3222 + "node": ">=8"
3223 + }
3224 + },
3225 + "node_modules/callsites": {
3226 + "version": "3.1.0",
3227 + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
3228 + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
3229 + "dev": true,
3230 + "license": "MIT",
3231 + "engines": {
3232 + "node": ">=6"
3233 + }
3234 + },
3235 + "node_modules/camelcase": {
3236 + "version": "5.3.1",
3237 + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
3238 + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
3239 + "license": "MIT",
3240 + "engines": {
3241 + "node": ">=6"
3242 + }
3243 + },
3244 + "node_modules/ccount": {
3245 + "version": "2.0.1",
3246 + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
3247 + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
3248 + "license": "MIT",
3249 + "funding": {
3250 + "type": "github",
3251 + "url": "https://github.com/sponsors/wooorm"
3252 + }
3253 + },
3254 + "node_modules/cfb": {
3255 + "version": "1.2.2",
3256 + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
3257 + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
3258 + "license": "Apache-2.0",
3259 + "dependencies": {
3260 + "adler-32": "~1.3.0",
3261 + "crc-32": "~1.2.0"
3262 + },
3263 + "engines": {
3264 + "node": ">=0.8"
3265 + }
3266 + },
3267 + "node_modules/chai": {
3268 + "version": "5.3.3",
3269 + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
3270 + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
3271 + "dev": true,
3272 + "license": "MIT",
3273 + "dependencies": {
3274 + "assertion-error": "^2.0.1",
3275 + "check-error": "^2.1.1",
3276 + "deep-eql": "^5.0.1",
3277 + "loupe": "^3.1.0",
3278 + "pathval": "^2.0.0"
3279 + },
3280 + "engines": {
3281 + "node": ">=18"
3282 + }
3283 + },
3284 + "node_modules/chalk": {
3285 + "version": "4.1.2",
3286 + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
3287 + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
3288 + "dev": true,
3289 + "license": "MIT",
3290 + "dependencies": {
3291 + "ansi-styles": "^4.1.0",
3292 + "supports-color": "^7.1.0"
3293 + },
3294 + "engines": {
3295 + "node": ">=10"
3296 + },
3297 + "funding": {
3298 + "url": "https://github.com/chalk/chalk?sponsor=1"
3299 + }
3300 + },
3301 + "node_modules/character-entities-html4": {
3302 + "version": "2.1.0",
3303 + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
3304 + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
3305 + "license": "MIT",
3306 + "funding": {
3307 + "type": "github",
3308 + "url": "https://github.com/sponsors/wooorm"
3309 + }
3310 + },
3311 + "node_modules/character-entities-legacy": {
3312 + "version": "3.0.0",
3313 + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
3314 + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
3315 + "license": "MIT",
3316 + "funding": {
3317 + "type": "github",
3318 + "url": "https://github.com/sponsors/wooorm"
3319 + }
3320 + },
3321 + "node_modules/check-error": {
3322 + "version": "2.1.3",
3323 + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
3324 + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
3325 + "dev": true,
3326 + "license": "MIT",
3327 + "engines": {
3328 + "node": ">= 16"
3329 + }
3330 + },
3331 + "node_modules/chownr": {
3332 + "version": "1.1.4",
3333 + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
3334 + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
3335 + "license": "ISC"
3336 + },
3337 + "node_modules/cliui": {
3338 + "version": "6.0.0",
3339 + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
3340 + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
3341 + "license": "ISC",
3342 + "dependencies": {
3343 + "string-width": "^4.2.0",
3344 + "strip-ansi": "^6.0.0",
3345 + "wrap-ansi": "^6.2.0"
3346 + }
3347 + },
3348 + "node_modules/codepage": {
3349 + "version": "1.15.0",
3350 + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
3351 + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
3352 + "license": "Apache-2.0",
3353 + "engines": {
3354 + "node": ">=0.8"
3355 + }
3356 + },
3357 + "node_modules/color-convert": {
3358 + "version": "2.0.1",
3359 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
3360 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
3361 + "license": "MIT",
3362 + "dependencies": {
3363 + "color-name": "~1.1.4"
3364 + },
3365 + "engines": {
3366 + "node": ">=7.0.0"
3367 + }
3368 + },
3369 + "node_modules/color-name": {
3370 + "version": "1.1.4",
3371 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
3372 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
3373 + "license": "MIT"
3374 + },
3375 + "node_modules/colorette": {
3376 + "version": "2.0.20",
3377 + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
3378 + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
3379 + "dev": true,
3380 + "license": "MIT"
3381 + },
3382 + "node_modules/comma-separated-tokens": {
3383 + "version": "2.0.3",
3384 + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
3385 + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
3386 + "license": "MIT",
3387 + "funding": {
3388 + "type": "github",
3389 + "url": "https://github.com/sponsors/wooorm"
3390 + }
3391 + },
3392 + "node_modules/commander": {
3393 + "version": "14.0.3",
3394 + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
3395 + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
3396 + "license": "MIT",
3397 + "engines": {
3398 + "node": ">=20"
3399 + }
3400 + },
3401 + "node_modules/compress-commons": {
3402 + "version": "6.0.2",
3403 + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
3404 + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==",
3405 + "license": "MIT",
3406 + "dependencies": {
3407 + "crc-32": "^1.2.0",
3408 + "crc32-stream": "^6.0.0",
3409 + "is-stream": "^2.0.1",
3410 + "normalize-path": "^3.0.0",
3411 + "readable-stream": "^4.0.0"
3412 + },
3413 + "engines": {
3414 + "node": ">= 14"
3415 + }
3416 + },
3417 + "node_modules/concat-map": {
3418 + "version": "0.0.1",
3419 + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
3420 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
3421 + "dev": true,
3422 + "license": "MIT"
3423 + },
3424 + "node_modules/content-disposition": {
3425 + "version": "0.5.4",
3426 + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
3427 + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
3428 + "license": "MIT",
3429 + "dependencies": {
3430 + "safe-buffer": "5.2.1"
3431 + },
3432 + "engines": {
3433 + "node": ">= 0.6"
3434 + }
3435 + },
3436 + "node_modules/cookie": {
3437 + "version": "2.0.1",
3438 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz",
3439 + "integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==",
3440 + "license": "MIT",
3441 + "engines": {
3442 + "node": ">=22"
3443 + },
3444 + "funding": {
3445 + "type": "opencollective",
3446 + "url": "https://opencollective.com/express"
3447 + }
3448 + },
3449 + "node_modules/core-util-is": {
3450 + "version": "1.0.3",
3451 + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
3452 + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
3453 + "license": "MIT"
3454 + },
3455 + "node_modules/crc-32": {
3456 + "version": "1.2.2",
3457 + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
3458 + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
3459 + "license": "Apache-2.0",
3460 + "bin": {
3461 + "crc32": "bin/crc32.njs"
3462 + },
3463 + "engines": {
3464 + "node": ">=0.8"
3465 + }
3466 + },
3467 + "node_modules/crc32-stream": {
3468 + "version": "6.0.0",
3469 + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz",
3470 + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==",
3471 + "license": "MIT",
3472 + "dependencies": {
3473 + "crc-32": "^1.2.0",
3474 + "readable-stream": "^4.0.0"
3475 + },
3476 + "engines": {
3477 + "node": ">= 14"
3478 + }
3479 + },
3480 + "node_modules/cross-env": {
3481 + "version": "10.1.0",
3482 + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
3483 + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
3484 + "license": "MIT",
3485 + "dependencies": {
3486 + "@epic-web/invariant": "^1.0.0",
3487 + "cross-spawn": "^7.0.6"
3488 + },
3489 + "bin": {
3490 + "cross-env": "dist/bin/cross-env.js",
3491 + "cross-env-shell": "dist/bin/cross-env-shell.js"
3492 + },
3493 + "engines": {
3494 + "node": ">=20"
3495 + }
3496 + },
3497 + "node_modules/cross-spawn": {
3498 + "version": "7.0.6",
3499 + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
3500 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
3501 + "license": "MIT",
3502 + "dependencies": {
3503 + "path-key": "^3.1.0",
3504 + "shebang-command": "^2.0.0",
3505 + "which": "^2.0.1"
3506 + },
3507 + "engines": {
3508 + "node": ">= 8"
3509 + }
3510 + },
3511 + "node_modules/dateformat": {
3512 + "version": "4.6.3",
3513 + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
3514 + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
3515 + "dev": true,
3516 + "license": "MIT",
3517 + "engines": {
3518 + "node": "*"
3519 + }
3520 + },
3521 + "node_modules/debug": {
3522 + "version": "4.4.3",
3523 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
3524 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
3525 + "dev": true,
3526 + "license": "MIT",
3527 + "dependencies": {
3528 + "ms": "^2.1.3"
3529 + },
3530 + "engines": {
3531 + "node": ">=6.0"
3532 + },
3533 + "peerDependenciesMeta": {
3534 + "supports-color": {
3535 + "optional": true
3536 + }
3537 + }
3538 + },
3539 + "node_modules/decamelize": {
3540 + "version": "1.2.0",
3541 + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
3542 + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
3543 + "license": "MIT",
3544 + "engines": {
3545 + "node": ">=0.10.0"
3546 + }
3547 + },
3548 + "node_modules/decompress-response": {
3549 + "version": "6.0.0",
3550 + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
3551 + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
3552 + "license": "MIT",
3553 + "dependencies": {
3554 + "mimic-response": "^3.1.0"
3555 + },
3556 + "engines": {
3557 + "node": ">=10"
3558 + },
3559 + "funding": {
3560 + "url": "https://github.com/sponsors/sindresorhus"
3561 + }
3562 + },
3563 + "node_modules/deep-eql": {
3564 + "version": "5.0.2",
3565 + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
3566 + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
3567 + "dev": true,
3568 + "license": "MIT",
3569 + "engines": {
3570 + "node": ">=6"
3571 + }
3572 + },
3573 + "node_modules/deep-extend": {
3574 + "version": "0.6.0",
3575 + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
3576 + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
3577 + "license": "MIT",
3578 + "engines": {
3579 + "node": ">=4.0.0"
3580 + }
3581 + },
3582 + "node_modules/deep-is": {
3583 + "version": "0.1.4",
3584 + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
3585 + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
3586 + "dev": true,
3587 + "license": "MIT"
3588 + },
3589 + "node_modules/depd": {
3590 + "version": "2.0.0",
3591 + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
3592 + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
3593 + "license": "MIT",
3594 + "engines": {
3595 + "node": ">= 0.8"
3596 + }
3597 + },
3598 + "node_modules/dequal": {
3599 + "version": "2.0.3",
3600 + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
3601 + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
3602 + "license": "MIT",
3603 + "engines": {
3604 + "node": ">=6"
3605 + }
3606 + },
3607 + "node_modules/detect-libc": {
3608 + "version": "2.1.2",
3609 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
3610 + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
3611 + "license": "Apache-2.0",
3612 + "engines": {
3613 + "node": ">=8"
3614 + }
3615 + },
3616 + "node_modules/devlop": {
3617 + "version": "1.1.0",
3618 + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
3619 + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
3620 + "license": "MIT",
3621 + "dependencies": {
3622 + "dequal": "^2.0.0"
3623 + },
3624 + "funding": {
3625 + "type": "github",
3626 + "url": "https://github.com/sponsors/wooorm"
3627 + }
3628 + },
3629 + "node_modules/dijkstrajs": {
3630 + "version": "1.0.3",
3631 + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
3632 + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
3633 + "license": "MIT"
3634 + },
3635 + "node_modules/eastasianwidth": {
3636 + "version": "0.2.0",
3637 + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
3638 + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
3639 + "license": "MIT"
3640 + },
3641 + "node_modules/emoji-regex": {
3642 + "version": "8.0.0",
3643 + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
3644 + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
3645 + "license": "MIT"
3646 + },
3647 + "node_modules/end-of-stream": {
3648 + "version": "1.4.5",
3649 + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
3650 + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
3651 + "license": "MIT",
3652 + "dependencies": {
3653 + "once": "^1.4.0"
3654 + }
3655 + },
3656 + "node_modules/entities": {
3657 + "version": "4.5.0",
3658 + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
3659 + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
3660 + "license": "BSD-2-Clause",
3661 + "engines": {
3662 + "node": ">=0.12"
3663 + },
3664 + "funding": {
3665 + "url": "https://github.com/fb55/entities?sponsor=1"
3666 + }
3667 + },
3668 + "node_modules/es-module-lexer": {
3669 + "version": "1.7.0",
3670 + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
3671 + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
3672 + "dev": true,
3673 + "license": "MIT"
3674 + },
3675 + "node_modules/esbuild": {
3676 + "version": "0.28.2",
3677 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
3678 + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
3679 + "dev": true,
3680 + "hasInstallScript": true,
3681 + "license": "MIT",
3682 + "bin": {
3683 + "esbuild": "bin/esbuild"
3684 + },
3685 + "engines": {
3686 + "node": ">=18"
3687 + },
3688 + "optionalDependencies": {
3689 + "@esbuild/aix-ppc64": "0.28.2",
3690 + "@esbuild/android-arm": "0.28.2",
3691 + "@esbuild/android-arm64": "0.28.2",
3692 + "@esbuild/android-x64": "0.28.2",
3693 + "@esbuild/darwin-arm64": "0.28.2",
3694 + "@esbuild/darwin-x64": "0.28.2",
3695 + "@esbuild/freebsd-arm64": "0.28.2",
3696 + "@esbuild/freebsd-x64": "0.28.2",
3697 + "@esbuild/linux-arm": "0.28.2",
3698 + "@esbuild/linux-arm64": "0.28.2",
3699 + "@esbuild/linux-ia32": "0.28.2",
3700 + "@esbuild/linux-loong64": "0.28.2",
3701 + "@esbuild/linux-mips64el": "0.28.2",
3702 + "@esbuild/linux-ppc64": "0.28.2",
3703 + "@esbuild/linux-riscv64": "0.28.2",
3704 + "@esbuild/linux-s390x": "0.28.2",
3705 + "@esbuild/linux-x64": "0.28.2",
3706 + "@esbuild/netbsd-arm64": "0.28.2",
3707 + "@esbuild/netbsd-x64": "0.28.2",
3708 + "@esbuild/openbsd-arm64": "0.28.2",
3709 + "@esbuild/openbsd-x64": "0.28.2",
3710 + "@esbuild/openharmony-arm64": "0.28.2",
3711 + "@esbuild/sunos-x64": "0.28.2",
3712 + "@esbuild/win32-arm64": "0.28.2",
3713 + "@esbuild/win32-ia32": "0.28.2",
3714 + "@esbuild/win32-x64": "0.28.2"
3715 + }
3716 + },
3717 + "node_modules/escape-html": {
3718 + "version": "1.0.3",
3719 + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
3720 + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
3721 + "license": "MIT"
3722 + },
3723 + "node_modules/escape-string-regexp": {
3724 + "version": "4.0.0",
3725 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
3726 + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
3727 + "dev": true,
3728 + "license": "MIT",
3729 + "engines": {
3730 + "node": ">=10"
3731 + },
3732 + "funding": {
3733 + "url": "https://github.com/sponsors/sindresorhus"
3734 + }
3735 + },
3736 + "node_modules/eslint": {
3737 + "version": "9.39.5",
3738 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz",
3739 + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==",
3740 + "dev": true,
3741 + "license": "MIT",
3742 + "dependencies": {
3743 + "@eslint-community/eslint-utils": "^4.8.0",
3744 + "@eslint-community/regexpp": "^4.12.1",
3745 + "@eslint/config-array": "^0.21.2",
3746 + "@eslint/config-helpers": "^0.4.2",
3747 + "@eslint/core": "^0.17.0",
3748 + "@eslint/eslintrc": "^3.3.6",
3749 + "@eslint/js": "9.39.5",
3750 + "@eslint/plugin-kit": "^0.4.1",
3751 + "@humanfs/node": "^0.16.6",
3752 + "@humanwhocodes/module-importer": "^1.0.1",
3753 + "@humanwhocodes/retry": "^0.4.2",
3754 + "@types/estree": "^1.0.6",
3755 + "ajv": "^6.14.0",
3756 + "chalk": "^4.0.0",
3757 + "cross-spawn": "^7.0.6",
3758 + "debug": "^4.3.2",
3759 + "escape-string-regexp": "^4.0.0",
3760 + "eslint-scope": "^8.4.0",
3761 + "eslint-visitor-keys": "^4.2.1",
3762 + "espree": "^10.4.0",
3763 + "esquery": "^1.5.0",
3764 + "esutils": "^2.0.2",
3765 + "fast-deep-equal": "^3.1.3",
3766 + "file-entry-cache": "^8.0.0",
3767 + "find-up": "^5.0.0",
3768 + "glob-parent": "^6.0.2",
3769 + "ignore": "^5.2.0",
3770 + "imurmurhash": "^0.1.4",
3771 + "is-glob": "^4.0.0",
3772 + "json-stable-stringify-without-jsonify": "^1.0.1",
3773 + "lodash.merge": "^4.6.2",
3774 + "minimatch": "^3.1.5",
3775 + "natural-compare": "^1.4.0",
3776 + "optionator": "^0.9.3"
3777 + },
3778 + "bin": {
3779 + "eslint": "bin/eslint.js"
3780 + },
3781 + "engines": {
3782 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
3783 + },
3784 + "funding": {
3785 + "url": "https://eslint.org/donate"
3786 + },
3787 + "peerDependencies": {
3788 + "jiti": "*"
3789 + },
3790 + "peerDependenciesMeta": {
3791 + "jiti": {
3792 + "optional": true
3793 + }
3794 + }
3795 + },
3796 + "node_modules/eslint-scope": {
3797 + "version": "8.4.0",
3798 + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
3799 + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
3800 + "dev": true,
3801 + "license": "BSD-2-Clause",
3802 + "dependencies": {
3803 + "esrecurse": "^4.3.0",
3804 + "estraverse": "^5.2.0"
3805 + },
3806 + "engines": {
3807 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
3808 + },
3809 + "funding": {
3810 + "url": "https://opencollective.com/eslint"
3811 + }
3812 + },
3813 + "node_modules/eslint-visitor-keys": {
3814 + "version": "4.2.1",
3815 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
3816 + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
3817 + "dev": true,
3818 + "license": "Apache-2.0",
3819 + "engines": {
3820 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
3821 + },
3822 + "funding": {
3823 + "url": "https://opencollective.com/eslint"
3824 + }
3825 + },
3826 + "node_modules/espree": {
3827 + "version": "10.4.0",
3828 + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
3829 + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
3830 + "dev": true,
3831 + "license": "BSD-2-Clause",
3832 + "dependencies": {
3833 + "acorn": "^8.15.0",
3834 + "acorn-jsx": "^5.3.2",
3835 + "eslint-visitor-keys": "^4.2.1"
3836 + },
3837 + "engines": {
3838 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
3839 + },
3840 + "funding": {
3841 + "url": "https://opencollective.com/eslint"
3842 + }
3843 + },
3844 + "node_modules/esquery": {
3845 + "version": "1.7.0",
3846 + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
3847 + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
3848 + "dev": true,
3849 + "license": "BSD-3-Clause",
3850 + "dependencies": {
3851 + "estraverse": "^5.1.0"
3852 + },
3853 + "engines": {
3854 + "node": ">=0.10"
3855 + }
3856 + },
3857 + "node_modules/esrecurse": {
3858 + "version": "4.3.0",
3859 + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
3860 + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
3861 + "dev": true,
3862 + "license": "BSD-2-Clause",
3863 + "dependencies": {
3864 + "estraverse": "^5.2.0"
3865 + },
3866 + "engines": {
3867 + "node": ">=4.0"
3868 + }
3869 + },
3870 + "node_modules/estraverse": {
3871 + "version": "5.3.0",
3872 + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
3873 + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
3874 + "dev": true,
3875 + "license": "BSD-2-Clause",
3876 + "engines": {
3877 + "node": ">=4.0"
3878 + }
3879 + },
3880 + "node_modules/estree-walker": {
3881 + "version": "3.0.3",
3882 + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
3883 + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
3884 + "dev": true,
3885 + "license": "MIT",
3886 + "dependencies": {
3887 + "@types/estree": "^1.0.0"
3888 + }
3889 + },
3890 + "node_modules/esutils": {
3891 + "version": "2.0.3",
3892 + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
3893 + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
3894 + "dev": true,
3895 + "license": "BSD-2-Clause",
3896 + "engines": {
3897 + "node": ">=0.10.0"
3898 + }
3899 + },
3900 + "node_modules/event-target-shim": {
3901 + "version": "5.0.1",
3902 + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
3903 + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
3904 + "license": "MIT",
3905 + "engines": {
3906 + "node": ">=6"
3907 + }
3908 + },
3909 + "node_modules/events": {
3910 + "version": "3.3.0",
3911 + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
3912 + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
3913 + "license": "MIT",
3914 + "engines": {
3915 + "node": ">=0.8.x"
3916 + }
3917 + },
3918 + "node_modules/events-universal": {
3919 + "version": "1.0.1",
3920 + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
3921 + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
3922 + "license": "Apache-2.0",
3923 + "dependencies": {
3924 + "bare-events": "^2.7.0"
3925 + }
3926 + },
3927 + "node_modules/exif-reader": {
3928 + "version": "2.0.3",
3929 + "resolved": "https://registry.npmjs.org/exif-reader/-/exif-reader-2.0.3.tgz",
3930 + "integrity": "sha512-zFbQvguwT9JkqyYhR7pjE1Yn8SagwaGLNRU0Oh14xFa1paSf5Gzxn4gxgk0XhnudI0UIqU+HgnBX93+nva592A==",
3931 + "license": "MIT"
3932 + },
3933 + "node_modules/expand-template": {
3934 + "version": "2.0.3",
3935 + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
3936 + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
3937 + "license": "(MIT OR WTFPL)",
3938 + "engines": {
3939 + "node": ">=6"
3940 + }
3941 + },
3942 + "node_modules/expect-type": {
3943 + "version": "1.4.0",
3944 + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
3945 + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
3946 + "dev": true,
3947 + "license": "Apache-2.0",
3948 + "engines": {
3949 + "node": ">=12.0.0"
3950 + }
3951 + },
3952 + "node_modules/fast-copy": {
3953 + "version": "4.0.4",
3954 + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.4.tgz",
3955 + "integrity": "sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA==",
3956 + "dev": true,
3957 + "license": "MIT"
3958 + },
3959 + "node_modules/fast-decode-uri-component": {
3960 + "version": "1.0.1",
3961 + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
3962 + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==",
3963 + "license": "MIT"
3964 + },
3965 + "node_modules/fast-deep-equal": {
3966 + "version": "3.1.3",
3967 + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
3968 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
3969 + "license": "MIT"
3970 + },
3971 + "node_modules/fast-fifo": {
3972 + "version": "1.3.2",
3973 + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
3974 + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
3975 + "license": "MIT"
3976 + },
3977 + "node_modules/fast-json-stable-stringify": {
3978 + "version": "2.1.0",
3979 + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
3980 + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
3981 + "dev": true,
3982 + "license": "MIT"
3983 + },
3984 + "node_modules/fast-json-stringify": {
3985 + "version": "7.0.1",
3986 + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz",
3987 + "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==",
3988 + "funding": [
3989 + {
3990 + "type": "github",
3991 + "url": "https://github.com/sponsors/fastify"
3992 + },
3993 + {
3994 + "type": "opencollective",
3995 + "url": "https://opencollective.com/fastify"
3996 + }
3997 + ],
3998 + "license": "MIT",
3999 + "dependencies": {
4000 + "@fastify/merge-json-schemas": "^0.2.0",
4001 + "ajv": "^8.12.0",
4002 + "ajv-formats": "^3.0.1",
4003 + "fast-uri": "^4.0.0",
4004 + "json-schema-ref-resolver": "^3.0.0",
4005 + "rfdc": "^1.2.0"
4006 + }
4007 + },
4008 + "node_modules/fast-json-stringify/node_modules/ajv": {
4009 + "version": "8.20.0",
4010 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
4011 + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
4012 + "license": "MIT",
4013 + "dependencies": {
4014 + "fast-deep-equal": "^3.1.3",
4015 + "fast-uri": "^3.0.1",
4016 + "json-schema-traverse": "^1.0.0",
4017 + "require-from-string": "^2.0.2"
4018 + },
4019 + "funding": {
4020 + "type": "github",
4021 + "url": "https://github.com/sponsors/epoberezkin"
4022 + }
4023 + },
4024 + "node_modules/fast-json-stringify/node_modules/ajv/node_modules/fast-uri": {
4025 + "version": "3.1.5",
4026 + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
4027 + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
4028 + "funding": [
4029 + {
4030 + "type": "github",
4031 + "url": "https://github.com/sponsors/fastify"
4032 + },
4033 + {
4034 + "type": "opencollective",
4035 + "url": "https://opencollective.com/fastify"
4036 + }
4037 + ],
4038 + "license": "BSD-3-Clause"
4039 + },
4040 + "node_modules/fast-json-stringify/node_modules/json-schema-traverse": {
4041 + "version": "1.0.0",
4042 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
4043 + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
4044 + "license": "MIT"
4045 + },
4046 + "node_modules/fast-levenshtein": {
4047 + "version": "2.0.6",
4048 + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
4049 + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
4050 + "dev": true,
4051 + "license": "MIT"
4052 + },
4053 + "node_modules/fast-querystring": {
4054 + "version": "1.1.2",
4055 + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz",
4056 + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==",
4057 + "license": "MIT",
4058 + "dependencies": {
4059 + "fast-decode-uri-component": "^1.0.1"
4060 + }
4061 + },
4062 + "node_modules/fast-safe-stringify": {
4063 + "version": "2.1.1",
4064 + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
4065 + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
4066 + "dev": true,
4067 + "license": "MIT"
4068 + },
4069 + "node_modules/fast-uri": {
4070 + "version": "4.1.2",
4071 + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.2.tgz",
4072 + "integrity": "sha512-TyGmBcbDTZXcb2cj5MV89DrF42DKvb3y5DDUNh95iO+IMeAzMkVSxK1PZRrRIpc9yg8U2GhGdbofNa0LS/a4Bw==",
4073 + "funding": [
4074 + {
4075 + "type": "github",
4076 + "url": "https://github.com/sponsors/fastify"
4077 + },
4078 + {
4079 + "type": "opencollective",
4080 + "url": "https://opencollective.com/fastify"
4081 + }
4082 + ],
4083 + "license": "BSD-3-Clause"
4084 + },
4085 + "node_modules/fastify": {
4086 + "version": "5.11.3",
4087 + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.11.3.tgz",
4088 + "integrity": "sha512-W6hzDP8s0iSeL7LGwY6Oc/ZxuXWOvFEMs6p2L0Si415YRo27W5pBKdOTXxhemBDeSTAcpYf5evRA9onF2OYhPA==",
4089 + "funding": [
4090 + {
4091 + "type": "github",
4092 + "url": "https://github.com/sponsors/fastify"
4093 + },
4094 + {
4095 + "type": "opencollective",
4096 + "url": "https://opencollective.com/fastify"
4097 + }
4098 + ],
4099 + "license": "MIT",
4100 + "dependencies": {
4101 + "@fastify/ajv-compiler": "^4.0.5",
4102 + "@fastify/error": "^4.0.0",
4103 + "@fastify/fast-json-stringify-compiler": "^5.0.0",
4104 + "@fastify/proxy-addr": "^5.0.0",
4105 + "abstract-logging": "^2.0.1",
4106 + "avvio": "^9.0.0",
4107 + "fast-json-stringify": "^7.0.0",
4108 + "find-my-way": "^9.6.0",
4109 + "light-my-request": "^6.0.0",
4110 + "pino": "^9.14.0 || ^10.1.0",
4111 + "process-warning": "^5.0.0",
4112 + "rfdc": "^1.3.1",
4113 + "secure-json-parse": "^4.0.0",
4114 + "semver": "^7.6.0",
4115 + "toad-cache": "^3.7.0"
4116 + }
4117 + },
4118 + "node_modules/fastify-plugin": {
4119 + "version": "6.0.0",
4120 + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.0.tgz",
4121 + "integrity": "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==",
4122 + "funding": [
4123 + {
4124 + "type": "github",
4125 + "url": "https://github.com/sponsors/fastify"
4126 + },
4127 + {
4128 + "type": "opencollective",
4129 + "url": "https://opencollective.com/fastify"
4130 + }
4131 + ],
4132 + "license": "MIT"
4133 + },
4134 + "node_modules/fastq": {
4135 + "version": "1.20.1",
4136 + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
4137 + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
4138 + "license": "ISC",
4139 + "dependencies": {
4140 + "reusify": "^1.0.4"
4141 + }
4142 + },
4143 + "node_modules/fdir": {
4144 + "version": "6.5.0",
4145 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
4146 + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
4147 + "dev": true,
4148 + "license": "MIT",
4149 + "engines": {
4150 + "node": ">=12.0.0"
4151 + },
4152 + "peerDependencies": {
4153 + "picomatch": "^3 || ^4"
4154 + },
4155 + "peerDependenciesMeta": {
4156 + "picomatch": {
4157 + "optional": true
4158 + }
4159 + }
4160 + },
4161 + "node_modules/file-entry-cache": {
4162 + "version": "8.0.0",
4163 + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
4164 + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
4165 + "dev": true,
4166 + "license": "MIT",
4167 + "dependencies": {
4168 + "flat-cache": "^4.0.0"
4169 + },
4170 + "engines": {
4171 + "node": ">=16.0.0"
4172 + }
4173 + },
4174 + "node_modules/file-uri-to-path": {
4175 + "version": "1.0.0",
4176 + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
4177 + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
4178 + "license": "MIT"
4179 + },
4180 + "node_modules/find-my-way": {
4181 + "version": "9.7.0",
4182 + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz",
4183 + "integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==",
4184 + "license": "MIT",
4185 + "dependencies": {
4186 + "fast-deep-equal": "^3.1.3",
4187 + "fast-querystring": "^1.0.0",
4188 + "safe-regex2": "^5.0.0"
4189 + },
4190 + "engines": {
4191 + "node": ">=20"
4192 + }
4193 + },
4194 + "node_modules/find-up": {
4195 + "version": "5.0.0",
4196 + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
4197 + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
4198 + "dev": true,
4199 + "license": "MIT",
4200 + "dependencies": {
4201 + "locate-path": "^6.0.0",
4202 + "path-exists": "^4.0.0"
4203 + },
4204 + "engines": {
4205 + "node": ">=10"
4206 + },
4207 + "funding": {
4208 + "url": "https://github.com/sponsors/sindresorhus"
4209 + }
4210 + },
4211 + "node_modules/flat-cache": {
4212 + "version": "4.0.1",
4213 + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
4214 + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
4215 + "dev": true,
4216 + "license": "MIT",
4217 + "dependencies": {
4218 + "flatted": "^3.2.9",
4219 + "keyv": "^4.5.4"
4220 + },
4221 + "engines": {
4222 + "node": ">=16"
4223 + }
4224 + },
4225 + "node_modules/flatted": {
4226 + "version": "3.4.4",
4227 + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
4228 + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
4229 + "dev": true,
4230 + "license": "ISC"
4231 + },
4232 + "node_modules/foreground-child": {
4233 + "version": "3.3.1",
4234 + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
4235 + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
4236 + "license": "ISC",
4237 + "dependencies": {
4238 + "cross-spawn": "^7.0.6",
4239 + "signal-exit": "^4.0.1"
4240 + },
4241 + "engines": {
4242 + "node": ">=14"
4243 + },
4244 + "funding": {
4245 + "url": "https://github.com/sponsors/isaacs"
4246 + }
4247 + },
4248 + "node_modules/frac": {
4249 + "version": "1.1.2",
4250 + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
4251 + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
4252 + "license": "Apache-2.0",
4253 + "engines": {
4254 + "node": ">=0.8"
4255 + }
4256 + },
4257 + "node_modules/fs-constants": {
4258 + "version": "1.0.0",
4259 + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
4260 + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
4261 + "license": "MIT"
4262 + },
4263 + "node_modules/fsevents": {
4264 + "version": "2.3.3",
4265 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
4266 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
4267 + "dev": true,
4268 + "hasInstallScript": true,
4269 + "license": "MIT",
4270 + "optional": true,
4271 + "os": [
4272 + "darwin"
4273 + ],
4274 + "engines": {
4275 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
4276 + }
4277 + },
4278 + "node_modules/get-caller-file": {
4279 + "version": "2.0.5",
4280 + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
4281 + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
4282 + "license": "ISC",
4283 + "engines": {
4284 + "node": "6.* || 8.* || >= 10.*"
4285 + }
4286 + },
4287 + "node_modules/github-from-package": {
4288 + "version": "0.0.0",
4289 + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
4290 + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
4291 + "license": "MIT"
4292 + },
4293 + "node_modules/glob": {
4294 + "version": "11.1.0",
4295 + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
4296 + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
4297 + "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",
4298 + "license": "BlueOak-1.0.0",
4299 + "dependencies": {
4300 + "foreground-child": "^3.3.1",
4301 + "jackspeak": "^4.1.1",
4302 + "minimatch": "^10.1.1",
4303 + "minipass": "^7.1.2",
4304 + "package-json-from-dist": "^1.0.0",
4305 + "path-scurry": "^2.0.0"
4306 + },
4307 + "bin": {
4308 + "glob": "dist/esm/bin.mjs"
4309 + },
4310 + "engines": {
4311 + "node": "20 || >=22"
4312 + },
4313 + "funding": {
4314 + "url": "https://github.com/sponsors/isaacs"
4315 + }
4316 + },
4317 + "node_modules/glob-parent": {
4318 + "version": "6.0.2",
4319 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
4320 + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
4321 + "dev": true,
4322 + "license": "ISC",
4323 + "dependencies": {
4324 + "is-glob": "^4.0.3"
4325 + },
4326 + "engines": {
4327 + "node": ">=10.13.0"
4328 + }
4329 + },
4330 + "node_modules/glob/node_modules/balanced-match": {
4331 + "version": "4.0.4",
4332 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
4333 + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
4334 + "license": "MIT",
4335 + "engines": {
4336 + "node": "18 || 20 || >=22"
4337 + }
4338 + },
4339 + "node_modules/glob/node_modules/brace-expansion": {
4340 + "version": "5.0.9",
4341 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
4342 + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
4343 + "license": "MIT",
4344 + "dependencies": {
4345 + "balanced-match": "^4.0.2"
4346 + },
4347 + "engines": {
4348 + "node": "20 || >=22"
4349 + }
4350 + },
4351 + "node_modules/glob/node_modules/minimatch": {
4352 + "version": "10.2.6",
4353 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
4354 + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
4355 + "license": "BlueOak-1.0.0",
4356 + "dependencies": {
4357 + "brace-expansion": "^5.0.8"
4358 + },
4359 + "engines": {
4360 + "node": "18 || 20 || >=22"
4361 + },
4362 + "funding": {
4363 + "url": "https://github.com/sponsors/isaacs"
4364 + }
4365 + },
4366 + "node_modules/globals": {
4367 + "version": "14.0.0",
4368 + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
4369 + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
4370 + "dev": true,
4371 + "license": "MIT",
4372 + "engines": {
4373 + "node": ">=18"
4374 + },
4375 + "funding": {
4376 + "url": "https://github.com/sponsors/sindresorhus"
4377 + }
4378 + },
4379 + "node_modules/graceful-fs": {
4380 + "version": "4.2.11",
4381 + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
4382 + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
4383 + "license": "ISC"
4384 + },
4385 + "node_modules/has-flag": {
4386 + "version": "4.0.0",
4387 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
4388 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
4389 + "dev": true,
4390 + "license": "MIT",
4391 + "engines": {
4392 + "node": ">=8"
4393 + }
4394 + },
4395 + "node_modules/hast-util-to-html": {
4396 + "version": "9.0.5",
4397 + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
4398 + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
4399 + "license": "MIT",
4400 + "dependencies": {
4401 + "@types/hast": "^3.0.0",
4402 + "@types/unist": "^3.0.0",
4403 + "ccount": "^2.0.0",
4404 + "comma-separated-tokens": "^2.0.0",
4405 + "hast-util-whitespace": "^3.0.0",
4406 + "html-void-elements": "^3.0.0",
4407 + "mdast-util-to-hast": "^13.0.0",
4408 + "property-information": "^7.0.0",
4409 + "space-separated-tokens": "^2.0.0",
4410 + "stringify-entities": "^4.0.0",
4411 + "zwitch": "^2.0.4"
4412 + },
4413 + "funding": {
4414 + "type": "opencollective",
4415 + "url": "https://opencollective.com/unified"
4416 + }
4417 + },
4418 + "node_modules/hast-util-whitespace": {
4419 + "version": "3.0.0",
4420 + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
4421 + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
4422 + "license": "MIT",
4423 + "dependencies": {
4424 + "@types/hast": "^3.0.0"
4425 + },
4426 + "funding": {
4427 + "type": "opencollective",
4428 + "url": "https://opencollective.com/unified"
4429 + }
4430 + },
4431 + "node_modules/help-me": {
4432 + "version": "5.0.0",
4433 + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
4434 + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
4435 + "dev": true,
4436 + "license": "MIT"
4437 + },
4438 + "node_modules/html-void-elements": {
4439 + "version": "3.0.0",
4440 + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
4441 + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
4442 + "license": "MIT",
4443 + "funding": {
4444 + "type": "github",
4445 + "url": "https://github.com/sponsors/wooorm"
4446 + }
4447 + },
4448 + "node_modules/http-errors": {
4449 + "version": "2.0.1",
4450 + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
4451 + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
4452 + "license": "MIT",
4453 + "dependencies": {
4454 + "depd": "~2.0.0",
4455 + "inherits": "~2.0.4",
4456 + "setprototypeof": "~1.2.0",
4457 + "statuses": "~2.0.2",
4458 + "toidentifier": "~1.0.1"
4459 + },
4460 + "engines": {
4461 + "node": ">= 0.8"
4462 + },
4463 + "funding": {
4464 + "type": "opencollective",
4465 + "url": "https://opencollective.com/express"
4466 + }
4467 + },
4468 + "node_modules/ieee754": {
4469 + "version": "1.2.1",
4470 + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
4471 + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
4472 + "funding": [
4473 + {
4474 + "type": "github",
4475 + "url": "https://github.com/sponsors/feross"
4476 + },
4477 + {
4478 + "type": "patreon",
4479 + "url": "https://www.patreon.com/feross"
4480 + },
4481 + {
4482 + "type": "consulting",
4483 + "url": "https://feross.org/support"
4484 + }
4485 + ],
4486 + "license": "BSD-3-Clause"
4487 + },
4488 + "node_modules/ignore": {
4489 + "version": "5.3.2",
4490 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
4491 + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
4492 + "dev": true,
4493 + "license": "MIT",
4494 + "engines": {
4495 + "node": ">= 4"
4496 + }
4497 + },
4498 + "node_modules/import-fresh": {
4499 + "version": "3.3.1",
4500 + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
4501 + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
4502 + "dev": true,
4503 + "license": "MIT",
4504 + "dependencies": {
4505 + "parent-module": "^1.0.0",
4506 + "resolve-from": "^4.0.0"
4507 + },
4508 + "engines": {
4509 + "node": ">=6"
4510 + },
4511 + "funding": {
4512 + "url": "https://github.com/sponsors/sindresorhus"
4513 + }
4514 + },
4515 + "node_modules/imurmurhash": {
4516 + "version": "0.1.4",
4517 + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
4518 + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
4519 + "dev": true,
4520 + "license": "MIT",
4521 + "engines": {
4522 + "node": ">=0.8.19"
4523 + }
4524 + },
4525 + "node_modules/inherits": {
4526 + "version": "2.0.4",
4527 + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
4528 + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
4529 + "license": "ISC"
4530 + },
4531 + "node_modules/ini": {
4532 + "version": "1.3.8",
4533 + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
4534 + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
4535 + "license": "ISC"
4536 + },
4537 + "node_modules/ipaddr.js": {
4538 + "version": "2.5.0",
4539 + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz",
4540 + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==",
4541 + "license": "MIT",
4542 + "engines": {
4543 + "node": ">= 10"
4544 + }
4545 + },
4546 + "node_modules/is-extglob": {
4547 + "version": "2.1.1",
4548 + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
4549 + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
4550 + "dev": true,
4551 + "license": "MIT",
4552 + "engines": {
4553 + "node": ">=0.10.0"
4554 + }
4555 + },
4556 + "node_modules/is-fullwidth-code-point": {
4557 + "version": "3.0.0",
4558 + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
4559 + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
4560 + "license": "MIT",
4561 + "engines": {
4562 + "node": ">=8"
4563 + }
4564 + },
4565 + "node_modules/is-glob": {
4566 + "version": "4.0.3",
4567 + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
4568 + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
4569 + "dev": true,
4570 + "license": "MIT",
4571 + "dependencies": {
4572 + "is-extglob": "^2.1.1"
4573 + },
4574 + "engines": {
4575 + "node": ">=0.10.0"
4576 + }
4577 + },
4578 + "node_modules/is-stream": {
4579 + "version": "2.0.1",
4580 + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
4581 + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
4582 + "license": "MIT",
4583 + "engines": {
4584 + "node": ">=8"
4585 + },
4586 + "funding": {
4587 + "url": "https://github.com/sponsors/sindresorhus"
4588 + }
4589 + },
4590 + "node_modules/isarray": {
4591 + "version": "1.0.0",
4592 + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
4593 + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
4594 + "license": "MIT"
4595 + },
4596 + "node_modules/isexe": {
4597 + "version": "2.0.0",
4598 + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
4599 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
4600 + "license": "ISC"
4601 + },
4602 + "node_modules/jackspeak": {
4603 + "version": "4.2.3",
4604 + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
4605 + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
4606 + "license": "BlueOak-1.0.0",
4607 + "dependencies": {
4608 + "@isaacs/cliui": "^9.0.0"
4609 + },
4610 + "engines": {
4611 + "node": "20 || >=22"
4612 + },
4613 + "funding": {
4614 + "url": "https://github.com/sponsors/isaacs"
4615 + }
4616 + },
4617 + "node_modules/joycon": {
4618 + "version": "3.1.1",
4619 + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
4620 + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
4621 + "dev": true,
4622 + "license": "MIT",
4623 + "engines": {
4624 + "node": ">=10"
4625 + }
4626 + },
4627 + "node_modules/js-tokens": {
4628 + "version": "9.0.1",
4629 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
4630 + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
4631 + "dev": true,
4632 + "license": "MIT"
4633 + },
4634 + "node_modules/js-yaml": {
4635 + "version": "4.3.1",
4636 + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
4637 + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
4638 + "dev": true,
4639 + "funding": [
4640 + {
4641 + "type": "github",
4642 + "url": "https://github.com/sponsors/puzrin"
4643 + },
4644 + {
4645 + "type": "github",
4646 + "url": "https://github.com/sponsors/nodeca"
4647 + }
4648 + ],
4649 + "license": "MIT",
4650 + "dependencies": {
4651 + "argparse": "^2.0.1"
4652 + },
4653 + "bin": {
4654 + "js-yaml": "bin/js-yaml.js"
4655 + }
4656 + },
4657 + "node_modules/json-buffer": {
4658 + "version": "3.0.1",
4659 + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
4660 + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
4661 + "dev": true,
4662 + "license": "MIT"
4663 + },
4664 + "node_modules/json-schema-ref-resolver": {
4665 + "version": "3.0.0",
4666 + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz",
4667 + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==",
4668 + "funding": [
4669 + {
4670 + "type": "github",
4671 + "url": "https://github.com/sponsors/fastify"
4672 + },
4673 + {
4674 + "type": "opencollective",
4675 + "url": "https://opencollective.com/fastify"
4676 + }
4677 + ],
4678 + "license": "MIT",
4679 + "dependencies": {
4680 + "dequal": "^2.0.3"
4681 + }
4682 + },
4683 + "node_modules/json-schema-traverse": {
4684 + "version": "0.4.1",
4685 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
4686 + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
4687 + "dev": true,
4688 + "license": "MIT"
4689 + },
4690 + "node_modules/json-stable-stringify-without-jsonify": {
4691 + "version": "1.0.1",
4692 + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
4693 + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
4694 + "dev": true,
4695 + "license": "MIT"
4696 + },
4697 + "node_modules/keyv": {
4698 + "version": "4.5.4",
4699 + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
4700 + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
4701 + "dev": true,
4702 + "license": "MIT",
4703 + "dependencies": {
4704 + "json-buffer": "3.0.1"
4705 + }
4706 + },
4707 + "node_modules/lazystream": {
4708 + "version": "1.0.1",
4709 + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
4710 + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
4711 + "license": "MIT",
4712 + "dependencies": {
4713 + "readable-stream": "^2.0.5"
4714 + },
4715 + "engines": {
4716 + "node": ">= 0.6.3"
4717 + }
4718 + },
4719 + "node_modules/lazystream/node_modules/readable-stream": {
4720 + "version": "2.3.8",
4721 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
4722 + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
4723 + "license": "MIT",
4724 + "dependencies": {
4725 + "core-util-is": "~1.0.0",
4726 + "inherits": "~2.0.3",
4727 + "isarray": "~1.0.0",
4728 + "process-nextick-args": "~2.0.0",
4729 + "safe-buffer": "~5.1.1",
4730 + "string_decoder": "~1.1.1",
4731 + "util-deprecate": "~1.0.1"
4732 + }
4733 + },
4734 + "node_modules/lazystream/node_modules/safe-buffer": {
4735 + "version": "5.1.2",
4736 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
4737 + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
4738 + "license": "MIT"
4739 + },
4740 + "node_modules/lazystream/node_modules/string_decoder": {
4741 + "version": "1.1.1",
4742 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
4743 + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
4744 + "license": "MIT",
4745 + "dependencies": {
4746 + "safe-buffer": "~5.1.0"
4747 + }
4748 + },
4749 + "node_modules/levn": {
4750 + "version": "0.4.1",
4751 + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
4752 + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
4753 + "dev": true,
4754 + "license": "MIT",
4755 + "dependencies": {
4756 + "prelude-ls": "^1.2.1",
4757 + "type-check": "~0.4.0"
4758 + },
4759 + "engines": {
4760 + "node": ">= 0.8.0"
4761 + }
4762 + },
4763 + "node_modules/light-my-request": {
4764 + "version": "6.6.0",
4765 + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz",
4766 + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==",
4767 + "funding": [
4768 + {
4769 + "type": "github",
4770 + "url": "https://github.com/sponsors/fastify"
4771 + },
4772 + {
4773 + "type": "opencollective",
4774 + "url": "https://opencollective.com/fastify"
4775 + }
4776 + ],
4777 + "license": "BSD-3-Clause",
4778 + "dependencies": {
4779 + "cookie": "^1.0.1",
4780 + "process-warning": "^4.0.0",
4781 + "set-cookie-parser": "^2.6.0"
4782 + }
4783 + },
4784 + "node_modules/light-my-request/node_modules/cookie": {
4785 + "version": "1.1.1",
4786 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
4787 + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
4788 + "license": "MIT",
4789 + "engines": {
4790 + "node": ">=18"
4791 + },
4792 + "funding": {
4793 + "type": "opencollective",
4794 + "url": "https://opencollective.com/express"
4795 + }
4796 + },
4797 + "node_modules/light-my-request/node_modules/process-warning": {
4798 + "version": "4.0.1",
4799 + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz",
4800 + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==",
4801 + "funding": [
4802 + {
4803 + "type": "github",
4804 + "url": "https://github.com/sponsors/fastify"
4805 + },
4806 + {
4807 + "type": "opencollective",
4808 + "url": "https://opencollective.com/fastify"
4809 + }
4810 + ],
4811 + "license": "MIT"
4812 + },
4813 + "node_modules/linkify-it": {
4814 + "version": "5.0.2",
4815 + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz",
4816 + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==",
4817 + "funding": [
4818 + {
4819 + "type": "github",
4820 + "url": "https://github.com/sponsors/puzrin"
4821 + },
4822 + {
4823 + "type": "github",
4824 + "url": "https://github.com/sponsors/markdown-it"
4825 + }
4826 + ],
4827 + "license": "MIT",
4828 + "dependencies": {
4829 + "uc.micro": "^2.0.0"
4830 + }
4831 + },
4832 + "node_modules/locate-path": {
4833 + "version": "6.0.0",
4834 + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
4835 + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
4836 + "dev": true,
4837 + "license": "MIT",
4838 + "dependencies": {
4839 + "p-locate": "^5.0.0"
4840 + },
4841 + "engines": {
4842 + "node": ">=10"
4843 + },
4844 + "funding": {
4845 + "url": "https://github.com/sponsors/sindresorhus"
4846 + }
4847 + },
4848 + "node_modules/lodash": {
4849 + "version": "4.18.1",
4850 + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
4851 + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
4852 + "license": "MIT"
4853 + },
4854 + "node_modules/lodash.merge": {
4855 + "version": "4.6.2",
4856 + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
4857 + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
4858 + "dev": true,
4859 + "license": "MIT"
4860 + },
4861 + "node_modules/loupe": {
4862 + "version": "3.2.1",
4863 + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
4864 + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
4865 + "dev": true,
4866 + "license": "MIT"
4867 + },
4868 + "node_modules/lru-cache": {
4869 + "version": "11.5.2",
4870 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
4871 + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
4872 + "license": "BlueOak-1.0.0",
4873 + "engines": {
4874 + "node": "20 || >=22"
4875 + }
4876 + },
4877 + "node_modules/magic-string": {
4878 + "version": "0.30.21",
4879 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
4880 + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
4881 + "dev": true,
4882 + "license": "MIT",
4883 + "dependencies": {
4884 + "@jridgewell/sourcemap-codec": "^1.5.5"
4885 + }
4886 + },
4887 + "node_modules/markdown-it": {
4888 + "version": "14.3.0",
4889 + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz",
4890 + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==",
4891 + "funding": [
4892 + {
4893 + "type": "github",
4894 + "url": "https://github.com/sponsors/puzrin"
4895 + },
4896 + {
4897 + "type": "github",
4898 + "url": "https://github.com/sponsors/markdown-it"
4899 + }
4900 + ],
4901 + "license": "MIT",
4902 + "dependencies": {
4903 + "argparse": "^2.0.1",
4904 + "entities": "^4.5.0",
4905 + "linkify-it": "^5.0.2",
4906 + "mdurl": "^2.0.0",
4907 + "punycode.js": "^2.3.1",
4908 + "uc.micro": "^2.1.0"
4909 + },
4910 + "bin": {
4911 + "markdown-it": "bin/markdown-it.mjs"
4912 + }
4913 + },
4914 + "node_modules/markdown-it-anchor": {
4915 + "version": "9.2.1",
4916 + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-9.2.1.tgz",
4917 + "integrity": "sha512-p6APiLJDFAW2GEvaavDvhIBn7jrX2jLv77NkBGgNacFTurbORYc4pyYySg/mI6mpR6cHQuAtzKtmqgQr4K8dsQ==",
4918 + "license": "Unlicense",
4919 + "peerDependencies": {
4920 + "@types/markdown-it": "*",
4921 + "markdown-it": "*"
4922 + }
4923 + },
4924 + "node_modules/markdown-it-task-lists": {
4925 + "version": "2.1.1",
4926 + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz",
4927 + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==",
4928 + "license": "ISC"
4929 + },
4930 + "node_modules/mdast-util-to-hast": {
4931 + "version": "13.2.1",
4932 + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
4933 + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
4934 + "license": "MIT",
4935 + "dependencies": {
4936 + "@types/hast": "^3.0.0",
4937 + "@types/mdast": "^4.0.0",
4938 + "@ungap/structured-clone": "^1.0.0",
4939 + "devlop": "^1.0.0",
4940 + "micromark-util-sanitize-uri": "^2.0.0",
4941 + "trim-lines": "^3.0.0",
4942 + "unist-util-position": "^5.0.0",
4943 + "unist-util-visit": "^5.0.0",
4944 + "vfile": "^6.0.0"
4945 + },
4946 + "funding": {
4947 + "type": "opencollective",
4948 + "url": "https://opencollective.com/unified"
4949 + }
4950 + },
4951 + "node_modules/mdurl": {
4952 + "version": "2.1.0",
4953 + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
4954 + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==",
4955 + "license": "MIT"
4956 + },
4957 + "node_modules/micromark-util-character": {
4958 + "version": "2.1.1",
4959 + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
4960 + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
4961 + "funding": [
4962 + {
4963 + "type": "GitHub Sponsors",
4964 + "url": "https://github.com/sponsors/unifiedjs"
4965 + },
4966 + {
4967 + "type": "OpenCollective",
4968 + "url": "https://opencollective.com/unified"
4969 + }
4970 + ],
4971 + "license": "MIT",
4972 + "dependencies": {
4973 + "micromark-util-symbol": "^2.0.0",
4974 + "micromark-util-types": "^2.0.0"
4975 + }
4976 + },
4977 + "node_modules/micromark-util-encode": {
4978 + "version": "2.0.1",
4979 + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
4980 + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
4981 + "funding": [
4982 + {
4983 + "type": "GitHub Sponsors",
4984 + "url": "https://github.com/sponsors/unifiedjs"
4985 + },
4986 + {
4987 + "type": "OpenCollective",
4988 + "url": "https://opencollective.com/unified"
4989 + }
4990 + ],
4991 + "license": "MIT"
4992 + },
4993 + "node_modules/micromark-util-sanitize-uri": {
4994 + "version": "2.0.1",
4995 + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
4996 + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
4997 + "funding": [
4998 + {
4999 + "type": "GitHub Sponsors",
5000 + "url": "https://github.com/sponsors/unifiedjs"
5001 + },
5002 + {
5003 + "type": "OpenCollective",
5004 + "url": "https://opencollective.com/unified"
5005 + }
5006 + ],
5007 + "license": "MIT",
5008 + "dependencies": {
5009 + "micromark-util-character": "^2.0.0",
5010 + "micromark-util-encode": "^2.0.0",
5011 + "micromark-util-symbol": "^2.0.0"
5012 + }
5013 + },
5014 + "node_modules/micromark-util-symbol": {
5015 + "version": "2.0.1",
5016 + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
5017 + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
5018 + "funding": [
5019 + {
5020 + "type": "GitHub Sponsors",
5021 + "url": "https://github.com/sponsors/unifiedjs"
5022 + },
5023 + {
5024 + "type": "OpenCollective",
5025 + "url": "https://opencollective.com/unified"
5026 + }
5027 + ],
5028 + "license": "MIT"
5029 + },
5030 + "node_modules/micromark-util-types": {
5031 + "version": "2.0.2",
5032 + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
5033 + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
5034 + "funding": [
5035 + {
5036 + "type": "GitHub Sponsors",
5037 + "url": "https://github.com/sponsors/unifiedjs"
5038 + },
5039 + {
5040 + "type": "OpenCollective",
5041 + "url": "https://opencollective.com/unified"
5042 + }
5043 + ],
5044 + "license": "MIT"
5045 + },
5046 + "node_modules/mime": {
5047 + "version": "4.1.0",
5048 + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz",
5049 + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==",
5050 + "funding": [
5051 + "https://github.com/sponsors/broofa"
5052 + ],
5053 + "license": "MIT",
5054 + "bin": {
5055 + "mime": "bin/cli.js"
5056 + },
5057 + "engines": {
5058 + "node": ">=16"
5059 + }
5060 + },
5061 + "node_modules/mimic-response": {
5062 + "version": "3.1.0",
5063 + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
5064 + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
5065 + "license": "MIT",
5066 + "engines": {
5067 + "node": ">=10"
5068 + },
5069 + "funding": {
5070 + "url": "https://github.com/sponsors/sindresorhus"
5071 + }
5072 + },
5073 + "node_modules/minimatch": {
5074 + "version": "3.1.5",
5075 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
5076 + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
5077 + "dev": true,
5078 + "license": "ISC",
5079 + "dependencies": {
5080 + "brace-expansion": "^1.1.7"
5081 + },
5082 + "engines": {
5083 + "node": "*"
5084 + }
5085 + },
5086 + "node_modules/minimist": {
5087 + "version": "1.2.8",
5088 + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
5089 + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
5090 + "license": "MIT",
5091 + "funding": {
5092 + "url": "https://github.com/sponsors/ljharb"
5093 + }
5094 + },
5095 + "node_modules/minipass": {
5096 + "version": "7.1.3",
5097 + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
5098 + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
5099 + "license": "BlueOak-1.0.0",
5100 + "engines": {
5101 + "node": ">=16 || 14 >=14.17"
5102 + }
5103 + },
5104 + "node_modules/mkdirp-classic": {
5105 + "version": "0.5.3",
5106 + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
5107 + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
5108 + "license": "MIT"
5109 + },
5110 + "node_modules/ms": {
5111 + "version": "2.1.3",
5112 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
5113 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
5114 + "dev": true,
5115 + "license": "MIT"
5116 + },
5117 + "node_modules/nanoid": {
5118 + "version": "3.3.18",
5119 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
5120 + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
5121 + "dev": true,
5122 + "funding": [
5123 + {
5124 + "type": "github",
5125 + "url": "https://github.com/sponsors/ai"
5126 + }
5127 + ],
5128 + "license": "MIT",
5129 + "bin": {
5130 + "nanoid": "bin/nanoid.cjs"
5131 + },
5132 + "engines": {
5133 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
5134 + }
5135 + },
5136 + "node_modules/napi-build-utils": {
5137 + "version": "2.0.0",
5138 + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
5139 + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
5140 + "license": "MIT"
5141 + },
5142 + "node_modules/natural-compare": {
5143 + "version": "1.4.0",
5144 + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
5145 + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
5146 + "dev": true,
5147 + "license": "MIT"
5148 + },
5149 + "node_modules/node-abi": {
5150 + "version": "3.94.0",
5151 + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz",
5152 + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==",
5153 + "license": "MIT",
5154 + "dependencies": {
5155 + "semver": "^7.3.5"
5156 + },
5157 + "engines": {
5158 + "node": ">=10"
5159 + }
5160 + },
5161 + "node_modules/node-addon-api": {
5162 + "version": "8.9.1",
5163 + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.1.tgz",
5164 + "integrity": "sha512-4eUQWVPCUUUiBjLnHS3cXWeC6ryoPUc0U3rP7IuzapoGbzMqd/r6KKO0clr0b+snQhsrueFEhCZDdK+LK7hxKg==",
5165 + "license": "MIT",
5166 + "engines": {
5167 + "node": "^18 || ^20 || >= 21"
5168 + }
5169 + },
5170 + "node_modules/node-gyp-build": {
5171 + "version": "4.8.4",
5172 + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
5173 + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
5174 + "license": "MIT",
5175 + "bin": {
5176 + "node-gyp-build": "bin.js",
5177 + "node-gyp-build-optional": "optional.js",
5178 + "node-gyp-build-test": "build-test.js"
5179 + }
5180 + },
5181 + "node_modules/normalize-path": {
5182 + "version": "3.0.0",
5183 + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
5184 + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
5185 + "license": "MIT",
5186 + "engines": {
5187 + "node": ">=0.10.0"
5188 + }
5189 + },
5190 + "node_modules/nunjucks": {
5191 + "version": "3.2.4",
5192 + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz",
5193 + "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==",
5194 + "license": "BSD-2-Clause",
5195 + "dependencies": {
5196 + "a-sync-waterfall": "^1.0.0",
5197 + "asap": "^2.0.3",
5198 + "commander": "^5.1.0"
5199 + },
5200 + "bin": {
5201 + "nunjucks-precompile": "bin/precompile"
5202 + },
5203 + "engines": {
5204 + "node": ">= 6.9.0"
5205 + },
5206 + "peerDependencies": {
5207 + "chokidar": "^3.3.0"
5208 + },
5209 + "peerDependenciesMeta": {
5210 + "chokidar": {
5211 + "optional": true
5212 + }
5213 + }
5214 + },
5215 + "node_modules/nunjucks/node_modules/commander": {
5216 + "version": "5.1.0",
5217 + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
5218 + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
5219 + "license": "MIT",
5220 + "engines": {
5221 + "node": ">= 6"
5222 + }
5223 + },
5224 + "node_modules/on-exit-leak-free": {
5225 + "version": "2.1.2",
5226 + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
5227 + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
5228 + "license": "MIT",
5229 + "engines": {
5230 + "node": ">=14.0.0"
5231 + }
5232 + },
5233 + "node_modules/once": {
5234 + "version": "1.4.0",
5235 + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
5236 + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
5237 + "license": "ISC",
5238 + "dependencies": {
5239 + "wrappy": "1"
5240 + }
5241 + },
5242 + "node_modules/oniguruma-parser": {
5243 + "version": "0.12.2",
5244 + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz",
5245 + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==",
5246 + "license": "MIT"
5247 + },
5248 + "node_modules/oniguruma-to-es": {
5249 + "version": "4.3.6",
5250 + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz",
5251 + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==",
5252 + "license": "MIT",
5253 + "dependencies": {
5254 + "oniguruma-parser": "^0.12.2",
5255 + "regex": "^6.1.0",
5256 + "regex-recursion": "^6.0.2"
5257 + }
5258 + },
5259 + "node_modules/optionator": {
5260 + "version": "0.9.4",
5261 + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
5262 + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
5263 + "dev": true,
5264 + "license": "MIT",
5265 + "dependencies": {
5266 + "deep-is": "^0.1.3",
5267 + "fast-levenshtein": "^2.0.6",
5268 + "levn": "^0.4.1",
5269 + "prelude-ls": "^1.2.1",
5270 + "type-check": "^0.4.0",
5271 + "word-wrap": "^1.2.5"
5272 + },
5273 + "engines": {
5274 + "node": ">= 0.8.0"
5275 + }
5276 + },
5277 + "node_modules/p-limit": {
5278 + "version": "3.1.0",
5279 + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
5280 + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
5281 + "dev": true,
5282 + "license": "MIT",
5283 + "dependencies": {
5284 + "yocto-queue": "^0.1.0"
5285 + },
5286 + "engines": {
5287 + "node": ">=10"
5288 + },
5289 + "funding": {
5290 + "url": "https://github.com/sponsors/sindresorhus"
5291 + }
5292 + },
5293 + "node_modules/p-locate": {
5294 + "version": "5.0.0",
5295 + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
5296 + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
5297 + "dev": true,
5298 + "license": "MIT",
5299 + "dependencies": {
5300 + "p-limit": "^3.0.2"
5301 + },
5302 + "engines": {
5303 + "node": ">=10"
5304 + },
5305 + "funding": {
5306 + "url": "https://github.com/sponsors/sindresorhus"
5307 + }
5308 + },
5309 + "node_modules/p-try": {
5310 + "version": "2.2.0",
5311 + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
5312 + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
5313 + "license": "MIT",
5314 + "engines": {
5315 + "node": ">=6"
5316 + }
5317 + },
5318 + "node_modules/package-json-from-dist": {
5319 + "version": "1.0.1",
5320 + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
5321 + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
5322 + "license": "BlueOak-1.0.0"
5323 + },
5324 + "node_modules/parent-module": {
5325 + "version": "1.0.1",
5326 + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
5327 + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
5328 + "dev": true,
5329 + "license": "MIT",
5330 + "dependencies": {
5331 + "callsites": "^3.0.0"
5332 + },
5333 + "engines": {
5334 + "node": ">=6"
5335 + }
5336 + },
5337 + "node_modules/path-exists": {
5338 + "version": "4.0.0",
5339 + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
5340 + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
5341 + "license": "MIT",
5342 + "engines": {
5343 + "node": ">=8"
5344 + }
5345 + },
5346 + "node_modules/path-key": {
5347 + "version": "3.1.1",
5348 + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
5349 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
5350 + "license": "MIT",
5351 + "engines": {
5352 + "node": ">=8"
5353 + }
5354 + },
5355 + "node_modules/path-scurry": {
5356 + "version": "2.0.2",
5357 + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
5358 + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
5359 + "license": "BlueOak-1.0.0",
5360 + "dependencies": {
5361 + "lru-cache": "^11.0.0",
5362 + "minipass": "^7.1.2"
5363 + },
5364 + "engines": {
5365 + "node": "18 || 20 || >=22"
5366 + },
5367 + "funding": {
5368 + "url": "https://github.com/sponsors/isaacs"
5369 + }
5370 + },
5371 + "node_modules/pathe": {
5372 + "version": "2.0.3",
5373 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
5374 + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
5375 + "dev": true,
5376 + "license": "MIT"
5377 + },
5378 + "node_modules/pathval": {
5379 + "version": "2.0.1",
5380 + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
5381 + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
5382 + "dev": true,
5383 + "license": "MIT",
5384 + "engines": {
5385 + "node": ">= 14.16"
5386 + }
5387 + },
5388 + "node_modules/pdfjs-dist": {
5389 + "version": "4.10.38",
5390 + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz",
5391 + "integrity": "sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==",
5392 + "license": "Apache-2.0",
5393 + "engines": {
5394 + "node": ">=20"
5395 + },
5396 + "optionalDependencies": {
5397 + "@napi-rs/canvas": "^0.1.65"
5398 + }
5399 + },
5400 + "node_modules/pend": {
5401 + "version": "1.2.0",
5402 + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
5403 + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
5404 + "license": "MIT"
5405 + },
5406 + "node_modules/picocolors": {
5407 + "version": "1.1.1",
5408 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
5409 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
5410 + "dev": true,
5411 + "license": "ISC"
5412 + },
5413 + "node_modules/picomatch": {
5414 + "version": "4.0.5",
5415 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
5416 + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
5417 + "dev": true,
5418 + "license": "MIT",
5419 + "engines": {
5420 + "node": ">=12"
5421 + },
5422 + "funding": {
5423 + "url": "https://github.com/sponsors/jonschlinkert"
5424 + }
5425 + },
5426 + "node_modules/pino": {
5427 + "version": "9.14.0",
5428 + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz",
5429 + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==",
5430 + "license": "MIT",
5431 + "dependencies": {
5432 + "@pinojs/redact": "^0.4.0",
5433 + "atomic-sleep": "^1.0.0",
5434 + "on-exit-leak-free": "^2.1.0",
5435 + "pino-abstract-transport": "^2.0.0",
5436 + "pino-std-serializers": "^7.0.0",
5437 + "process-warning": "^5.0.0",
5438 + "quick-format-unescaped": "^4.0.3",
5439 + "real-require": "^0.2.0",
5440 + "safe-stable-stringify": "^2.3.1",
5441 + "sonic-boom": "^4.0.1",
5442 + "thread-stream": "^3.0.0"
5443 + },
5444 + "bin": {
5445 + "pino": "bin.js"
5446 + }
5447 + },
5448 + "node_modules/pino-abstract-transport": {
5449 + "version": "2.0.0",
5450 + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
5451 + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
5452 + "license": "MIT",
5453 + "dependencies": {
5454 + "split2": "^4.0.0"
5455 + }
5456 + },
5457 + "node_modules/pino-pretty": {
5458 + "version": "13.1.3",
5459 + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz",
5460 + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==",
5461 + "dev": true,
5462 + "license": "MIT",
5463 + "dependencies": {
5464 + "colorette": "^2.0.7",
5465 + "dateformat": "^4.6.3",
5466 + "fast-copy": "^4.0.0",
5467 + "fast-safe-stringify": "^2.1.1",
5468 + "help-me": "^5.0.0",
5469 + "joycon": "^3.1.1",
5470 + "minimist": "^1.2.6",
5471 + "on-exit-leak-free": "^2.1.0",
5472 + "pino-abstract-transport": "^3.0.0",
5473 + "pump": "^3.0.0",
5474 + "secure-json-parse": "^4.0.0",
5475 + "sonic-boom": "^4.0.1",
5476 + "strip-json-comments": "^5.0.2"
5477 + },
5478 + "bin": {
5479 + "pino-pretty": "bin.js"
5480 + }
5481 + },
5482 + "node_modules/pino-pretty/node_modules/pino-abstract-transport": {
5483 + "version": "3.0.0",
5484 + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
5485 + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
5486 + "dev": true,
5487 + "license": "MIT",
5488 + "dependencies": {
5489 + "split2": "^4.0.0"
5490 + }
5491 + },
5492 + "node_modules/pino-pretty/node_modules/strip-json-comments": {
5493 + "version": "5.0.3",
5494 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
5495 + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==",
5496 + "dev": true,
5497 + "license": "MIT",
5498 + "engines": {
5499 + "node": ">=14.16"
5500 + },
5501 + "funding": {
5502 + "url": "https://github.com/sponsors/sindresorhus"
5503 + }
5504 + },
5505 + "node_modules/pino-std-serializers": {
5506 + "version": "7.1.0",
5507 + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
5508 + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
5509 + "license": "MIT"
5510 + },
5511 + "node_modules/pngjs": {
5512 + "version": "5.0.0",
5513 + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
5514 + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
5515 + "license": "MIT",
5516 + "engines": {
5517 + "node": ">=10.13.0"
5518 + }
5519 + },
5520 + "node_modules/postal-mime": {
5521 + "version": "2.7.6",
5522 + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.6.tgz",
5523 + "integrity": "sha512-UUlyE2KlxmvwMGq060onF/VWU3zD6dVW31FwokQ5v26jWd35fbs2MoTFzh+jXnPwAyV2MULMKXcBInGOl5TO6Q==",
5524 + "license": "MIT-0"
5525 + },
5526 + "node_modules/postcss": {
5527 + "version": "8.5.26",
5528 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
5529 + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
5530 + "dev": true,
5531 + "funding": [
5532 + {
5533 + "type": "opencollective",
5534 + "url": "https://opencollective.com/postcss/"
5535 + },
5536 + {
5537 + "type": "tidelift",
5538 + "url": "https://tidelift.com/funding/github/npm/postcss"
5539 + },
5540 + {
5541 + "type": "github",
5542 + "url": "https://github.com/sponsors/ai"
5543 + }
5544 + ],
5545 + "license": "MIT",
5546 + "dependencies": {
5547 + "nanoid": "^3.3.17",
5548 + "picocolors": "^1.1.1",
5549 + "source-map-js": "^1.2.1"
5550 + },
5551 + "engines": {
5552 + "node": "^10 || ^12 || >=14"
5553 + }
5554 + },
5555 + "node_modules/prebuild-install": {
5556 + "version": "7.1.3",
5557 + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
5558 + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
5559 + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
5560 + "license": "MIT",
5561 + "dependencies": {
5562 + "detect-libc": "^2.0.0",
5563 + "expand-template": "^2.0.3",
5564 + "github-from-package": "0.0.0",
5565 + "minimist": "^1.2.3",
5566 + "mkdirp-classic": "^0.5.3",
5567 + "napi-build-utils": "^2.0.0",
5568 + "node-abi": "^3.3.0",
5569 + "pump": "^3.0.0",
5570 + "rc": "^1.2.7",
5571 + "simple-get": "^4.0.0",
5572 + "tar-fs": "^2.0.0",
5573 + "tunnel-agent": "^0.6.0"
5574 + },
5575 + "bin": {
5576 + "prebuild-install": "bin.js"
5577 + },
5578 + "engines": {
5579 + "node": ">=10"
5580 + }
5581 + },
5582 + "node_modules/prelude-ls": {
5583 + "version": "1.2.1",
5584 + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
5585 + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
5586 + "dev": true,
5587 + "license": "MIT",
5588 + "engines": {
5589 + "node": ">= 0.8.0"
5590 + }
5591 + },
5592 + "node_modules/process": {
5593 + "version": "0.11.10",
5594 + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
5595 + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
5596 + "license": "MIT",
5597 + "engines": {
5598 + "node": ">= 0.6.0"
5599 + }
5600 + },
5601 + "node_modules/process-nextick-args": {
5602 + "version": "2.0.1",
5603 + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
5604 + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
5605 + "license": "MIT"
5606 + },
5607 + "node_modules/process-warning": {
5608 + "version": "5.1.0",
5609 + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz",
5610 + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==",
5611 + "funding": [
5612 + {
5613 + "type": "github",
5614 + "url": "https://github.com/sponsors/fastify"
5615 + },
5616 + {
5617 + "type": "opencollective",
5618 + "url": "https://opencollective.com/fastify"
5619 + }
5620 + ],
5621 + "license": "MIT"
5622 + },
5623 + "node_modules/property-information": {
5624 + "version": "7.2.0",
5625 + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
5626 + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
5627 + "license": "MIT",
5628 + "funding": {
5629 + "type": "github",
5630 + "url": "https://github.com/sponsors/wooorm"
5631 + }
5632 + },
5633 + "node_modules/pump": {
5634 + "version": "3.0.4",
5635 + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
5636 + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
5637 + "license": "MIT",
5638 + "dependencies": {
5639 + "end-of-stream": "^1.1.0",
5640 + "once": "^1.3.1"
5641 + }
5642 + },
5643 + "node_modules/punycode": {
5644 + "version": "2.3.1",
5645 + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
5646 + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
5647 + "dev": true,
5648 + "license": "MIT",
5649 + "engines": {
5650 + "node": ">=6"
5651 + }
5652 + },
5653 + "node_modules/punycode.js": {
5654 + "version": "2.3.1",
5655 + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
5656 + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
5657 + "license": "MIT",
5658 + "engines": {
5659 + "node": ">=6"
5660 + }
5661 + },
5662 + "node_modules/qrcode": {
5663 + "version": "1.5.4",
5664 + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
5665 + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
5666 + "license": "MIT",
5667 + "dependencies": {
5668 + "dijkstrajs": "^1.0.1",
5669 + "pngjs": "^5.0.0",
5670 + "yargs": "^15.3.1"
5671 + },
5672 + "bin": {
5673 + "qrcode": "bin/qrcode"
5674 + },
5675 + "engines": {
5676 + "node": ">=10.13.0"
5677 + }
5678 + },
5679 + "node_modules/quick-format-unescaped": {
5680 + "version": "4.0.4",
5681 + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
5682 + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
5683 + "license": "MIT"
5684 + },
5685 + "node_modules/rc": {
5686 + "version": "1.2.8",
5687 + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
5688 + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
5689 + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
5690 + "dependencies": {
5691 + "deep-extend": "^0.6.0",
5692 + "ini": "~1.3.0",
5693 + "minimist": "^1.2.0",
5694 + "strip-json-comments": "~2.0.1"
5695 + },
5696 + "bin": {
5697 + "rc": "cli.js"
5698 + }
5699 + },
5700 + "node_modules/rc/node_modules/strip-json-comments": {
5701 + "version": "2.0.1",
5702 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
5703 + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
5704 + "license": "MIT",
5705 + "engines": {
5706 + "node": ">=0.10.0"
5707 + }
5708 + },
5709 + "node_modules/readable-stream": {
5710 + "version": "4.7.0",
5711 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
5712 + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
5713 + "license": "MIT",
5714 + "dependencies": {
5715 + "abort-controller": "^3.0.0",
5716 + "buffer": "^6.0.3",
5717 + "events": "^3.3.0",
5718 + "process": "^0.11.10",
5719 + "string_decoder": "^1.3.0"
5720 + },
5721 + "engines": {
5722 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
5723 + }
5724 + },
5725 + "node_modules/readdir-glob": {
5726 + "version": "1.1.3",
5727 + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
5728 + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
5729 + "license": "Apache-2.0",
5730 + "dependencies": {
5731 + "minimatch": "^5.1.0"
5732 + }
5733 + },
5734 + "node_modules/readdir-glob/node_modules/brace-expansion": {
5735 + "version": "2.1.4",
5736 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
5737 + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
5738 + "license": "MIT",
5739 + "dependencies": {
5740 + "balanced-match": "^1.0.0"
5741 + }
5742 + },
5743 + "node_modules/readdir-glob/node_modules/minimatch": {
5744 + "version": "5.1.9",
5745 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
5746 + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
5747 + "license": "ISC",
5748 + "dependencies": {
5749 + "brace-expansion": "^2.0.1"
5750 + },
5751 + "engines": {
5752 + "node": ">=10"
5753 + }
5754 + },
5755 + "node_modules/real-require": {
5756 + "version": "0.2.0",
5757 + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
5758 + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
5759 + "license": "MIT",
5760 + "engines": {
5761 + "node": ">= 12.13.0"
5762 + }
5763 + },
5764 + "node_modules/regex": {
5765 + "version": "6.1.0",
5766 + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
5767 + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==",
5768 + "license": "MIT",
5769 + "dependencies": {
5770 + "regex-utilities": "^2.3.0"
5771 + }
5772 + },
5773 + "node_modules/regex-recursion": {
5774 + "version": "6.0.2",
5775 + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
5776 + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
5777 + "license": "MIT",
5778 + "dependencies": {
5779 + "regex-utilities": "^2.3.0"
5780 + }
5781 + },
5782 + "node_modules/regex-utilities": {
5783 + "version": "2.3.0",
5784 + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
5785 + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
5786 + "license": "MIT"
5787 + },
5788 + "node_modules/require-directory": {
5789 + "version": "2.1.1",
5790 + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
5791 + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
5792 + "license": "MIT",
5793 + "engines": {
5794 + "node": ">=0.10.0"
5795 + }
5796 + },
5797 + "node_modules/require-from-string": {
5798 + "version": "2.0.2",
5799 + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
5800 + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
5801 + "license": "MIT",
5802 + "engines": {
5803 + "node": ">=0.10.0"
5804 + }
5805 + },
5806 + "node_modules/require-main-filename": {
5807 + "version": "2.0.0",
5808 + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
5809 + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
5810 + "license": "ISC"
5811 + },
5812 + "node_modules/resolve-from": {
5813 + "version": "4.0.0",
5814 + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
5815 + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
5816 + "dev": true,
5817 + "license": "MIT",
5818 + "engines": {
5819 + "node": ">=4"
5820 + }
5821 + },
5822 + "node_modules/ret": {
5823 + "version": "0.5.0",
5824 + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz",
5825 + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==",
5826 + "license": "MIT",
5827 + "engines": {
5828 + "node": ">=10"
5829 + }
5830 + },
5831 + "node_modules/reusify": {
5832 + "version": "1.1.0",
5833 + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
5834 + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
5835 + "license": "MIT",
5836 + "engines": {
5837 + "iojs": ">=1.0.0",
5838 + "node": ">=0.10.0"
5839 + }
5840 + },
5841 + "node_modules/rfdc": {
5842 + "version": "1.4.1",
5843 + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
5844 + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
5845 + "license": "MIT"
5846 + },
5847 + "node_modules/rollup": {
5848 + "version": "4.62.4",
5849 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz",
5850 + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==",
5851 + "dev": true,
5852 + "license": "MIT",
5853 + "dependencies": {
5854 + "@types/estree": "1.0.9"
5855 + },
5856 + "bin": {
5857 + "rollup": "dist/bin/rollup"
5858 + },
5859 + "engines": {
5860 + "node": ">=18.0.0",
5861 + "npm": ">=8.0.0"
5862 + },
5863 + "optionalDependencies": {
5864 + "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
5865 + "@rollup/rollup-android-arm-eabi": "4.62.4",
5866 + "@rollup/rollup-android-arm64": "4.62.4",
5867 + "@rollup/rollup-darwin-arm64": "4.62.4",
5868 + "@rollup/rollup-darwin-x64": "4.62.4",
5869 + "@rollup/rollup-freebsd-arm64": "4.62.4",
5870 + "@rollup/rollup-freebsd-x64": "4.62.4",
5871 + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4",
5872 + "@rollup/rollup-linux-arm-musleabihf": "4.62.4",
5873 + "@rollup/rollup-linux-arm64-gnu": "4.62.4",
5874 + "@rollup/rollup-linux-arm64-musl": "4.62.4",
5875 + "@rollup/rollup-linux-loong64-gnu": "4.62.4",
5876 + "@rollup/rollup-linux-loong64-musl": "4.62.4",
5877 + "@rollup/rollup-linux-ppc64-gnu": "4.62.4",
5878 + "@rollup/rollup-linux-ppc64-musl": "4.62.4",
5879 + "@rollup/rollup-linux-riscv64-gnu": "4.62.4",
5880 + "@rollup/rollup-linux-riscv64-musl": "4.62.4",
5881 + "@rollup/rollup-linux-s390x-gnu": "4.62.4",
5882 + "@rollup/rollup-linux-x64-gnu": "4.62.4",
5883 + "@rollup/rollup-linux-x64-musl": "4.62.4",
5884 + "@rollup/rollup-openbsd-x64": "4.62.4",
5885 + "@rollup/rollup-openharmony-arm64": "4.62.4",
5886 + "@rollup/rollup-win32-arm64-msvc": "4.62.4",
5887 + "@rollup/rollup-win32-ia32-msvc": "4.62.4",
5888 + "@rollup/rollup-win32-x64-gnu": "4.62.4",
5889 + "@rollup/rollup-win32-x64-msvc": "4.62.4",
5890 + "fsevents": "~2.3.2"
5891 + }
5892 + },
5893 + "node_modules/safe-buffer": {
5894 + "version": "5.2.1",
5895 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
5896 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
5897 + "funding": [
5898 + {
5899 + "type": "github",
5900 + "url": "https://github.com/sponsors/feross"
5901 + },
5902 + {
5903 + "type": "patreon",
5904 + "url": "https://www.patreon.com/feross"
5905 + },
5906 + {
5907 + "type": "consulting",
5908 + "url": "https://feross.org/support"
5909 + }
5910 + ],
5911 + "license": "MIT"
5912 + },
5913 + "node_modules/safe-regex2": {
5914 + "version": "5.1.1",
5915 + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz",
5916 + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==",
5917 + "funding": [
5918 + {
5919 + "type": "github",
5920 + "url": "https://github.com/sponsors/fastify"
5921 + },
5922 + {
5923 + "type": "opencollective",
5924 + "url": "https://opencollective.com/fastify"
5925 + }
5926 + ],
5927 + "license": "MIT",
5928 + "dependencies": {
5929 + "ret": "~0.5.0"
5930 + },
5931 + "bin": {
5932 + "safe-regex2": "bin/safe-regex2.js"
5933 + }
5934 + },
5935 + "node_modules/safe-stable-stringify": {
5936 + "version": "2.5.0",
5937 + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
5938 + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
5939 + "license": "MIT",
5940 + "engines": {
5941 + "node": ">=10"
5942 + }
5943 + },
5944 + "node_modules/secure-json-parse": {
5945 + "version": "4.1.0",
5946 + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz",
5947 + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==",
5948 + "funding": [
5949 + {
5950 + "type": "github",
5951 + "url": "https://github.com/sponsors/fastify"
5952 + },
5953 + {
5954 + "type": "opencollective",
5955 + "url": "https://opencollective.com/fastify"
5956 + }
5957 + ],
5958 + "license": "BSD-3-Clause"
5959 + },
5960 + "node_modules/semver": {
5961 + "version": "7.8.5",
5962 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
5963 + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
5964 + "license": "ISC",
5965 + "bin": {
5966 + "semver": "bin/semver.js"
5967 + },
5968 + "engines": {
5969 + "node": ">=10"
5970 + }
5971 + },
5972 + "node_modules/set-blocking": {
5973 + "version": "2.0.0",
5974 + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
5975 + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
5976 + "license": "ISC"
5977 + },
5978 + "node_modules/set-cookie-parser": {
5979 + "version": "2.7.2",
5980 + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
5981 + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
5982 + "license": "MIT"
5983 + },
5984 + "node_modules/setprototypeof": {
5985 + "version": "1.2.0",
5986 + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
5987 + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
5988 + "license": "ISC"
5989 + },
5990 + "node_modules/sharp": {
5991 + "version": "0.34.5",
5992 + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
5993 + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
5994 + "hasInstallScript": true,
5995 + "license": "Apache-2.0",
5996 + "dependencies": {
5997 + "@img/colour": "^1.0.0",
5998 + "detect-libc": "^2.1.2",
5999 + "semver": "^7.7.3"
6000 + },
6001 + "engines": {
6002 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
6003 + },
6004 + "funding": {
6005 + "url": "https://opencollective.com/libvips"
6006 + },
6007 + "optionalDependencies": {
6008 + "@img/sharp-darwin-arm64": "0.34.5",
6009 + "@img/sharp-darwin-x64": "0.34.5",
6010 + "@img/sharp-libvips-darwin-arm64": "1.2.4",
6011 + "@img/sharp-libvips-darwin-x64": "1.2.4",
6012 + "@img/sharp-libvips-linux-arm": "1.2.4",
6013 + "@img/sharp-libvips-linux-arm64": "1.2.4",
6014 + "@img/sharp-libvips-linux-ppc64": "1.2.4",
6015 + "@img/sharp-libvips-linux-riscv64": "1.2.4",
6016 + "@img/sharp-libvips-linux-s390x": "1.2.4",
6017 + "@img/sharp-libvips-linux-x64": "1.2.4",
6018 + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
6019 + "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
6020 + "@img/sharp-linux-arm": "0.34.5",
6021 + "@img/sharp-linux-arm64": "0.34.5",
6022 + "@img/sharp-linux-ppc64": "0.34.5",
6023 + "@img/sharp-linux-riscv64": "0.34.5",
6024 + "@img/sharp-linux-s390x": "0.34.5",
6025 + "@img/sharp-linux-x64": "0.34.5",
6026 + "@img/sharp-linuxmusl-arm64": "0.34.5",
6027 + "@img/sharp-linuxmusl-x64": "0.34.5",
6028 + "@img/sharp-wasm32": "0.34.5",
6029 + "@img/sharp-win32-arm64": "0.34.5",
6030 + "@img/sharp-win32-ia32": "0.34.5",
6031 + "@img/sharp-win32-x64": "0.34.5"
6032 + }
6033 + },
6034 + "node_modules/shebang-command": {
6035 + "version": "2.0.0",
6036 + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
6037 + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
6038 + "license": "MIT",
6039 + "dependencies": {
6040 + "shebang-regex": "^3.0.0"
6041 + },
6042 + "engines": {
6043 + "node": ">=8"
6044 + }
6045 + },
6046 + "node_modules/shebang-regex": {
6047 + "version": "3.0.0",
6048 + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
6049 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
6050 + "license": "MIT",
6051 + "engines": {
6052 + "node": ">=8"
6053 + }
6054 + },
6055 + "node_modules/shiki": {
6056 + "version": "3.23.0",
6057 + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz",
6058 + "integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==",
6059 + "license": "MIT",
6060 + "dependencies": {
6061 + "@shikijs/core": "3.23.0",
6062 + "@shikijs/engine-javascript": "3.23.0",
6063 + "@shikijs/engine-oniguruma": "3.23.0",
6064 + "@shikijs/langs": "3.23.0",
6065 + "@shikijs/themes": "3.23.0",
6066 + "@shikijs/types": "3.23.0",
6067 + "@shikijs/vscode-textmate": "^10.0.2",
6068 + "@types/hast": "^3.0.4"
6069 + }
6070 + },
6071 + "node_modules/siginfo": {
6072 + "version": "2.0.0",
6073 + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
6074 + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
6075 + "dev": true,
6076 + "license": "ISC"
6077 + },
6078 + "node_modules/signal-exit": {
6079 + "version": "4.1.0",
6080 + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
6081 + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
6082 + "license": "ISC",
6083 + "engines": {
6084 + "node": ">=14"
6085 + },
6086 + "funding": {
6087 + "url": "https://github.com/sponsors/isaacs"
6088 + }
6089 + },
6090 + "node_modules/simple-concat": {
6091 + "version": "1.0.1",
6092 + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
6093 + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
6094 + "funding": [
6095 + {
6096 + "type": "github",
6097 + "url": "https://github.com/sponsors/feross"
6098 + },
6099 + {
6100 + "type": "patreon",
6101 + "url": "https://www.patreon.com/feross"
6102 + },
6103 + {
6104 + "type": "consulting",
6105 + "url": "https://feross.org/support"
6106 + }
6107 + ],
6108 + "license": "MIT"
6109 + },
6110 + "node_modules/simple-get": {
6111 + "version": "4.0.1",
6112 + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
6113 + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
6114 + "funding": [
6115 + {
6116 + "type": "github",
6117 + "url": "https://github.com/sponsors/feross"
6118 + },
6119 + {
6120 + "type": "patreon",
6121 + "url": "https://www.patreon.com/feross"
6122 + },
6123 + {
6124 + "type": "consulting",
6125 + "url": "https://feross.org/support"
6126 + }
6127 + ],
6128 + "license": "MIT",
6129 + "dependencies": {
6130 + "decompress-response": "^6.0.0",
6131 + "once": "^1.3.1",
6132 + "simple-concat": "^1.0.0"
6133 + }
6134 + },
6135 + "node_modules/sonic-boom": {
6136 + "version": "4.2.1",
6137 + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
6138 + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
6139 + "license": "MIT",
6140 + "dependencies": {
6141 + "atomic-sleep": "^1.0.0"
6142 + }
6143 + },
6144 + "node_modules/source-map-js": {
6145 + "version": "1.2.1",
6146 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
6147 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
6148 + "dev": true,
6149 + "license": "BSD-3-Clause",
6150 + "engines": {
6151 + "node": ">=0.10.0"
6152 + }
6153 + },
6154 + "node_modules/space-separated-tokens": {
6155 + "version": "2.0.2",
6156 + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
6157 + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
6158 + "license": "MIT",
6159 + "funding": {
6160 + "type": "github",
6161 + "url": "https://github.com/sponsors/wooorm"
6162 + }
6163 + },
6164 + "node_modules/split2": {
6165 + "version": "4.2.0",
6166 + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
6167 + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
6168 + "license": "ISC",
6169 + "engines": {
6170 + "node": ">= 10.x"
6171 + }
6172 + },
6173 + "node_modules/ssf": {
6174 + "version": "0.11.2",
6175 + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
6176 + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
6177 + "license": "Apache-2.0",
6178 + "dependencies": {
6179 + "frac": "~1.1.2"
6180 + },
6181 + "engines": {
6182 + "node": ">=0.8"
6183 + }
6184 + },
6185 + "node_modules/stackback": {
6186 + "version": "0.0.2",
6187 + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
6188 + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
6189 + "dev": true,
6190 + "license": "MIT"
6191 + },
6192 + "node_modules/statuses": {
6193 + "version": "2.0.2",
6194 + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
6195 + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
6196 + "license": "MIT",
6197 + "engines": {
6198 + "node": ">= 0.8"
6199 + }
6200 + },
6201 + "node_modules/std-env": {
6202 + "version": "3.10.0",
6203 + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
6204 + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
6205 + "dev": true,
6206 + "license": "MIT"
6207 + },
6208 + "node_modules/streamx": {
6209 + "version": "2.28.0",
6210 + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz",
6211 + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==",
6212 + "license": "MIT",
6213 + "dependencies": {
6214 + "events-universal": "^1.0.0",
6215 + "fast-fifo": "^1.3.2",
6216 + "text-decoder": "^1.1.0"
6217 + }
6218 + },
6219 + "node_modules/string_decoder": {
6220 + "version": "1.3.0",
6221 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
6222 + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
6223 + "license": "MIT",
6224 + "dependencies": {
6225 + "safe-buffer": "~5.2.0"
6226 + }
6227 + },
6228 + "node_modules/string-width": {
6229 + "version": "4.2.3",
6230 + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
6231 + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
6232 + "license": "MIT",
6233 + "dependencies": {
6234 + "emoji-regex": "^8.0.0",
6235 + "is-fullwidth-code-point": "^3.0.0",
6236 + "strip-ansi": "^6.0.1"
6237 + },
6238 + "engines": {
6239 + "node": ">=8"
6240 + }
6241 + },
6242 + "node_modules/string-width-cjs": {
6243 + "name": "string-width",
6244 + "version": "4.2.3",
6245 + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
6246 + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
6247 + "license": "MIT",
6248 + "dependencies": {
6249 + "emoji-regex": "^8.0.0",
6250 + "is-fullwidth-code-point": "^3.0.0",
6251 + "strip-ansi": "^6.0.1"
6252 + },
6253 + "engines": {
6254 + "node": ">=8"
6255 + }
6256 + },
6257 + "node_modules/stringify-entities": {
6258 + "version": "4.0.4",
6259 + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
6260 + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
6261 + "license": "MIT",
6262 + "dependencies": {
6263 + "character-entities-html4": "^2.0.0",
6264 + "character-entities-legacy": "^3.0.0"
6265 + },
6266 + "funding": {
6267 + "type": "github",
6268 + "url": "https://github.com/sponsors/wooorm"
6269 + }
6270 + },
6271 + "node_modules/strip-ansi": {
6272 + "version": "6.0.1",
6273 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
6274 + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
6275 + "license": "MIT",
6276 + "dependencies": {
6277 + "ansi-regex": "^5.0.1"
6278 + },
6279 + "engines": {
6280 + "node": ">=8"
6281 + }
6282 + },
6283 + "node_modules/strip-ansi-cjs": {
6284 + "name": "strip-ansi",
6285 + "version": "6.0.1",
6286 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
6287 + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
6288 + "license": "MIT",
6289 + "dependencies": {
6290 + "ansi-regex": "^5.0.1"
6291 + },
6292 + "engines": {
6293 + "node": ">=8"
6294 + }
6295 + },
6296 + "node_modules/strip-json-comments": {
6297 + "version": "3.1.1",
6298 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
6299 + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
6300 + "dev": true,
6301 + "license": "MIT",
6302 + "engines": {
6303 + "node": ">=8"
6304 + },
6305 + "funding": {
6306 + "url": "https://github.com/sponsors/sindresorhus"
6307 + }
6308 + },
6309 + "node_modules/strip-literal": {
6310 + "version": "3.1.0",
6311 + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
6312 + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
6313 + "dev": true,
6314 + "license": "MIT",
6315 + "dependencies": {
6316 + "js-tokens": "^9.0.1"
6317 + },
6318 + "funding": {
6319 + "url": "https://github.com/sponsors/antfu"
6320 + }
6321 + },
6322 + "node_modules/supports-color": {
6323 + "version": "7.2.0",
6324 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
6325 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
6326 + "dev": true,
6327 + "license": "MIT",
6328 + "dependencies": {
6329 + "has-flag": "^4.0.0"
6330 + },
6331 + "engines": {
6332 + "node": ">=8"
6333 + }
6334 + },
6335 + "node_modules/tar-fs": {
6336 + "version": "2.1.5",
6337 + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
6338 + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
6339 + "license": "MIT",
6340 + "dependencies": {
6341 + "chownr": "^1.1.1",
6342 + "mkdirp-classic": "^0.5.2",
6343 + "pump": "^3.0.0",
6344 + "tar-stream": "^2.1.4"
6345 + }
6346 + },
6347 + "node_modules/tar-fs/node_modules/readable-stream": {
6348 + "version": "3.6.2",
6349 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
6350 + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
6351 + "license": "MIT",
6352 + "dependencies": {
6353 + "inherits": "^2.0.3",
6354 + "string_decoder": "^1.1.1",
6355 + "util-deprecate": "^1.0.1"
6356 + },
6357 + "engines": {
6358 + "node": ">= 6"
6359 + }
6360 + },
6361 + "node_modules/tar-fs/node_modules/tar-stream": {
6362 + "version": "2.2.0",
6363 + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
6364 + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
6365 + "license": "MIT",
6366 + "dependencies": {
6367 + "bl": "^4.0.3",
6368 + "end-of-stream": "^1.4.1",
6369 + "fs-constants": "^1.0.0",
6370 + "inherits": "^2.0.3",
6371 + "readable-stream": "^3.1.1"
6372 + },
6373 + "engines": {
6374 + "node": ">=6"
6375 + }
6376 + },
6377 + "node_modules/tar-stream": {
6378 + "version": "3.2.0",
6379 + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz",
6380 + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==",
6381 + "license": "MIT",
6382 + "dependencies": {
6383 + "b4a": "^1.6.4",
6384 + "bare-fs": "^4.5.5",
6385 + "fast-fifo": "^1.2.0",
6386 + "streamx": "^2.15.0"
6387 + }
6388 + },
6389 + "node_modules/teex": {
6390 + "version": "1.0.1",
6391 + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
6392 + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
6393 + "license": "MIT",
6394 + "dependencies": {
6395 + "streamx": "^2.12.5"
6396 + }
6397 + },
6398 + "node_modules/text-decoder": {
6399 + "version": "1.2.7",
6400 + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
6401 + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
6402 + "license": "Apache-2.0",
6403 + "dependencies": {
6404 + "b4a": "^1.6.4"
6405 + }
6406 + },
6407 + "node_modules/thread-stream": {
6408 + "version": "3.2.0",
6409 + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz",
6410 + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==",
6411 + "license": "MIT",
6412 + "dependencies": {
6413 + "real-require": "^0.2.0"
6414 + }
6415 + },
6416 + "node_modules/tinybench": {
6417 + "version": "2.9.0",
6418 + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
6419 + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
6420 + "dev": true,
6421 + "license": "MIT"
6422 + },
6423 + "node_modules/tinyexec": {
6424 + "version": "0.3.2",
6425 + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
6426 + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
6427 + "dev": true,
6428 + "license": "MIT"
6429 + },
6430 + "node_modules/tinyglobby": {
6431 + "version": "0.2.17",
6432 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
6433 + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
6434 + "dev": true,
6435 + "license": "MIT",
6436 + "dependencies": {
6437 + "fdir": "^6.5.0",
6438 + "picomatch": "^4.0.4"
6439 + },
6440 + "engines": {
6441 + "node": ">=12.0.0"
6442 + },
6443 + "funding": {
6444 + "url": "https://github.com/sponsors/SuperchupuDev"
6445 + }
6446 + },
6447 + "node_modules/tinypool": {
6448 + "version": "1.1.1",
6449 + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
6450 + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
6451 + "dev": true,
6452 + "license": "MIT",
6453 + "engines": {
6454 + "node": "^18.0.0 || >=20.0.0"
6455 + }
6456 + },
6457 + "node_modules/tinyrainbow": {
6458 + "version": "2.0.0",
6459 + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
6460 + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
6461 + "dev": true,
6462 + "license": "MIT",
6463 + "engines": {
6464 + "node": ">=14.0.0"
6465 + }
6466 + },
6467 + "node_modules/tinyspy": {
6468 + "version": "4.0.4",
6469 + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
6470 + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
6471 + "dev": true,
6472 + "license": "MIT",
6473 + "engines": {
6474 + "node": ">=14.0.0"
6475 + }
6476 + },
6477 + "node_modules/toad-cache": {
6478 + "version": "3.7.4",
6479 + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz",
6480 + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==",
6481 + "license": "MIT",
6482 + "engines": {
6483 + "node": ">=20"
6484 + }
6485 + },
6486 + "node_modules/toidentifier": {
6487 + "version": "1.0.1",
6488 + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
6489 + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
6490 + "license": "MIT",
6491 + "engines": {
6492 + "node": ">=0.6"
6493 + }
6494 + },
6495 + "node_modules/trim-lines": {
6496 + "version": "3.0.1",
6497 + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
6498 + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
6499 + "license": "MIT",
6500 + "funding": {
6501 + "type": "github",
6502 + "url": "https://github.com/sponsors/wooorm"
6503 + }
6504 + },
6505 + "node_modules/tslib": {
6506 + "version": "2.8.1",
6507 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
6508 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
6509 + "license": "0BSD",
6510 + "optional": true
6511 + },
6512 + "node_modules/tunnel-agent": {
6513 + "version": "0.6.0",
6514 + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
6515 + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
6516 + "license": "Apache-2.0",
6517 + "dependencies": {
6518 + "safe-buffer": "^5.0.1"
6519 + },
6520 + "engines": {
6521 + "node": "*"
6522 + }
6523 + },
6524 + "node_modules/type-check": {
6525 + "version": "0.4.0",
6526 + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
6527 + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
6528 + "dev": true,
6529 + "license": "MIT",
6530 + "dependencies": {
6531 + "prelude-ls": "^1.2.1"
6532 + },
6533 + "engines": {
6534 + "node": ">= 0.8.0"
6535 + }
6536 + },
6537 + "node_modules/uc.micro": {
6538 + "version": "2.1.0",
6539 + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
6540 + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
6541 + "license": "MIT"
6542 + },
6543 + "node_modules/unist-util-is": {
6544 + "version": "6.0.1",
6545 + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
6546 + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
6547 + "license": "MIT",
6548 + "dependencies": {
6549 + "@types/unist": "^3.0.0"
6550 + },
6551 + "funding": {
6552 + "type": "opencollective",
6553 + "url": "https://opencollective.com/unified"
6554 + }
6555 + },
6556 + "node_modules/unist-util-position": {
6557 + "version": "5.0.0",
6558 + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
6559 + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
6560 + "license": "MIT",
6561 + "dependencies": {
6562 + "@types/unist": "^3.0.0"
6563 + },
6564 + "funding": {
6565 + "type": "opencollective",
6566 + "url": "https://opencollective.com/unified"
6567 + }
6568 + },
6569 + "node_modules/unist-util-stringify-position": {
6570 + "version": "4.0.0",
6571 + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
6572 + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
6573 + "license": "MIT",
6574 + "dependencies": {
6575 + "@types/unist": "^3.0.0"
6576 + },
6577 + "funding": {
6578 + "type": "opencollective",
6579 + "url": "https://opencollective.com/unified"
6580 + }
6581 + },
6582 + "node_modules/unist-util-visit": {
6583 + "version": "5.1.0",
6584 + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
6585 + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
6586 + "license": "MIT",
6587 + "dependencies": {
6588 + "@types/unist": "^3.0.0",
6589 + "unist-util-is": "^6.0.0",
6590 + "unist-util-visit-parents": "^6.0.0"
6591 + },
6592 + "funding": {
6593 + "type": "opencollective",
6594 + "url": "https://opencollective.com/unified"
6595 + }
6596 + },
6597 + "node_modules/unist-util-visit-parents": {
6598 + "version": "6.0.2",
6599 + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
6600 + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
6601 + "license": "MIT",
6602 + "dependencies": {
6603 + "@types/unist": "^3.0.0",
6604 + "unist-util-is": "^6.0.0"
6605 + },
6606 + "funding": {
6607 + "type": "opencollective",
6608 + "url": "https://opencollective.com/unified"
6609 + }
6610 + },
6611 + "node_modules/uri-js": {
6612 + "version": "4.4.1",
6613 + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
6614 + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
6615 + "dev": true,
6616 + "license": "BSD-2-Clause",
6617 + "dependencies": {
6618 + "punycode": "^2.1.0"
6619 + }
6620 + },
6621 + "node_modules/util-deprecate": {
6622 + "version": "1.0.2",
6623 + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
6624 + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
6625 + "license": "MIT"
6626 + },
6627 + "node_modules/vfile": {
6628 + "version": "6.0.3",
6629 + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
6630 + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
6631 + "license": "MIT",
6632 + "dependencies": {
6633 + "@types/unist": "^3.0.0",
6634 + "vfile-message": "^4.0.0"
6635 + },
6636 + "funding": {
6637 + "type": "opencollective",
6638 + "url": "https://opencollective.com/unified"
6639 + }
6640 + },
6641 + "node_modules/vfile-message": {
6642 + "version": "4.0.3",
6643 + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
6644 + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
6645 + "license": "MIT",
6646 + "dependencies": {
6647 + "@types/unist": "^3.0.0",
6648 + "unist-util-stringify-position": "^4.0.0"
6649 + },
6650 + "funding": {
6651 + "type": "opencollective",
6652 + "url": "https://opencollective.com/unified"
6653 + }
6654 + },
6655 + "node_modules/vite": {
6656 + "version": "7.3.6",
6657 + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
6658 + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
6659 + "dev": true,
6660 + "license": "MIT",
6661 + "dependencies": {
6662 + "esbuild": "^0.27.0 || ^0.28.0",
6663 + "fdir": "^6.5.0",
6664 + "picomatch": "^4.0.3",
6665 + "postcss": "^8.5.6",
6666 + "rollup": "^4.43.0",
6667 + "tinyglobby": "^0.2.15"
6668 + },
6669 + "bin": {
6670 + "vite": "bin/vite.js"
6671 + },
6672 + "engines": {
6673 + "node": "^20.19.0 || >=22.12.0"
6674 + },
6675 + "funding": {
6676 + "url": "https://github.com/vitejs/vite?sponsor=1"
6677 + },
6678 + "optionalDependencies": {
6679 + "fsevents": "~2.3.3"
6680 + },
6681 + "peerDependencies": {
6682 + "@types/node": "^20.19.0 || >=22.12.0",
6683 + "jiti": ">=1.21.0",
6684 + "less": "^4.0.0",
6685 + "lightningcss": "^1.21.0",
6686 + "sass": "^1.70.0",
6687 + "sass-embedded": "^1.70.0",
6688 + "stylus": ">=0.54.8",
6689 + "sugarss": "^5.0.0",
6690 + "terser": "^5.16.0",
6691 + "tsx": "^4.8.1",
6692 + "yaml": "^2.4.2"
6693 + },
6694 + "peerDependenciesMeta": {
6695 + "@types/node": {
6696 + "optional": true
6697 + },
6698 + "jiti": {
6699 + "optional": true
6700 + },
6701 + "less": {
6702 + "optional": true
6703 + },
6704 + "lightningcss": {
6705 + "optional": true
6706 + },
6707 + "sass": {
6708 + "optional": true
6709 + },
6710 + "sass-embedded": {
6711 + "optional": true
6712 + },
6713 + "stylus": {
6714 + "optional": true
6715 + },
6716 + "sugarss": {
6717 + "optional": true
6718 + },
6719 + "terser": {
6720 + "optional": true
6721 + },
6722 + "tsx": {
6723 + "optional": true
6724 + },
6725 + "yaml": {
6726 + "optional": true
6727 + }
6728 + }
6729 + },
6730 + "node_modules/vite-node": {
6731 + "version": "3.2.4",
6732 + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
6733 + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
6734 + "dev": true,
6735 + "license": "MIT",
6736 + "dependencies": {
6737 + "cac": "^6.7.14",
6738 + "debug": "^4.4.1",
6739 + "es-module-lexer": "^1.7.0",
6740 + "pathe": "^2.0.3",
6741 + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
6742 + },
6743 + "bin": {
6744 + "vite-node": "vite-node.mjs"
6745 + },
6746 + "engines": {
6747 + "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
6748 + },
6749 + "funding": {
6750 + "url": "https://opencollective.com/vitest"
6751 + }
6752 + },
6753 + "node_modules/vitest": {
6754 + "version": "3.2.7",
6755 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
6756 + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
6757 + "dev": true,
6758 + "license": "MIT",
6759 + "dependencies": {
6760 + "@types/chai": "^5.2.2",
6761 + "@vitest/expect": "3.2.7",
6762 + "@vitest/mocker": "3.2.7",
6763 + "@vitest/pretty-format": "^3.2.7",
6764 + "@vitest/runner": "3.2.7",
6765 + "@vitest/snapshot": "3.2.7",
6766 + "@vitest/spy": "3.2.7",
6767 + "@vitest/utils": "3.2.7",
6768 + "chai": "^5.2.0",
6769 + "debug": "^4.4.1",
6770 + "expect-type": "^1.2.1",
6771 + "magic-string": "^0.30.17",
6772 + "pathe": "^2.0.3",
6773 + "picomatch": "^4.0.2",
6774 + "std-env": "^3.9.0",
6775 + "tinybench": "^2.9.0",
6776 + "tinyexec": "^0.3.2",
6777 + "tinyglobby": "^0.2.14",
6778 + "tinypool": "^1.1.1",
6779 + "tinyrainbow": "^2.0.0",
6780 + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
6781 + "vite-node": "3.2.4",
6782 + "why-is-node-running": "^2.3.0"
6783 + },
6784 + "bin": {
6785 + "vitest": "vitest.mjs"
6786 + },
6787 + "engines": {
6788 + "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
6789 + },
6790 + "funding": {
6791 + "url": "https://opencollective.com/vitest"
6792 + },
6793 + "peerDependencies": {
6794 + "@edge-runtime/vm": "*",
6795 + "@types/debug": "^4.1.12",
6796 + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
6797 + "@vitest/browser": "3.2.7",
6798 + "@vitest/ui": "3.2.7",
6799 + "happy-dom": "*",
6800 + "jsdom": "*"
6801 + },
6802 + "peerDependenciesMeta": {
6803 + "@edge-runtime/vm": {
6804 + "optional": true
6805 + },
6806 + "@types/debug": {
6807 + "optional": true
6808 + },
6809 + "@types/node": {
6810 + "optional": true
6811 + },
6812 + "@vitest/browser": {
6813 + "optional": true
6814 + },
6815 + "@vitest/ui": {
6816 + "optional": true
6817 + },
6818 + "happy-dom": {
6819 + "optional": true
6820 + },
6821 + "jsdom": {
6822 + "optional": true
6823 + }
6824 + }
6825 + },
6826 + "node_modules/wavesurfer.js": {
6827 + "version": "7.12.11",
6828 + "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.12.11.tgz",
6829 + "integrity": "sha512-Sx0yZIz7jRJ9J9p7UL+pl9y0UQBB6UN/XNo+R7Gy4tCR5xZI9jMyA0dnt1R8TMVSQ4LZ10SeB2HJYQ+dV2dPSA==",
6830 + "license": "BSD-3-Clause"
6831 + },
6832 + "node_modules/which": {
6833 + "version": "2.0.2",
6834 + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
6835 + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
6836 + "license": "ISC",
6837 + "dependencies": {
6838 + "isexe": "^2.0.0"
6839 + },
6840 + "bin": {
6841 + "node-which": "bin/node-which"
6842 + },
6843 + "engines": {
6844 + "node": ">= 8"
6845 + }
6846 + },
6847 + "node_modules/which-module": {
6848 + "version": "2.0.1",
6849 + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
6850 + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
6851 + "license": "ISC"
6852 + },
6853 + "node_modules/why-is-node-running": {
6854 + "version": "2.3.0",
6855 + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
6856 + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
6857 + "dev": true,
6858 + "license": "MIT",
6859 + "dependencies": {
6860 + "siginfo": "^2.0.0",
6861 + "stackback": "0.0.2"
6862 + },
6863 + "bin": {
6864 + "why-is-node-running": "cli.js"
6865 + },
6866 + "engines": {
6867 + "node": ">=8"
6868 + }
6869 + },
6870 + "node_modules/wmf": {
6871 + "version": "1.0.2",
6872 + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
6873 + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
6874 + "license": "Apache-2.0",
6875 + "engines": {
6876 + "node": ">=0.8"
6877 + }
6878 + },
6879 + "node_modules/word": {
6880 + "version": "0.3.0",
6881 + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
6882 + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
6883 + "license": "Apache-2.0",
6884 + "engines": {
6885 + "node": ">=0.8"
6886 + }
6887 + },
6888 + "node_modules/word-wrap": {
6889 + "version": "1.2.5",
6890 + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
6891 + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
6892 + "dev": true,
6893 + "license": "MIT",
6894 + "engines": {
6895 + "node": ">=0.10.0"
6896 + }
6897 + },
6898 + "node_modules/wrap-ansi": {
6899 + "version": "6.2.0",
6900 + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
6901 + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
6902 + "license": "MIT",
6903 + "dependencies": {
6904 + "ansi-styles": "^4.0.0",
6905 + "string-width": "^4.1.0",
6906 + "strip-ansi": "^6.0.0"
6907 + },
6908 + "engines": {
6909 + "node": ">=8"
6910 + }
6911 + },
6912 + "node_modules/wrap-ansi-cjs": {
6913 + "name": "wrap-ansi",
6914 + "version": "7.0.0",
6915 + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
6916 + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
6917 + "license": "MIT",
6918 + "dependencies": {
6919 + "ansi-styles": "^4.0.0",
6920 + "string-width": "^4.1.0",
6921 + "strip-ansi": "^6.0.0"
6922 + },
6923 + "engines": {
6924 + "node": ">=10"
6925 + },
6926 + "funding": {
6927 + "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
6928 + }
6929 + },
6930 + "node_modules/wrappy": {
6931 + "version": "1.0.2",
6932 + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
6933 + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
6934 + "license": "ISC"
6935 + },
6936 + "node_modules/xlsx": {
6937 + "version": "0.18.5",
6938 + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
6939 + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
6940 + "license": "Apache-2.0",
6941 + "dependencies": {
6942 + "adler-32": "~1.3.0",
6943 + "cfb": "~1.2.1",
6944 + "codepage": "~1.15.0",
6945 + "crc-32": "~1.2.1",
6946 + "ssf": "~0.11.2",
6947 + "wmf": "~1.0.1",
6948 + "word": "~0.3.0"
6949 + },
6950 + "bin": {
6951 + "xlsx": "bin/xlsx.njs"
6952 + },
6953 + "engines": {
6954 + "node": ">=0.8"
6955 + }
6956 + },
6957 + "node_modules/y18n": {
6958 + "version": "4.0.3",
6959 + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
6960 + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
6961 + "license": "ISC"
6962 + },
6963 + "node_modules/yargs": {
6964 + "version": "15.4.1",
6965 + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
6966 + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
6967 + "license": "MIT",
6968 + "dependencies": {
6969 + "cliui": "^6.0.0",
6970 + "decamelize": "^1.2.0",
6971 + "find-up": "^4.1.0",
6972 + "get-caller-file": "^2.0.1",
6973 + "require-directory": "^2.1.1",
6974 + "require-main-filename": "^2.0.0",
6975 + "set-blocking": "^2.0.0",
6976 + "string-width": "^4.2.0",
6977 + "which-module": "^2.0.0",
6978 + "y18n": "^4.0.0",
6979 + "yargs-parser": "^18.1.2"
6980 + },
6981 + "engines": {
6982 + "node": ">=8"
6983 + }
6984 + },
6985 + "node_modules/yargs-parser": {
6986 + "version": "18.1.3",
6987 + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
6988 + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
6989 + "license": "ISC",
6990 + "dependencies": {
6991 + "camelcase": "^5.0.0",
6992 + "decamelize": "^1.2.0"
6993 + },
6994 + "engines": {
6995 + "node": ">=6"
6996 + }
6997 + },
6998 + "node_modules/yargs/node_modules/find-up": {
6999 + "version": "4.1.0",
7000 + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
7001 + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
7002 + "license": "MIT",
7003 + "dependencies": {
7004 + "locate-path": "^5.0.0",
7005 + "path-exists": "^4.0.0"
7006 + },
7007 + "engines": {
7008 + "node": ">=8"
7009 + }
7010 + },
7011 + "node_modules/yargs/node_modules/locate-path": {
7012 + "version": "5.0.0",
7013 + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
7014 + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
7015 + "license": "MIT",
7016 + "dependencies": {
7017 + "p-locate": "^4.1.0"
7018 + },
7019 + "engines": {
7020 + "node": ">=8"
7021 + }
7022 + },
7023 + "node_modules/yargs/node_modules/p-limit": {
7024 + "version": "2.3.0",
7025 + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
7026 + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
7027 + "license": "MIT",
7028 + "dependencies": {
7029 + "p-try": "^2.0.0"
7030 + },
7031 + "engines": {
7032 + "node": ">=6"
7033 + },
7034 + "funding": {
7035 + "url": "https://github.com/sponsors/sindresorhus"
7036 + }
7037 + },
7038 + "node_modules/yargs/node_modules/p-locate": {
7039 + "version": "4.1.0",
7040 + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
7041 + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
7042 + "license": "MIT",
7043 + "dependencies": {
7044 + "p-limit": "^2.2.0"
7045 + },
7046 + "engines": {
7047 + "node": ">=8"
7048 + }
7049 + },
7050 + "node_modules/yauzl": {
7051 + "version": "3.4.0",
7052 + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz",
7053 + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==",
7054 + "license": "MIT",
7055 + "dependencies": {
7056 + "pend": "~1.2.0"
7057 + },
7058 + "engines": {
7059 + "node": ">=12"
7060 + }
7061 + },
7062 + "node_modules/yocto-queue": {
7063 + "version": "0.1.0",
7064 + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
7065 + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
7066 + "dev": true,
7067 + "license": "MIT",
7068 + "engines": {
7069 + "node": ">=10"
7070 + },
7071 + "funding": {
7072 + "url": "https://github.com/sponsors/sindresorhus"
7073 + }
7074 + },
7075 + "node_modules/zip-stream": {
7076 + "version": "6.0.1",
7077 + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
7078 + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==",
7079 + "license": "MIT",
7080 + "dependencies": {
7081 + "archiver-utils": "^5.0.0",
7082 + "compress-commons": "^6.0.2",
7083 + "readable-stream": "^4.0.0"
7084 + },
7085 + "engines": {
7086 + "node": ">= 14"
7087 + }
7088 + },
7089 + "node_modules/zod": {
7090 + "version": "3.25.76",
7091 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
7092 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
7093 + "license": "MIT",
7094 + "funding": {
7095 + "url": "https://github.com/sponsors/colinhacks"
7096 + }
7097 + },
7098 + "node_modules/zwitch": {
7099 + "version": "2.0.4",
7100 + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
7101 + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
7102 + "license": "MIT",
7103 + "funding": {
7104 + "type": "github",
7105 + "url": "https://github.com/sponsors/wooorm"
7106 + }
7107 + }
7108 + }
7109 +}
added package.json +57 −0
@@ -0,0 +1,57 @@
1 +{
2 + "name": "spbdrive",
3 + "version": "1.0.0",
4 + "description": "SPB Drive — Personal Cloud Drive for Simon-Pierre Boucher",
5 + "author": "Simon-Pierre Boucher <contact@spboucher.ai>",
6 + "license": "MIT",
7 + "type": "module",
8 + "engines": {
9 + "node": ">=20"
10 + },
11 + "bin": {
12 + "spbdrive": "cli/spbdrive.mjs"
13 + },
14 + "scripts": {
15 + "start": "node src/server.mjs",
16 + "dev": "SPBDRIVE_DATA_DIR=./data node src/server.mjs",
17 + "test": "vitest run",
18 + "lint": "eslint src cli scripts test",
19 + "check:headers": "node scripts/check-headers.mjs",
20 + "check:secrets": "node scripts/check-secrets.mjs",
21 + "check": "npm run lint && npm run check:headers && npm run check:secrets && npm run test"
22 + },
23 + "dependencies": {
24 + "@fastify/cookie": "^11.0.2",
25 + "@fastify/formbody": "^8.0.2",
26 + "@fastify/static": "^8.1.1",
27 + "@fontsource/inter": "^5.2.5",
28 + "@fontsource/jetbrains-mono": "^5.2.5",
29 + "archiver": "^7.0.1",
30 + "argon2": "^0.44.0",
31 + "better-sqlite3": "^12.2.0",
32 + "commander": "^14.0.0",
33 + "exif-reader": "^2.0.2",
34 + "fastify": "^5.4.0",
35 + "markdown-it": "^14.1.0",
36 + "markdown-it-anchor": "^9.2.0",
37 + "markdown-it-task-lists": "^2.1.1",
38 + "mime": "^4.0.7",
39 + "nunjucks": "^3.2.4",
40 + "pdfjs-dist": "^4.10.38",
41 + "pino": "^9.7.0",
42 + "postal-mime": "^2.4.3",
43 + "qrcode": "^1.5.4",
44 + "sharp": "^0.34.2",
45 + "shiki": "^3.7.0",
46 + "tar-stream": "^3.1.7",
47 + "wavesurfer.js": "^7.9.5",
48 + "xlsx": "^0.18.5",
49 + "yauzl": "^3.2.0",
50 + "zod": "^3.25.67"
51 + },
52 + "devDependencies": {
53 + "eslint": "^9.30.0",
54 + "pino-pretty": "^13.0.0",
55 + "vitest": "^3.2.4"
56 + }
57 +}
added scripts/check-headers.mjs +68 −0
@@ -0,0 +1,68 @@
1 +#!/usr/bin/env node
2 +/**
3 + * ─────────────────────────────────────────────
4 + * SPB Drive — Personal Cloud Drive
5 + * ─────────────────────────────────────────────
6 + * Author : Simon-Pierre Boucher
7 + * Contact : contact@spboucher.ai
8 + * File : scripts/check-headers.mjs
9 + * Purpose : CI gate — fail if any tracked source file lacks the author header
10 + * License : MIT © Simon-Pierre Boucher
11 + * ─────────────────────────────────────────────
12 + */
13 +
14 +import { execSync } from 'node:child_process';
15 +import { readFileSync } from 'node:fs';
16 +
17 +/** Extensions that must carry the author header. */
18 +const CHECKED_EXT = new Set([
19 + '.js', '.mjs', '.cjs', '.ts', '.css', '.scss', '.py', '.sh', '.bash',
20 + '.yml', '.yaml', '.toml', '.sql', '.html', '.njk', '.md',
21 +]);
22 +
23 +/** Files checked regardless of extension. */
24 +const CHECKED_NAMES = new Set(['Dockerfile', '.gitignore']);
25 +
26 +/** Files exempt from the header requirement (external formats / lockfiles). */
27 +const EXEMPT = new Set([
28 + 'package.json', 'package-lock.json', 'CLAUDE.md',
29 +]);
30 +
31 +const REQUIRED = ['Simon-Pierre Boucher', 'contact@spboucher.ai', 'SPB Drive'];
32 +const HEAD_BYTES = 1200;
33 +
34 +function trackedFiles() {
35 + const out = execSync('git ls-files', { encoding: 'utf8' });
36 + return out.split('\n').filter(Boolean);
37 +}
38 +
39 +function needsHeader(file) {
40 + const base = file.split('/').pop();
41 + if (EXEMPT.has(base) || EXEMPT.has(file)) return false;
42 + if (CHECKED_NAMES.has(base)) return true;
43 + const dot = base.lastIndexOf('.');
44 + if (dot < 0) return false;
45 + return CHECKED_EXT.has(base.slice(dot).toLowerCase());
46 +}
47 +
48 +const failures = [];
49 +for (const file of trackedFiles()) {
50 + if (!needsHeader(file)) continue;
51 + let head = '';
52 + try {
53 + head = readFileSync(file, 'utf8').slice(0, HEAD_BYTES);
54 + } catch {
55 + continue; // deleted-but-tracked edge case
56 + }
57 + const missing = REQUIRED.filter((needle) => !head.includes(needle));
58 + if (missing.length > 0) failures.push({ file, missing });
59 +}
60 +
61 +if (failures.length > 0) {
62 + console.error('✗ Author header missing or incomplete in:');
63 + for (const { file, missing } of failures) {
64 + console.error(` ${file} (missing: ${missing.join(', ')})`);
65 + }
66 + process.exit(1);
67 +}
68 +console.log('✓ check:headers — every tracked file carries the author header.');
added scripts/check-secrets.mjs +38 −0
@@ -0,0 +1,38 @@
1 +#!/usr/bin/env node
2 +/**
3 + * ─────────────────────────────────────────────
4 + * SPB Drive — Personal Cloud Drive
5 + * ─────────────────────────────────────────────
6 + * Author : Simon-Pierre Boucher
7 + * Contact : contact@spboucher.ai
8 + * File : scripts/check-secrets.mjs
9 + * Purpose : CI gate — fail if any forbidden secret literal appears in the repo
10 + * License : MIT © Simon-Pierre Boucher
11 + * ─────────────────────────────────────────────
12 + */
13 +
14 +import { execSync } from 'node:child_process';
15 +import { readFileSync } from 'node:fs';
16 +
17 +// Forbidden literals are stored base64-encoded so this checker never
18 +// contains the plaintext it is guarding against.
19 +const FORBIDDEN = ['c24xOGJyYWR5'].map((b64) => Buffer.from(b64, 'base64').toString('utf8'));
20 +
21 +const files = execSync('git ls-files', { encoding: 'utf8' }).split('\n').filter(Boolean);
22 +const hits = [];
23 +
24 +for (const file of files) {
25 + let buf;
26 + try { buf = readFileSync(file); } catch { continue; }
27 + const text = buf.toString('utf8');
28 + for (const secret of FORBIDDEN) {
29 + if (text.includes(secret)) hits.push(file);
30 + }
31 +}
32 +
33 +if (hits.length > 0) {
34 + console.error('✗ Forbidden secret literal found in:');
35 + for (const file of hits) console.error(` ${file}`);
36 + process.exit(1);
37 +}
38 +console.log('✓ check:secrets — no forbidden literals in tracked files.');
added scripts/inject-headers.mjs +69 −0
@@ -0,0 +1,69 @@
1 +#!/usr/bin/env node
2 +/**
3 + * ─────────────────────────────────────────────
4 + * SPB Drive — Personal Cloud Drive
5 + * ─────────────────────────────────────────────
6 + * Author : Simon-Pierre Boucher
7 + * Contact : contact@spboucher.ai
8 + * File : scripts/inject-headers.mjs
9 + * Purpose : Prepend the canonical author header to any tracked file missing it
10 + * License : MIT © Simon-Pierre Boucher
11 + * ─────────────────────────────────────────────
12 + */
13 +
14 +import { execSync } from 'node:child_process';
15 +import { readFileSync, writeFileSync } from 'node:fs';
16 +
17 +const LINES = (file, purpose) => [
18 + '─────────────────────────────────────────────',
19 + ' SPB Drive — Personal Cloud Drive',
20 + '─────────────────────────────────────────────',
21 + ' Author : Simon-Pierre Boucher',
22 + ' Contact : contact@spboucher.ai',
23 + ` File : ${file}`,
24 + ` Purpose : ${purpose}`,
25 + ' License : MIT © Simon-Pierre Boucher',
26 + '─────────────────────────────────────────────',
27 +];
28 +
29 +/** Comment wrappers per extension family. */
30 +const STYLES = {
31 + block: (lines) => `/**\n${lines.map((l) => ` * ${l}`.trimEnd()).join('\n')}\n */\n\n`,
32 + hash: (lines) => `${lines.map((l) => `# ${l}`.trimEnd()).join('\n')}\n\n`,
33 + html: (lines) => `<!--\n${lines.map((l) => ` ${l}`.trimEnd()).join('\n')}\n-->\n\n`,
34 + sql: (lines) => `${lines.map((l) => `-- ${l}`.trimEnd()).join('\n')}\n\n`,
35 +};
36 +
37 +const EXT_STYLE = {
38 + '.js': 'block', '.mjs': 'block', '.cjs': 'block', '.ts': 'block',
39 + '.css': 'block', '.scss': 'block',
40 + '.py': 'hash', '.sh': 'hash', '.bash': 'hash', '.yml': 'hash', '.yaml': 'hash', '.toml': 'hash',
41 + '.html': 'html', '.njk': 'html', '.md': 'html',
42 + '.sql': 'sql',
43 +};
44 +
45 +const files = execSync('git ls-files', { encoding: 'utf8' }).split('\n').filter(Boolean);
46 +let injected = 0;
47 +
48 +for (const file of files) {
49 + const base = file.split('/').pop();
50 + const dot = base.lastIndexOf('.');
51 + const style = dot >= 0 ? EXT_STYLE[base.slice(dot).toLowerCase()] : null;
52 + if (!style) continue;
53 + let text;
54 + try { text = readFileSync(file, 'utf8'); } catch { continue; }
55 + if (text.slice(0, 1200).includes('contact@spboucher.ai')) continue;
56 +
57 + let header = STYLES[style](LINES(file, 'Part of SPB Drive'));
58 + let body = text;
59 + if (body.startsWith('#!')) {
60 + const nl = body.indexOf('\n') + 1;
61 + header = body.slice(0, nl) + header;
62 + body = body.slice(nl);
63 + }
64 + writeFileSync(file, header + body);
65 + console.log(`+ header → ${file}`);
66 + injected += 1;
67 +}
68 +
69 +console.log(injected > 0 ? `✓ Injected ${injected} header(s).` : '✓ Nothing to inject.');
added scripts/reset-password.mjs +53 −0
@@ -0,0 +1,53 @@
1 +#!/usr/bin/env node
2 +/**
3 + * ─────────────────────────────────────────────
4 + * SPB Drive — Personal Cloud Drive
5 + * ─────────────────────────────────────────────
6 + * Author : Simon-Pierre Boucher
7 + * Contact : contact@spboucher.ai
8 + * File : scripts/reset-password.mjs
9 + * Purpose : Break-glass local password reset (interactive, server-side only)
10 + * License : MIT © Simon-Pierre Boucher
11 + * ─────────────────────────────────────────────
12 + */
13 +
14 +import readline from 'node:readline';
15 +import { resetPassword } from '../src/auth/password.mjs';
16 +import { ensureDataDirs } from '../src/config.mjs';
17 +
18 +function ask(question, { hidden = false } = {}) {
19 + return new Promise((resolve) => {
20 + const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
21 + if (hidden) {
22 + const orig = rl._writeToOutput.bind(rl);
23 + rl._writeToOutput = (s) => { if (s.includes(question)) orig(question); };
24 + rl.question(question, (answer) => {
25 + rl._writeToOutput = orig;
26 + process.stdout.write('\n');
27 + rl.close();
28 + resolve(answer);
29 + });
30 + } else {
31 + rl.question(question, (answer) => { rl.close(); resolve(answer); });
32 + }
33 + });
34 +}
35 +
36 +if (!process.stdin.isTTY) {
37 + console.error('reset-password must be run interactively on the server.');
38 + process.exit(2);
39 +}
40 +
41 +ensureDataDirs();
42 +const pw1 = await ask('New SPB Drive password: ', { hidden: true });
43 +if (!pw1 || pw1.length < 6) {
44 + console.error('Password must be at least 6 characters.');
45 + process.exit(1);
46 +}
47 +const pw2 = await ask('Repeat: ', { hidden: true });
48 +if (pw1 !== pw2) {
49 + console.error('Passwords do not match.');
50 + process.exit(1);
51 +}
52 +await resetPassword(pw1);
53 +console.log('✓ Password updated. Existing sessions remain valid — revoke them from Settings if needed.');
added src/api/v1.mjs +490 −0
@@ -0,0 +1,490 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/api/v1.mjs
8 + * Purpose : JSON API /api/v1 — nodes, uploads, shares, tags, search, auth
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import QRCode from 'qrcode';
14 +import mime from 'mime';
15 +import archiver from 'archiver';
16 +import { config } from '../config.mjs';
17 +import { getDb, logActivity } from '../db/db.mjs';
18 +import {
19 + changePassword, checkLockout, clearLoginFailures, recordLoginFailure, verifyPassword,
20 +} from '../auth/password.mjs';
21 +import {
22 + SESSION_COOKIE, createApiToken, createSession, destroySession, listApiTokens,
23 + listSessions, revokeAllSessions, revokeApiToken, revokeSession,
24 +} from '../auth/session.mjs';
25 +import { storageStats } from '../storage/blobs.mjs';
26 +import {
27 + ROOT_ID, childrenOf, copyNode, deleteForever, duplicateNode, getNode,
28 + listDescendantFiles, listTrash, mkdir, moveNode, mustGetNode, pathOf,
29 + purgeTrash, recentFiles, renameNode, resolvePath, restoreNode, trashNode,
30 +} from '../storage/nodes.mjs';
31 +import {
32 + abortUpload, chunksPresent, completeUpload, getUpload, initUpload, writeChunk,
33 +} from '../storage/upload.mjs';
34 +import {
35 + createShare, getShareById, listShares, revokeShare, shareEvents, sharesForNode, updateShare,
36 +} from '../shares/shares.mjs';
37 +import { searchNodes } from '../search/search.mjs';
38 +import { extractNodeText } from '../search/extract.mjs';
39 +import { updateFtsTags } from '../search/index-sync.mjs';
40 +import { previewStrategy, thumbKind, iconFamily } from '../preview/router.mjs';
41 +import {
42 + describePreview, handleArchive, handleArchiveMember, handleExif, handleHeic,
43 + handleMarkdown, handleOfficePdf, handlePeaks, handleRawText, handleText,
44 + handleVideoFile, handleVideoStatus,
45 +} from '../preview/handlers.mjs';
46 +import { apiError, makeRateLimiter } from '../web/http-helpers.mjs';
47 +
48 +/** Serialize a node row for API responses. */
49 +export function nodeJson(node) {
50 + if (!node) return null;
51 + const tags = getDb()
52 + .prepare('SELECT t.id, t.name, t.color FROM node_tags nt JOIN tags t ON t.id = nt.tag_id WHERE nt.node_id = ?')
53 + .all(node.id);
54 + return {
55 + id: node.id,
56 + parentId: node.parent_id,
57 + name: node.name,
58 + type: node.type,
59 + size: node.size,
60 + mime: node.mime,
61 + created: node.created,
62 + modified: node.modified,
63 + starred: Boolean(node.starred),
64 + color: node.color,
65 + emoji: node.emoji,
66 + trashedAt: node.trashed_at,
67 + strategy: node.type === 'file' ? previewStrategy(node).strategy : null,
68 + icon: iconFamily(node),
69 + hasThumb: node.type === 'file' ? thumbKind(node) !== null : false,
70 + tags,
71 + };
72 +}
73 +
74 +function shareJson(share) {
75 + return {
76 + id: share.id,
77 + token: share.token,
78 + url: `${config.publicUrl}/s/${share.token}`,
79 + nodeId: share.node_id,
80 + nodeName: share.node_name,
81 + nodeType: share.node_type,
82 + created: share.created,
83 + expiresAt: share.expires_at,
84 + hasPassword: Boolean(share.password_hash),
85 + maxDownloads: share.max_downloads,
86 + downloads: share.downloads,
87 + visits: share.visits,
88 + allowDownload: Boolean(share.allow_download),
89 + label: share.label,
90 + revokedAt: share.revoked_at,
91 + };
92 +}
93 +
94 +/** Register every /api/v1 route. */
95 +export function registerApiV1(app) {
96 + const loginLimiter = makeRateLimiter({ windowMs: 60_000, max: 20 });
97 +
98 + // ── Auth (no session required) ─────────────────────────────────────
99 + app.post('/api/v1/auth/login', async (req, reply) => {
100 + if (!loginLimiter(req, reply)) return undefined;
101 + const lock = checkLockout(req.ip);
102 + if (lock.locked) {
103 + return apiError(reply, 429, 'locked_out',
104 + `Too many attempts. Retry in ${Math.ceil((lock.retryAfterMs ?? 0) / 1000)}s`);
105 + }
106 + const { password, remember } = req.body ?? {};
107 + if (!(await verifyPassword(password))) {
108 + recordLoginFailure(req.ip);
109 + return apiError(reply, 401, 'bad_password', 'Wrong password');
110 + }
111 + clearLoginFailures(req.ip);
112 + const session = createSession({
113 + remember: Boolean(remember),
114 + ip: req.ip,
115 + ua: req.headers['user-agent'] ?? '',
116 + });
117 + reply.setCookie(SESSION_COOKIE, session.token, {
118 + path: '/', httpOnly: true, secure: true, sameSite: 'lax', maxAge: session.maxAgeSec,
119 + });
120 + return { ok: true };
121 + });
122 +
123 + // Everything below requires auth (session cookie or Bearer API token) +
124 + // a CSRF header on state-changing calls when cookie-authenticated.
125 + app.addHook('preHandler', async (req, reply) => {
126 + if (req.url.startsWith('/api/v1/auth/login')) return;
127 + if (!req.url.startsWith('/api/v1/')) return;
128 + if (!req.authed) return apiError(reply, 401, 'unauthorized', 'Login required');
129 + if (req.authKind === 'session' && !['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
130 + if (req.headers['x-spbdrive-csrf'] !== '1') {
131 + return apiError(reply, 403, 'csrf', 'Missing CSRF header');
132 + }
133 + }
134 + return undefined;
135 + });
136 +
137 + app.post('/api/v1/auth/logout', async (req, reply) => {
138 + destroySession(req.cookies?.[SESSION_COOKIE]);
139 + reply.clearCookie(SESSION_COOKIE, { path: '/' });
140 + return { ok: true };
141 + });
142 +
143 + app.post('/api/v1/auth/password', async (req, reply) => {
144 + const { current, next } = req.body ?? {};
145 + const ok = await changePassword(current, next).catch((err) =>
146 + apiError(reply, 400, 'weak_password', err.message));
147 + if (ok === false) return apiError(reply, 403, 'bad_password', 'Current password is wrong');
148 + return { ok: true };
149 + });
150 +
151 + app.get('/api/v1/auth/sessions', async () => ({ sessions: listSessions() }));
152 + app.delete('/api/v1/auth/sessions/:id', async (req) => ({ ok: revokeSession(Number(req.params.id)) }));
153 + app.delete('/api/v1/auth/sessions', async (req, reply) => {
154 + revokeAllSessions();
155 + reply.clearCookie(SESSION_COOKIE, { path: '/' });
156 + return { ok: true };
157 + });
158 +
159 + app.get('/api/v1/auth/api-tokens', async () => ({ tokens: listApiTokens() }));
160 + app.post('/api/v1/auth/api-tokens', async (req) => createApiToken(req.body?.name ?? 'token'));
161 + app.delete('/api/v1/auth/api-tokens/:id', async (req) => ({ ok: revokeApiToken(Number(req.params.id)) }));
162 +
163 + app.get('/api/v1/me', async (req) => ({
164 + user: 'Simon-Pierre Boucher',
165 + email: 'contact@spboucher.ai',
166 + authKind: req.authKind,
167 + rootId: ROOT_ID,
168 + }));
169 +
170 + app.get('/api/v1/stats', async () => storageStats());
171 +
172 + // ── Nodes ───────────────────────────────────────────────────────────
173 + app.get('/api/v1/nodes/:id', async (req, reply) => {
174 + const node = getNode(Number(req.params.id));
175 + if (!node) return apiError(reply, 404, 'not_found', 'No such node');
176 + return { node: nodeJson(node), path: pathOf(node.id).map((n) => ({ id: n.id, name: n.name })) };
177 + });
178 +
179 + app.get('/api/v1/nodes/:id/children', async (req, reply) => {
180 + const id = Number(req.params.id);
181 + const node = getNode(id);
182 + if (!node) return apiError(reply, 404, 'not_found', 'No such folder');
183 + const { sort = 'name', dir = 'asc' } = req.query;
184 + return {
185 + children: childrenOf(id, { sort, dir }).map(nodeJson),
186 + path: pathOf(id).map((n) => ({ id: n.id, name: n.name })),
187 + };
188 + });
189 +
190 + // Full folder tree for the sidebar (folders only).
191 + app.get('/api/v1/tree', async () => {
192 + const rows = getDb()
193 + .prepare("SELECT id, parent_id, name, color, emoji FROM nodes WHERE type = 'folder' AND trashed_at IS NULL ORDER BY name COLLATE NOCASE")
194 + .all();
195 + return { folders: rows };
196 + });
197 +
198 + app.get('/api/v1/resolve', async (req, reply) => {
199 + const node = resolvePath(String(req.query.path ?? '/'));
200 + if (!node) return apiError(reply, 404, 'not_found', 'Path not found');
201 + return { node: nodeJson(node) };
202 + });
203 +
204 + app.post('/api/v1/nodes', async (req, reply) => {
205 + const { parentId, name, type } = req.body ?? {};
206 + if (type !== 'folder') return apiError(reply, 400, 'bad_type', 'Only folders are created here');
207 + const node = mkdir(Number(parentId ?? ROOT_ID), String(name ?? ''), { ip: req.ip });
208 + return { node: nodeJson(node) };
209 + });
210 +
211 + app.patch('/api/v1/nodes/:id', async (req, reply) => {
212 + const id = Number(req.params.id);
213 + let node = mustGetNode(id);
214 + const body = req.body ?? {};
215 + if (body.name !== undefined && body.name !== node.name) {
216 + node = renameNode(id, String(body.name), { ip: req.ip });
217 + }
218 + if (body.parentId !== undefined && Number(body.parentId) !== node.parent_id) {
219 + const moved = moveNode(id, Number(body.parentId), { conflict: body.conflict ?? 'keep-both', ip: req.ip });
220 + if (moved === null) return { node: null, skipped: true };
221 + node = moved;
222 + }
223 + const db = getDb();
224 + if (body.starred !== undefined) {
225 + db.prepare('UPDATE nodes SET starred = ? WHERE id = ?').run(body.starred ? 1 : 0, id);
226 + logActivity(body.starred ? 'node.star' : 'node.unstar', { nodeId: id, ip: req.ip });
227 + }
228 + if (body.color !== undefined) db.prepare('UPDATE nodes SET color = ? WHERE id = ?').run(body.color, id);
229 + if (body.emoji !== undefined) db.prepare('UPDATE nodes SET emoji = ? WHERE id = ?').run(body.emoji, id);
230 + if (Array.isArray(body.tagIds)) {
231 + db.prepare('DELETE FROM node_tags WHERE node_id = ?').run(id);
232 + const ins = db.prepare('INSERT OR IGNORE INTO node_tags (node_id, tag_id) VALUES (?, ?)');
233 + for (const tagId of body.tagIds) ins.run(id, Number(tagId));
234 + updateFtsTags(id);
235 + }
236 + return { node: nodeJson(getNode(id)) };
237 + });
238 +
239 + app.delete('/api/v1/nodes/:id', async (req) => {
240 + const id = Number(req.params.id);
241 + if (req.query.force === 'true') {
242 + deleteForever(id, { ip: req.ip });
243 + return { ok: true, forever: true };
244 + }
245 + return { ok: true, node: nodeJson(trashNode(id, { ip: req.ip })) };
246 + });
247 +
248 + app.post('/api/v1/nodes/:id/restore', async (req) =>
249 + ({ node: nodeJson(restoreNode(Number(req.params.id), { ip: req.ip })) }));
250 +
251 + app.post('/api/v1/nodes/:id/copy', async (req) =>
252 + ({ node: nodeJson(copyNode(Number(req.params.id), Number(req.body?.parentId ?? ROOT_ID), { ip: req.ip })) }));
253 +
254 + app.post('/api/v1/nodes/:id/duplicate', async (req) =>
255 + ({ node: nodeJson(duplicateNode(Number(req.params.id), { ip: req.ip })) }));
256 +
257 + app.get('/api/v1/trash', async () => ({ items: listTrash().map(nodeJson) }));
258 + app.post('/api/v1/trash/empty', async (req) => {
259 + const items = listTrash();
260 + for (const item of items) deleteForever(item.id, { ip: req.ip });
261 + return { ok: true, removed: items.length };
262 + });
263 +
264 + app.get('/api/v1/recent', async () => ({ items: recentFiles(50).map(nodeJson) }));
265 +
266 + // ── Uploads (chunked + resumable) ───────────────────────────────────
267 + app.post('/api/v1/upload/init', async (req, reply) => {
268 + const { parentId, name, size, path: relPath } = req.body ?? {};
269 + // Folder uploads send a relative path — create intermediate dirs.
270 + let parent = Number(parentId ?? ROOT_ID);
271 + if (relPath) {
272 + const parts = String(relPath).split('/').filter(Boolean).slice(0, -1);
273 + for (const part of parts) parent = mkdir(parent, part, { ip: req.ip }).id;
274 + }
275 + return initUpload({ parentId: parent, name: String(name ?? ''), size });
276 + });
277 +
278 + app.get('/api/v1/upload/:id', async (req) => {
279 + const up = getUpload(req.params.id);
280 + return { uploadId: up.id, nChunks: up.n_chunks, chunkSize: up.chunk_size, have: await chunksPresent(up.id) };
281 + });
282 +
283 + app.put('/api/v1/upload/:id/chunk/:n', async (req, reply) => {
284 + await writeChunk(req.params.id, req.params.n, req.raw);
285 + return { ok: true };
286 + });
287 +
288 + app.post('/api/v1/upload/:id/complete', async (req) => {
289 + const { conflict, mime: bodyMime } = req.body ?? {};
290 + const up = getUpload(req.params.id);
291 + const guessed = bodyMime || mime.getType(up.name) || 'application/octet-stream';
292 + const node = await completeUpload(req.params.id, { conflict, mime: guessed, ip: req.ip });
293 + if (node) extractNodeText(node); // background FTS extraction
294 + return { node: nodeJson(node), skipped: node === null };
295 + });
296 +
297 + app.delete('/api/v1/upload/:id', async (req) => {
298 + await abortUpload(req.params.id);
299 + return { ok: true };
300 + });
301 +
302 + // ── Multi-select ZIP download ───────────────────────────────────────
303 + app.get('/api/v1/zip', async (req, reply) => {
304 + const ids = String(req.query.ids ?? '').split(',').map(Number).filter(Boolean);
305 + if (ids.length === 0) return apiError(reply, 400, 'no_ids', 'ids required');
306 + const name = ids.length === 1 ? `${mustGetNode(ids[0]).name}.zip` : 'spbdrive-selection.zip';
307 + reply.header('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(name)}`);
308 + reply.type('application/zip');
309 + const archive = archiver('zip', { zlib: { level: 6 } });
310 + for (const id of ids) {
311 + const node = mustGetNode(id);
312 + if (node.type === 'file') {
313 + archive.file((await import('../storage/blobs.mjs')).blobPath(node.blob_sha), { name: node.name });
314 + } else {
315 + for (const { node: file, relPath } of listDescendantFiles(node.id, node.name)) {
316 + archive.file((await import('../storage/blobs.mjs')).blobPath(file.blob_sha), { name: relPath });
317 + }
318 + }
319 + }
320 + archive.finalize();
321 + logActivity('file.download_zip', { detail: `${ids.length} item(s)`, ip: req.ip });
322 + return reply.send(archive);
323 + });
324 +
325 + // ── Tags ────────────────────────────────────────────────────────────
326 + app.get('/api/v1/tags', async () => ({
327 + tags: getDb().prepare(
328 + `SELECT t.*, COUNT(nt.node_id) AS count FROM tags t
329 + LEFT JOIN node_tags nt ON nt.tag_id = t.id GROUP BY t.id ORDER BY t.name`,
330 + ).all(),
331 + }));
332 +
333 + app.post('/api/v1/tags', async (req, reply) => {
334 + const name = String(req.body?.name ?? '').trim().slice(0, 40);
335 + if (!name) return apiError(reply, 400, 'bad_name', 'Tag name required');
336 + const color = String(req.body?.color ?? '#4f8cff');
337 + const info = getDb()
338 + .prepare('INSERT INTO tags (name, color) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET color = excluded.color')
339 + .run(name, color);
340 + return { tag: getDb().prepare('SELECT * FROM tags WHERE name = ?').get(name) };
341 + });
342 +
343 + app.patch('/api/v1/tags/:id', async (req) => {
344 + const { name, color } = req.body ?? {};
345 + const db = getDb();
346 + if (name) db.prepare('UPDATE tags SET name = ? WHERE id = ?').run(String(name).slice(0, 40), Number(req.params.id));
347 + if (color) db.prepare('UPDATE tags SET color = ? WHERE id = ?').run(String(color), Number(req.params.id));
348 + return { tag: db.prepare('SELECT * FROM tags WHERE id = ?').get(Number(req.params.id)) };
349 + });
350 +
351 + app.delete('/api/v1/tags/:id', async (req) => {
352 + getDb().prepare('DELETE FROM tags WHERE id = ?').run(Number(req.params.id));
353 + return { ok: true };
354 + });
355 +
356 + app.get('/api/v1/tags/:id/nodes', async (req) => {
357 + const rows = getDb()
358 + .prepare(
359 + `SELECT n.* FROM node_tags nt JOIN nodes n ON n.id = nt.node_id
360 + WHERE nt.tag_id = ? AND n.trashed_at IS NULL ORDER BY n.modified DESC`,
361 + )
362 + .all(Number(req.params.id));
363 + return { items: rows.map(nodeJson) };
364 + });
365 +
366 + // ── Starred ─────────────────────────────────────────────────────────
367 + app.get('/api/v1/starred', async () => ({
368 + items: getDb()
369 + .prepare('SELECT * FROM nodes WHERE starred = 1 AND trashed_at IS NULL ORDER BY modified DESC')
370 + .all()
371 + .map(nodeJson),
372 + }));
373 +
374 + // ── Shares ──────────────────────────────────────────────────────────
375 + app.get('/api/v1/shares', async () => ({ shares: listShares().map(shareJson) }));
376 +
377 + app.get('/api/v1/nodes/:id/shares', async (req) => ({
378 + shares: sharesForNode(Number(req.params.id)).map((s) =>
379 + shareJson({ ...s, node_name: '', node_type: '' })),
380 + }));
381 +
382 + app.post('/api/v1/shares', async (req, reply) => {
383 + const { nodeId, expiresAt, password, maxDownloads, allowDownload, label } = req.body ?? {};
384 + const share = await createShare(Number(nodeId), {
385 + expiresAt: expiresAt ?? null,
386 + password: password || null,
387 + maxDownloads: maxDownloads ?? null,
388 + allowDownload,
389 + label: label ?? null,
390 + ip: req.ip,
391 + });
392 + const full = { ...share, node_name: mustGetNode(share.node_id).name, node_type: mustGetNode(share.node_id).type };
393 + return { share: shareJson(full) };
394 + });
395 +
396 + app.patch('/api/v1/shares/:id', async (req, reply) => {
397 + const share = await updateShare(Number(req.params.id), req.body ?? {});
398 + if (!share) return apiError(reply, 404, 'not_found', 'No such share');
399 + const node = mustGetNode(share.node_id);
400 + return { share: shareJson({ ...share, node_name: node.name, node_type: node.type }) };
401 + });
402 +
403 + app.delete('/api/v1/shares/:id', async (req, reply) => {
404 + if (!revokeShare(Number(req.params.id), { ip: req.ip })) {
405 + return apiError(reply, 404, 'not_found', 'No such share');
406 + }
407 + return { ok: true };
408 + });
409 +
410 + app.get('/api/v1/shares/:id/events', async (req, reply) => {
411 + const share = getShareById(Number(req.params.id));
412 + if (!share) return apiError(reply, 404, 'not_found', 'No such share');
413 + return { events: shareEvents(share.id) };
414 + });
415 +
416 + app.get('/api/v1/shares/:id/qr', async (req, reply) => {
417 + const share = getShareById(Number(req.params.id));
418 + if (!share) return apiError(reply, 404, 'not_found', 'No such share');
419 + const svg = await QRCode.toString(`${config.publicUrl}/s/${share.token}`, {
420 + type: 'svg', margin: 1, width: 240,
421 + color: { dark: '#0b0e14', light: '#ffffff' },
422 + });
423 + reply.type('image/svg+xml');
424 + return reply.send(svg);
425 + });
426 +
427 + // ── Search & activity ───────────────────────────────────────────────
428 + app.get('/api/v1/search', async (req) => {
429 + const { q = '', type, tag, folderId, starred, shared, after, before, minSize, maxSize } = req.query;
430 + const rows = searchNodes(String(q), {
431 + type, tag,
432 + folderId: folderId ? Number(folderId) : undefined,
433 + starred: starred === 'true',
434 + shared: shared === 'true',
435 + after: after ? Number(after) : undefined,
436 + before: before ? Number(before) : undefined,
437 + minSize: minSize ? Number(minSize) : undefined,
438 + maxSize: maxSize ? Number(maxSize) : undefined,
439 + });
440 + return { results: rows.map((r) => ({ ...nodeJson(r), snippet: r.snippet })) };
441 + });
442 +
443 + app.get('/api/v1/activity', async (req) => {
444 + const limit = Math.min(Number(req.query.limit ?? 200), 1000);
445 + const rows = getDb()
446 + .prepare(
447 + `SELECT a.*, n.name AS node_name, n.type AS node_type FROM activity a
448 + LEFT JOIN nodes n ON n.id = a.node_id ORDER BY a.ts DESC LIMIT ?`,
449 + )
450 + .all(limit);
451 + return { events: rows };
452 + });
453 +
454 + // ── Preview endpoints (session/API-token protected) ────────────────
455 + const previewNode = (req, reply) => {
456 + const node = getNode(Number(req.params.id));
457 + if (!node || node.type !== 'file') {
458 + apiError(reply, 404, 'not_found', 'No such file');
459 + return null;
460 + }
461 + return node;
462 + };
463 +
464 + app.get('/api/v1/preview/:id', async (req, reply) => {
465 + const node = previewNode(req, reply);
466 + if (!node) return undefined;
467 + return describePreview(node, `/api/v1/preview/${node.id}`);
468 + });
469 +
470 + const previewRoutes = {
471 + '/text': handleText,
472 + '/markdown': handleMarkdown,
473 + '/raw': handleRawText,
474 + '/archive': handleArchive,
475 + '/archive/member': handleArchiveMember,
476 + '/pdf': handleOfficePdf,
477 + '/video': handleVideoStatus,
478 + '/video/file': handleVideoFile,
479 + '/peaks': handlePeaks,
480 + '/exif': handleExif,
481 + '/heic': handleHeic,
482 + };
483 + for (const [suffix, handler] of Object.entries(previewRoutes)) {
484 + app.get(`/api/v1/preview/:id${suffix}`, async (req, reply) => {
485 + const node = previewNode(req, reply);
486 + if (!node) return undefined;
487 + return handler(node, req, reply);
488 + });
489 + }
490 +}
added src/auth/password.mjs +185 −0
@@ -0,0 +1,185 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/auth/password.mjs
8 + * Purpose : argon2id password bootstrap/verify/change + brute-force lockout
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { randomBytes } from 'node:crypto';
14 +import { existsSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
15 +import readline from 'node:readline';
16 +import argon2 from 'argon2';
17 +import { config } from '../config.mjs';
18 +import { getDb, logActivity } from '../db/db.mjs';
19 +
20 +const ARGON_OPTS = { type: argon2.argon2id, memoryCost: 65536, timeCost: 3, parallelism: 2 };
21 +
22 +/** Prompt for a password on the local TTY with echo disabled. */
23 +function promptPassword(question) {
24 + return new Promise((resolve) => {
25 + const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
26 + const orig = rl._writeToOutput.bind(rl);
27 + rl.question(question, (answer) => {
28 + rl._writeToOutput = orig;
29 + process.stdout.write('\n');
30 + rl.close();
31 + resolve(answer);
32 + });
33 + rl._writeToOutput = (s) => {
34 + if (s.includes(question)) orig(question);
35 + };
36 + });
37 +}
38 +
39 +/**
40 + * First-boot seeding: create data/auth.json (argon2id hash) and
41 + * data/keys.json (session + share signing keys), both chmod 600.
42 + * The bootstrap env var is consumed once and ignored forever after.
43 + */
44 +export async function ensureAuthBootstrap() {
45 + if (!existsSync(config.keysFile)) {
46 + writeFileSync(
47 + config.keysFile,
48 + JSON.stringify(
49 + {
50 + sessionSecret: randomBytes(32).toString('hex'),
51 + shareKey: randomBytes(32).toString('hex'),
52 + created: new Date().toISOString(),
53 + },
54 + null,
55 + 2,
56 + ),
57 + { mode: 0o600 },
58 + );
59 + chmodSync(config.keysFile, 0o600);
60 + }
61 +
62 + if (existsSync(config.authFile)) return;
63 +
64 + let password = config.bootstrapPassword;
65 + if (!password) {
66 + if (!process.stdin.isTTY) {
67 + throw new Error(
68 + 'First boot: set SPBDRIVE_BOOTSTRAP_PASSWORD or run interactively to seed the login password.',
69 + );
70 + }
71 + password = await promptPassword('SPB Drive first boot — choose the drive password: ');
72 + }
73 + if (!password || password.length < 6) {
74 + throw new Error('Bootstrap password must be at least 6 characters.');
75 + }
76 +
77 + const hash = await argon2.hash(password, ARGON_OPTS);
78 + writeFileSync(
79 + config.authFile,
80 + JSON.stringify({ passwordHash: hash, updated: new Date().toISOString() }, null, 2),
81 + { mode: 0o600 },
82 + );
83 + chmodSync(config.authFile, 0o600);
84 +}
85 +
86 +/** Load signing keys (session secret, share key). */
87 +export function getKeys() {
88 + return JSON.parse(readFileSync(config.keysFile, 'utf8'));
89 +}
90 +
91 +/**
92 + * Verify the drive password.
93 + * @returns {Promise<boolean>}
94 + */
95 +export async function verifyPassword(password) {
96 + const { passwordHash } = JSON.parse(readFileSync(config.authFile, 'utf8'));
97 + try {
98 + return await argon2.verify(passwordHash, password ?? '');
99 + } catch {
100 + return false;
101 + }
102 +}
103 +
104 +/**
105 + * Change the drive password (requires the current one).
106 + * @returns {Promise<boolean>} false if current password was wrong
107 + */
108 +export async function changePassword(current, next) {
109 + if (!(await verifyPassword(current))) return false;
110 + if (!next || next.length < 6) throw new Error('New password must be at least 6 characters.');
111 + const hash = await argon2.hash(next, ARGON_OPTS);
112 + writeFileSync(
113 + config.authFile,
114 + JSON.stringify({ passwordHash: hash, updated: new Date().toISOString() }, null, 2),
115 + { mode: 0o600 },
116 + );
117 + logActivity('auth.password_changed');
118 + return true;
119 +}
120 +
121 +/** Overwrite the password without knowing the old one (local CLI only). */
122 +export async function resetPassword(next) {
123 + const hash = await argon2.hash(next, ARGON_OPTS);
124 + writeFileSync(
125 + config.authFile,
126 + JSON.stringify({ passwordHash: hash, updated: new Date().toISOString() }, null, 2),
127 + { mode: 0o600 },
128 + );
129 + chmodSync(config.authFile, 0o600);
130 +}
131 +
132 +// ── Brute-force protection ──────────────────────────────────────────────
133 +// Per-IP: 5 failures → 15-minute lockout. Global: exponential backoff that
134 +// slows every attempt after repeated failures from anywhere.
135 +
136 +/**
137 + * @param {string} ip
138 + * @returns {{locked: boolean, retryAfterMs?: number}}
139 + */
140 +export function checkLockout(ip) {
141 + const db = getDb();
142 + const now = Date.now();
143 + const row = db.prepare('SELECT * FROM login_attempts WHERE ip = ?').get(ip);
144 + if (row?.locked_until && row.locked_until > now) {
145 + return { locked: true, retryAfterMs: row.locked_until - now };
146 + }
147 + const global = db.prepare("SELECT value FROM meta WHERE key = 'global_login_backoff'").get();
148 + if (global) {
149 + const { count, last } = JSON.parse(global.value);
150 + if (count >= config.loginMaxFailures) {
151 + const waitMs = Math.min(2 ** (count - config.loginMaxFailures) * 1000, 60_000);
152 + if (now - last < waitMs) return { locked: true, retryAfterMs: waitMs - (now - last) };
153 + }
154 + }
155 + return { locked: false };
156 +}
157 +
158 +/** Record a failed login for an IP; trip the lockout at the threshold. */
159 +export function recordLoginFailure(ip) {
160 + const db = getDb();
161 + const now = Date.now();
162 + const row = db.prepare('SELECT * FROM login_attempts WHERE ip = ?').get(ip);
163 + const failCount = (row?.fail_count ?? 0) + 1;
164 + const lockedUntil = failCount >= config.loginMaxFailures ? now + config.loginLockoutMs : null;
165 + db.prepare(
166 + `INSERT INTO login_attempts (ip, fail_count, last_fail, locked_until) VALUES (?, ?, ?, ?)
167 + ON CONFLICT(ip) DO UPDATE SET fail_count = ?, last_fail = ?, locked_until = ?`,
168 + ).run(ip, failCount, now, lockedUntil, failCount, now, lockedUntil);
169 +
170 + const global = db.prepare("SELECT value FROM meta WHERE key = 'global_login_backoff'").get();
171 + const g = global ? JSON.parse(global.value) : { count: 0, last: 0 };
172 + db.prepare(
173 + `INSERT INTO meta (key, value) VALUES ('global_login_backoff', ?)
174 + ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
175 + ).run(JSON.stringify({ count: g.count + 1, last: now }));
176 +
177 + logActivity('auth.login_failed', { ip });
178 +}
179 +
180 +/** Clear failure counters after a successful login. */
181 +export function clearLoginFailures(ip) {
182 + const db = getDb();
183 + db.prepare('DELETE FROM login_attempts WHERE ip = ?').run(ip);
184 + db.prepare("DELETE FROM meta WHERE key = 'global_login_backoff'").run();
185 +}
added src/auth/session.mjs +128 −0
@@ -0,0 +1,128 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/auth/session.mjs
8 + * Purpose : Cookie sessions (httpOnly/Secure/Lax) + API bearer tokens
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { createHash, randomBytes } from 'node:crypto';
14 +import { config } from '../config.mjs';
15 +import { getDb, logActivity } from '../db/db.mjs';
16 +
17 +export const SESSION_COOKIE = 'spbdrive_sid';
18 +
19 +const sha256 = (s) => createHash('sha256').update(s).digest('hex');
20 +
21 +/**
22 + * Create a session and return the raw cookie value (only the hash is stored).
23 + * @param {{remember?: boolean, ip?: string, ua?: string}} [opts]
24 + * @returns {{token: string, expiresAt: number, maxAgeSec: number}}
25 + */
26 +export function createSession({ remember = false, ip = '', ua = '' } = {}) {
27 + const token = randomBytes(32).toString('base64url');
28 + const now = Date.now();
29 + const ttl = remember ? config.rememberTtlMs : config.sessionTtlMs;
30 + const expiresAt = now + ttl;
31 + getDb()
32 + .prepare(
33 + `INSERT INTO sessions (token_hash, created, last_seen, expires_at, remember, ip, ua)
34 + VALUES (?, ?, ?, ?, ?, ?, ?)`,
35 + )
36 + .run(sha256(token), now, now, expiresAt, remember ? 1 : 0, ip, ua.slice(0, 300));
37 + logActivity('auth.login', { ip });
38 + return { token, expiresAt, maxAgeSec: Math.floor(ttl / 1000) };
39 +}
40 +
41 +/**
42 + * Validate a session cookie value; slides the expiry forward on use.
43 + * @returns {object|null} session row or null
44 + */
45 +export function getSession(token) {
46 + if (!token) return null;
47 + const db = getDb();
48 + const row = db.prepare('SELECT * FROM sessions WHERE token_hash = ?').get(sha256(token));
49 + if (!row) return null;
50 + const now = Date.now();
51 + if (row.expires_at < now) {
52 + db.prepare('DELETE FROM sessions WHERE id = ?').run(row.id);
53 + return null;
54 + }
55 + // Sliding renewal, throttled to once a minute to avoid write churn.
56 + if (now - row.last_seen > 60_000) {
57 + const ttl = row.remember ? config.rememberTtlMs : config.sessionTtlMs;
58 + db.prepare('UPDATE sessions SET last_seen = ?, expires_at = ? WHERE id = ?').run(
59 + now, now + ttl, row.id,
60 + );
61 + }
62 + return row;
63 +}
64 +
65 +/** Destroy one session by raw cookie value. */
66 +export function destroySession(token) {
67 + if (!token) return;
68 + getDb().prepare('DELETE FROM sessions WHERE token_hash = ?').run(sha256(token));
69 +}
70 +
71 +/** List active sessions (for the Settings page). */
72 +export function listSessions() {
73 + return getDb()
74 + .prepare('SELECT id, created, last_seen, expires_at, remember, ip, ua FROM sessions WHERE expires_at > ? ORDER BY last_seen DESC')
75 + .all(Date.now());
76 +}
77 +
78 +/** Revoke a session by row id; returns true if one was deleted. */
79 +export function revokeSession(id) {
80 + return getDb().prepare('DELETE FROM sessions WHERE id = ?').run(id).changes > 0;
81 +}
82 +
83 +/** Revoke every session (logout everywhere). */
84 +export function revokeAllSessions() {
85 + getDb().prepare('DELETE FROM sessions').run();
86 + logActivity('auth.logout_everywhere');
87 +}
88 +
89 +// ── API tokens (used by the spbdrive CLI) ───────────────────────────────
90 +
91 +/**
92 + * Create a named API token; the raw value is shown once and stored hashed.
93 + * @returns {{id: number, token: string}}
94 + */
95 +export function createApiToken(name) {
96 + const token = `spbd_${randomBytes(24).toString('base64url')}`;
97 + const info = getDb()
98 + .prepare('INSERT INTO api_tokens (name, token_hash, created) VALUES (?, ?, ?)')
99 + .run(String(name || 'token').slice(0, 80), sha256(token), Date.now());
100 + logActivity('auth.api_token_created', { detail: name });
101 + return { id: Number(info.lastInsertRowid), token };
102 +}
103 +
104 +/** Validate a Bearer token; returns the token row or null. */
105 +export function getApiToken(token) {
106 + if (!token) return null;
107 + const db = getDb();
108 + const row = db.prepare('SELECT * FROM api_tokens WHERE token_hash = ?').get(sha256(token));
109 + if (row) db.prepare('UPDATE api_tokens SET last_used = ? WHERE id = ?').run(Date.now(), row.id);
110 + return row;
111 +}
112 +
113 +/** List API tokens (names + metadata only). */
114 +export function listApiTokens() {
115 + return getDb()
116 + .prepare('SELECT id, name, created, last_used FROM api_tokens ORDER BY created DESC')
117 + .all();
118 +}
119 +
120 +/** Delete an API token by id. */
121 +export function revokeApiToken(id) {
122 + return getDb().prepare('DELETE FROM api_tokens WHERE id = ?').run(id).changes > 0;
123 +}
124 +
125 +/** Purge expired sessions (daily maintenance). */
126 +export function pruneSessions() {
127 + return getDb().prepare('DELETE FROM sessions WHERE expires_at < ?').run(Date.now()).changes;
128 +}
added src/config.mjs +75 −0
@@ -0,0 +1,75 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/config.mjs
8 + * Purpose : zod-validated environment configuration + data-dir layout
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { mkdirSync } from 'node:fs';
14 +import path from 'node:path';
15 +import { z } from 'zod';
16 +
17 +const schema = z.object({
18 + SPBDRIVE_DATA_DIR: z.string().default('/srv/drive'),
19 + SPBDRIVE_PORT: z.coerce.number().int().min(1).max(65535).default(7430),
20 + SPBDRIVE_HOST: z.string().default('127.0.0.1'),
21 + SPBDRIVE_PUBLIC_URL: z.string().url().default('https://drive.spboucher.ai'),
22 + SPBDRIVE_LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
23 + SPBDRIVE_TRUST_PROXY: z
24 + .string()
25 + .default('true')
26 + .transform((v) => v === 'true' || v === '1'),
27 + /** Read once at first boot to seed the login password; ignored afterwards. */
28 + SPBDRIVE_BOOTSTRAP_PASSWORD: z.string().optional(),
29 +});
30 +
31 +const env = schema.parse(process.env);
32 +const dataDir = path.resolve(env.SPBDRIVE_DATA_DIR);
33 +
34 +/** Resolved, validated application configuration (frozen). */
35 +export const config = Object.freeze({
36 + port: env.SPBDRIVE_PORT,
37 + host: env.SPBDRIVE_HOST,
38 + publicUrl: env.SPBDRIVE_PUBLIC_URL.replace(/\/$/, ''),
39 + logLevel: env.SPBDRIVE_LOG_LEVEL,
40 + trustProxy: env.SPBDRIVE_TRUST_PROXY,
41 + bootstrapPassword: env.SPBDRIVE_BOOTSTRAP_PASSWORD ?? null,
42 +
43 + dataDir,
44 + filesDir: path.join(dataDir, 'files'),
45 + dbDir: path.join(dataDir, 'db'),
46 + cacheDir: path.join(dataDir, 'cache'),
47 + uploadsDir: path.join(dataDir, 'cache', 'uploads'),
48 + logsDir: path.join(dataDir, 'logs'),
49 + dbFile: path.join(dataDir, 'db', 'drive.sqlite'),
50 + authFile: path.join(dataDir, 'auth.json'),
51 + keysFile: path.join(dataDir, 'keys.json'),
52 +
53 + chunkSize: 8 * 1024 * 1024,
54 + sessionTtlMs: 24 * 60 * 60 * 1000,
55 + rememberTtlMs: 30 * 24 * 60 * 60 * 1000,
56 + trashRetentionDays: 30,
57 + loginMaxFailures: 5,
58 + loginLockoutMs: 15 * 60 * 1000,
59 +});
60 +
61 +/** Create every runtime directory (idempotent, restrictive perms). */
62 +export function ensureDataDirs() {
63 + for (const dir of [
64 + config.dataDir, config.filesDir, config.dbDir, config.cacheDir,
65 + config.uploadsDir, config.logsDir,
66 + path.join(config.cacheDir, 'thumbs'),
67 + path.join(config.cacheDir, 'transcode'),
68 + path.join(config.cacheDir, 'office'),
69 + path.join(config.cacheDir, 'peaks'),
70 + path.join(config.cacheDir, 'text'),
71 + path.join(config.cacheDir, 'tmp'),
72 + ]) {
73 + mkdirSync(dir, { recursive: true, mode: 0o700 });
74 + }
75 +}
added src/db/db.mjs +66 −0
@@ -0,0 +1,66 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/db/db.mjs
8 + * Purpose : SQLite bootstrap (better-sqlite3, WAL), migrations, root node seed
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { readFileSync } from 'node:fs';
14 +import path from 'node:path';
15 +import { fileURLToPath } from 'node:url';
16 +import Database from 'better-sqlite3';
17 +import { config } from '../config.mjs';
18 +
19 +const HERE = path.dirname(fileURLToPath(import.meta.url));
20 +
21 +/** @type {import('better-sqlite3').Database | null} */
22 +let db = null;
23 +
24 +/**
25 + * Open (or return the already-open) database, applying schema migrations
26 + * and seeding the root folder node on first run.
27 + * @param {string} [file] Override DB path (tests).
28 + * @returns {import('better-sqlite3').Database}
29 + */
30 +export function getDb(file) {
31 + if (db) return db;
32 + db = new Database(file ?? config.dbFile);
33 + db.pragma('journal_mode = WAL');
34 + db.pragma('foreign_keys = ON');
35 + db.pragma('synchronous = NORMAL');
36 +
37 + db.exec(readFileSync(path.join(HERE, 'schema.sql'), 'utf8'));
38 +
39 + const root = db.prepare('SELECT id FROM nodes WHERE id = 1').get();
40 + if (!root) {
41 + db.prepare(
42 + `INSERT INTO nodes (id, parent_id, name, type, created, modified)
43 + VALUES (1, NULL, '', 'folder', ?, ?)`,
44 + ).run(Date.now(), Date.now());
45 + }
46 + return db;
47 +}
48 +
49 +/** Close the database (tests / graceful shutdown). */
50 +export function closeDb() {
51 + if (db) {
52 + db.close();
53 + db = null;
54 + }
55 +}
56 +
57 +/**
58 + * Record an activity-log entry.
59 + * @param {string} kind e.g. 'login', 'upload', 'rename', 'share.create'
60 + * @param {{nodeId?: number|null, detail?: string, ip?: string}} [extra]
61 + */
62 +export function logActivity(kind, { nodeId = null, detail = '', ip = '' } = {}) {
63 + getDb()
64 + .prepare('INSERT INTO activity (ts, kind, node_id, detail, ip) VALUES (?, ?, ?, ?, ?)')
65 + .run(Date.now(), kind, nodeId, detail, ip);
66 +}
added src/db/schema.sql +149 −0
@@ -0,0 +1,149 @@
1 +-- ─────────────────────────────────────────────
2 +-- SPB Drive — Personal Cloud Drive
3 +-- ─────────────────────────────────────────────
4 +-- Author : Simon-Pierre Boucher
5 +-- Contact : contact@spboucher.ai
6 +-- File : src/db/schema.sql
7 +-- Purpose : Full SQLite schema — nodes tree, blobs, shares, sessions, FTS5
8 +-- License : MIT © Simon-Pierre Boucher
9 +-- ─────────────────────────────────────────────
10 +
11 +PRAGMA journal_mode = WAL;
12 +
13 +CREATE TABLE IF NOT EXISTS meta (
14 + key TEXT PRIMARY KEY,
15 + value TEXT NOT NULL
16 +);
17 +
18 +-- Content-addressed blob registry. Path on disk: files/<sha[0:2]>/<sha[2:4]>/<sha>
19 +CREATE TABLE IF NOT EXISTS blobs (
20 + sha TEXT PRIMARY KEY,
21 + size INTEGER NOT NULL,
22 + refcount INTEGER NOT NULL DEFAULT 0,
23 + created INTEGER NOT NULL
24 +);
25 +
26 +-- Virtual folder tree. Root is id=1 (parent_id NULL, type 'folder').
27 +CREATE TABLE IF NOT EXISTS nodes (
28 + id INTEGER PRIMARY KEY AUTOINCREMENT,
29 + parent_id INTEGER REFERENCES nodes(id) ON DELETE CASCADE,
30 + name TEXT NOT NULL,
31 + type TEXT NOT NULL CHECK (type IN ('file', 'folder')),
32 + blob_sha TEXT REFERENCES blobs(sha),
33 + size INTEGER NOT NULL DEFAULT 0,
34 + mime TEXT,
35 + created INTEGER NOT NULL,
36 + modified INTEGER NOT NULL,
37 + starred INTEGER NOT NULL DEFAULT 0,
38 + color TEXT,
39 + emoji TEXT,
40 + trashed_at INTEGER,
41 + trash_orig_parent INTEGER
42 +);
43 +
44 +CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id, trashed_at);
45 +CREATE INDEX IF NOT EXISTS idx_nodes_blob ON nodes(blob_sha);
46 +CREATE INDEX IF NOT EXISTS idx_nodes_trashed ON nodes(trashed_at) WHERE trashed_at IS NOT NULL;
47 +CREATE INDEX IF NOT EXISTS idx_nodes_starred ON nodes(starred) WHERE starred = 1;
48 +CREATE INDEX IF NOT EXISTS idx_nodes_recent ON nodes(modified DESC);
49 +
50 +CREATE TABLE IF NOT EXISTS shares (
51 + id INTEGER PRIMARY KEY AUTOINCREMENT,
52 + token TEXT NOT NULL UNIQUE,
53 + node_id INTEGER NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
54 + created INTEGER NOT NULL,
55 + expires_at INTEGER,
56 + password_hash TEXT,
57 + max_downloads INTEGER,
58 + downloads INTEGER NOT NULL DEFAULT 0,
59 + visits INTEGER NOT NULL DEFAULT 0,
60 + allow_download INTEGER NOT NULL DEFAULT 1,
61 + label TEXT,
62 + revoked_at INTEGER
63 +);
64 +
65 +CREATE INDEX IF NOT EXISTS idx_shares_node ON shares(node_id);
66 +
67 +CREATE TABLE IF NOT EXISTS share_events (
68 + id INTEGER PRIMARY KEY AUTOINCREMENT,
69 + share_id INTEGER NOT NULL REFERENCES shares(id) ON DELETE CASCADE,
70 + ts INTEGER NOT NULL,
71 + kind TEXT NOT NULL CHECK (kind IN ('visit', 'download')),
72 + ip TEXT,
73 + ua TEXT
74 +);
75 +
76 +CREATE INDEX IF NOT EXISTS idx_share_events_share ON share_events(share_id, ts DESC);
77 +
78 +CREATE TABLE IF NOT EXISTS tags (
79 + id INTEGER PRIMARY KEY AUTOINCREMENT,
80 + name TEXT NOT NULL UNIQUE,
81 + color TEXT NOT NULL DEFAULT '#4f8cff'
82 +);
83 +
84 +CREATE TABLE IF NOT EXISTS node_tags (
85 + node_id INTEGER NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
86 + tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
87 + PRIMARY KEY (node_id, tag_id)
88 +);
89 +
90 +-- Session ids are stored hashed (sha256) — a DB leak never leaks live cookies.
91 +CREATE TABLE IF NOT EXISTS sessions (
92 + id INTEGER PRIMARY KEY AUTOINCREMENT,
93 + token_hash TEXT NOT NULL UNIQUE,
94 + created INTEGER NOT NULL,
95 + last_seen INTEGER NOT NULL,
96 + expires_at INTEGER NOT NULL,
97 + remember INTEGER NOT NULL DEFAULT 0,
98 + ip TEXT,
99 + ua TEXT
100 +);
101 +
102 +CREATE TABLE IF NOT EXISTS api_tokens (
103 + id INTEGER PRIMARY KEY AUTOINCREMENT,
104 + name TEXT NOT NULL,
105 + token_hash TEXT NOT NULL UNIQUE,
106 + created INTEGER NOT NULL,
107 + last_used INTEGER
108 +);
109 +
110 +CREATE TABLE IF NOT EXISTS activity (
111 + id INTEGER PRIMARY KEY AUTOINCREMENT,
112 + ts INTEGER NOT NULL,
113 + kind TEXT NOT NULL,
114 + node_id INTEGER,
115 + detail TEXT,
116 + ip TEXT
117 +);
118 +
119 +CREATE INDEX IF NOT EXISTS idx_activity_ts ON activity(ts DESC);
120 +
121 +CREATE TABLE IF NOT EXISTS login_attempts (
122 + ip TEXT PRIMARY KEY,
123 + fail_count INTEGER NOT NULL DEFAULT 0,
124 + last_fail INTEGER,
125 + locked_until INTEGER
126 +);
127 +
128 +-- Resumable upload sessions survive server restarts.
129 +CREATE TABLE IF NOT EXISTS uploads (
130 + id TEXT PRIMARY KEY,
131 + parent_id INTEGER NOT NULL,
132 + name TEXT NOT NULL,
133 + size INTEGER NOT NULL,
134 + chunk_size INTEGER NOT NULL,
135 + n_chunks INTEGER NOT NULL,
136 + created INTEGER NOT NULL
137 +);
138 +
139 +-- Full-text search over name, tags and extracted text content.
140 +CREATE VIRTUAL TABLE IF NOT EXISTS fts USING fts5(
141 + name, tags, content,
142 + tokenize = 'unicode61 remove_diacritics 2'
143 +);
144 +
145 +-- Map fts rowid <-> node id explicitly (fts rowid == node id).
146 +CREATE TABLE IF NOT EXISTS fts_state (
147 + node_id INTEGER PRIMARY KEY,
148 + content_sha TEXT
149 +);
added src/preview/archive.mjs +191 −0
@@ -0,0 +1,191 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/preview/archive.mjs
8 + * Purpose : Browse inside zip/tar/tar.gz (+7z/rar via 7z binary), stream members
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { execFile } from 'node:child_process';
14 +import { createReadStream } from 'node:fs';
15 +import { createGunzip } from 'node:zlib';
16 +import { promisify } from 'node:util';
17 +import { PassThrough } from 'node:stream';
18 +import yauzl from 'yauzl';
19 +import tarStream from 'tar-stream';
20 +import { blobPath } from '../storage/blobs.mjs';
21 +import { hasBin } from './thumbs.mjs';
22 +
23 +const execFileP = promisify(execFile);
24 +const MAX_ENTRIES = 5000;
25 +const MAX_MEMBER_BYTES = 200 * 1024 * 1024;
26 +
27 +/** Archive kind from filename ('zip' | 'tar' | 'tgz' | '7z' | 'rar' | null). */
28 +export function archiveKind(name) {
29 + const n = name.toLowerCase();
30 + if (n.endsWith('.zip')) return 'zip';
31 + if (n.endsWith('.tar.gz') || n.endsWith('.tgz')) return 'tgz';
32 + if (n.endsWith('.tar')) return 'tar';
33 + if (n.endsWith('.7z')) return '7z';
34 + if (n.endsWith('.rar')) return 'rar';
35 + return null;
36 +}
37 +
38 +function openZip(file) {
39 + return new Promise((resolve, reject) => {
40 + yauzl.open(file, { lazyEntries: true, autoClose: false }, (err, zip) =>
41 + err ? reject(err) : resolve(zip));
42 + });
43 +}
44 +
45 +async function listZip(file) {
46 + const zip = await openZip(file);
47 + const entries = [];
48 + return new Promise((resolve, reject) => {
49 + zip.readEntry();
50 + zip.on('entry', (entry) => {
51 + entries.push({
52 + path: entry.fileName,
53 + dir: entry.fileName.endsWith('/'),
54 + size: entry.uncompressedSize,
55 + });
56 + if (entries.length >= MAX_ENTRIES) {
57 + zip.close();
58 + resolve({ entries, truncated: true });
59 + return;
60 + }
61 + zip.readEntry();
62 + });
63 + zip.on('end', () => { zip.close(); resolve({ entries, truncated: false }); });
64 + zip.on('error', reject);
65 + });
66 +}
67 +
68 +function tarSource(file, kind) {
69 + const raw = createReadStream(file);
70 + return kind === 'tgz' ? raw.pipe(createGunzip()) : raw;
71 +}
72 +
73 +async function listTar(file, kind) {
74 + const extract = tarStream.extract();
75 + const entries = [];
76 + tarSource(file, kind).pipe(extract);
77 + return new Promise((resolve, reject) => {
78 + extract.on('entry', (header, stream, next) => {
79 + entries.push({
80 + path: header.name,
81 + dir: header.type === 'directory',
82 + size: header.size ?? 0,
83 + });
84 + stream.resume();
85 + if (entries.length >= MAX_ENTRIES) {
86 + extract.destroy();
87 + resolve({ entries, truncated: true });
88 + return;
89 + }
90 + stream.on('end', next);
91 + });
92 + extract.on('finish', () => resolve({ entries, truncated: false }));
93 + extract.on('error', reject);
94 + });
95 +}
96 +
97 +async function listWith7z(file) {
98 + if (!(await hasBin('7z'))) return null;
99 + const { stdout } = await execFileP('7z', ['l', '-slt', '-ba', file], {
100 + timeout: 60_000, maxBuffer: 32 * 1024 * 1024,
101 + });
102 + const entries = [];
103 + for (const block of stdout.split('\n\n')) {
104 + const get = (key) => block.match(new RegExp(`^${key} = (.*)$`, 'm'))?.[1];
105 + const p = get('Path');
106 + if (!p) continue;
107 + entries.push({
108 + path: p.replaceAll('\\', '/'),
109 + dir: (get('Attributes') ?? '').includes('D') || get('Folder') === '+',
110 + size: Number(get('Size') ?? 0),
111 + });
112 + if (entries.length >= MAX_ENTRIES) return { entries, truncated: true };
113 + }
114 + return { entries, truncated: false };
115 +}
116 +
117 +/**
118 + * List an archive's members.
119 + * @returns {Promise<{entries: {path,dir,size}[], truncated: boolean}|null>}
120 + * null when the format needs a missing external binary
121 + */
122 +export async function listArchive(sha, name) {
123 + const kind = archiveKind(name);
124 + const file = blobPath(sha);
125 + if (kind === 'zip') return listZip(file);
126 + if (kind === 'tar' || kind === 'tgz') return listTar(file, kind);
127 + if (kind === '7z' || kind === 'rar') return listWith7z(file);
128 + return null;
129 +}
130 +
131 +/**
132 + * Stream a single member out of an archive (for inner-file preview).
133 + * @returns {Promise<{stream: NodeJS.ReadableStream, size: number}|null>}
134 + */
135 +export async function extractMember(sha, name, memberPath) {
136 + const kind = archiveKind(name);
137 + const file = blobPath(sha);
138 + if (memberPath.includes('..')) return null;
139 +
140 + if (kind === 'zip') {
141 + const zip = await openZip(file);
142 + return new Promise((resolve, reject) => {
143 + zip.readEntry();
144 + zip.on('entry', (entry) => {
145 + if (entry.fileName === memberPath && !entry.fileName.endsWith('/')) {
146 + if (entry.uncompressedSize > MAX_MEMBER_BYTES) { zip.close(); resolve(null); return; }
147 + zip.openReadStream(entry, (err, stream) => {
148 + if (err) { zip.close(); reject(err); return; }
149 + stream.on('end', () => zip.close());
150 + resolve({ stream, size: entry.uncompressedSize });
151 + });
152 + } else {
153 + zip.readEntry();
154 + }
155 + });
156 + zip.on('end', () => { zip.close(); resolve(null); });
157 + zip.on('error', reject);
158 + });
159 + }
160 +
161 + if (kind === 'tar' || kind === 'tgz') {
162 + const extract = tarStream.extract();
163 + tarSource(file, kind).pipe(extract);
164 + return new Promise((resolve, reject) => {
165 + extract.on('entry', (header, stream, next) => {
166 + if (header.name === memberPath && header.type === 'file') {
167 + if ((header.size ?? 0) > MAX_MEMBER_BYTES) { extract.destroy(); resolve(null); return; }
168 + resolve({ stream, size: header.size ?? 0 });
169 + } else {
170 + stream.resume();
171 + stream.on('end', next);
172 + }
173 + });
174 + extract.on('finish', () => resolve(null));
175 + extract.on('error', reject);
176 + });
177 + }
178 +
179 + if (kind === '7z' || kind === 'rar') {
180 + if (!(await hasBin('7z'))) return null;
181 + const child = execFile('7z', ['x', '-so', file, memberPath], {
182 + timeout: 5 * 60_000, maxBuffer: MAX_MEMBER_BYTES,
183 + });
184 + const out = new PassThrough();
185 + child.stdout.pipe(out);
186 + child.on('error', (err) => out.destroy(err));
187 + return { stream: out, size: 0 };
188 + }
189 +
190 + return null;
191 +}
added src/preview/code.mjs +104 −0
@@ -0,0 +1,104 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/preview/code.mjs
8 + * Purpose : Shiki syntax highlighting + markdown-it GFM rendering
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { createHighlighter } from 'shiki';
14 +import MarkdownIt from 'markdown-it';
15 +import anchor from 'markdown-it-anchor';
16 +import taskLists from 'markdown-it-task-lists';
17 +
18 +/** Extension → shiki language id. Anything unknown highlights as plaintext. */
19 +export const EXT_LANG = {
20 + js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'jsx',
21 + ts: 'typescript', tsx: 'tsx', py: 'python', rb: 'ruby', go: 'go', rs: 'rust',
22 + c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', hpp: 'cpp', cs: 'csharp', java: 'java',
23 + kt: 'kotlin', swift: 'swift', m: 'objective-c', php: 'php', sh: 'shellscript',
24 + bash: 'shellscript', zsh: 'shellscript', fish: 'fish', ps1: 'powershell',
25 + sql: 'sql', html: 'html', htm: 'html', xml: 'xml', css: 'css', scss: 'scss',
26 + less: 'less', json: 'json', jsonc: 'jsonc', yaml: 'yaml', yml: 'yaml',
27 + toml: 'toml', ini: 'ini', conf: 'ini', env: 'ini', dockerfile: 'docker',
28 + makefile: 'make', mk: 'make', cmake: 'cmake', gradle: 'groovy', groovy: 'groovy',
29 + lua: 'lua', r: 'r', jl: 'julia', scala: 'scala', pl: 'perl', ex: 'elixir',
30 + exs: 'elixir', erl: 'erlang', hs: 'haskell', ml: 'ocaml', clj: 'clojure',
31 + vue: 'vue', svelte: 'svelte', astro: 'astro', graphql: 'graphql', gql: 'graphql',
32 + proto: 'proto', tf: 'hcl', hcl: 'hcl', nix: 'nix', zig: 'zig', dart: 'dart',
33 + sol: 'solidity', asm: 'asm', s: 'asm', vim: 'viml', diff: 'diff', patch: 'diff',
34 + tex: 'latex', bib: 'latex', md: 'markdown', njk: 'html', csv: 'csv', tsv: 'tsv',
35 + txt: 'plaintext', log: 'log', bat: 'bat', cmd: 'bat', vb: 'vb', fs: 'fsharp',
36 + nim: 'nim', cr: 'crystal', d: 'd', pas: 'pascal', f90: 'fortran-free-form',
37 + cob: 'cobol', ada: 'ada', prisma: 'prisma', cue: 'cue', http: 'http',
38 +};
39 +
40 +let highlighterPromise = null;
41 +
42 +async function getHighlighter() {
43 + if (!highlighterPromise) {
44 + highlighterPromise = createHighlighter({
45 + themes: ['dark-plus', 'light-plus'],
46 + langs: ['javascript', 'typescript', 'python', 'json', 'yaml', 'markdown', 'shellscript', 'html', 'css', 'sql'],
47 + });
48 + }
49 + return highlighterPromise;
50 +}
51 +
52 +/** Resolve a shiki language for a filename (plaintext fallback). */
53 +export function langForFile(name) {
54 + const base = name.toLowerCase();
55 + if (base === 'dockerfile') return 'docker';
56 + if (base === 'makefile') return 'make';
57 + const ext = base.includes('.') ? base.split('.').pop() : base;
58 + return EXT_LANG[ext] ?? 'plaintext';
59 +}
60 +
61 +/**
62 + * Highlight source text → themed HTML (dual theme via CSS variables).
63 + * Unknown or unloadable languages degrade to plaintext, never throw.
64 + */
65 +export async function highlightCode(text, name, { maxBytes = 2_000_000 } = {}) {
66 + const clipped = text.length > maxBytes;
67 + const source = clipped ? text.slice(0, maxBytes) : text;
68 + const hl = await getHighlighter();
69 + let lang = langForFile(name);
70 + if (!hl.getLoadedLanguages().includes(lang)) {
71 + try {
72 + await hl.loadLanguage(lang);
73 + } catch {
74 + lang = 'plaintext';
75 + }
76 + }
77 + const html = hl.codeToHtml(source, {
78 + lang,
79 + themes: { dark: 'dark-plus', light: 'light-plus' },
80 + defaultColor: false,
81 + });
82 + return { html, lang, clipped };
83 +}
84 +
85 +const md = new MarkdownIt({ html: false, linkify: true, typographer: true })
86 + .use(anchor, { permalink: anchor.permalink.headerLink({ safariReaderFix: true }) })
87 + .use(taskLists, { enabled: false });
88 +
89 +// Fenced code inside markdown gets a plain <pre> with a language class;
90 +// mermaid fences are tagged for client-side rendering.
91 +md.renderer.rules.fence = (tokens, idx) => {
92 + const token = tokens[idx];
93 + const info = (token.info || '').trim().split(/\s+/)[0];
94 + const escaped = md.utils.escapeHtml(token.content);
95 + if (info === 'mermaid') {
96 + return `<pre class="mermaid-block" data-mermaid>${escaped}</pre>\n`;
97 + }
98 + return `<pre class="code-fence"><code class="language-${md.utils.escapeHtml(info || 'text')}">${escaped}</code></pre>\n`;
99 +};
100 +
101 +/** Render GFM markdown → sanitized HTML (raw HTML disabled). */
102 +export function renderMarkdown(text) {
103 + return md.render(text ?? '');
104 +}
added src/preview/handlers.mjs +279 −0
@@ -0,0 +1,279 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/preview/handlers.mjs
8 + * Purpose : Preview endpoints shared by the app API and public share pages
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { createReadStream, existsSync } from 'node:fs';
14 +import { readFile, stat } from 'node:fs/promises';
15 +import sharp from 'sharp';
16 +import exifReader from 'exif-reader';
17 +import { blobPath } from '../storage/blobs.mjs';
18 +import { previewStrategy, extOf } from './router.mjs';
19 +import { getThumb, probeMedia } from './thumbs.mjs';
20 +import { isWebSafeVideo, startTranscode, transcodeState, getAudioPeaks } from './transcode.mjs';
21 +import { convertToPdf, officeState, sofficeBin } from './office.mjs';
22 +import { listArchive, extractMember } from './archive.mjs';
23 +import { highlightCode, renderMarkdown } from './code.mjs';
24 +import { sendBlob, parseRange, apiError } from '../web/http-helpers.mjs';
25 +
26 +const TEXT_LIMIT = 4 * 1024 * 1024;
27 +
28 +/**
29 + * Preview descriptor — everything the client viewer needs to pick a renderer.
30 + * `base` is the URL prefix for follow-up calls ('/api/v1/preview/123' or
31 + * '/s/<token>/preview/456').
32 + */
33 +export async function describePreview(node, base) {
34 + const { strategy, ext } = previewStrategy(node);
35 + const out = {
36 + strategy,
37 + ext,
38 + id: node.id,
39 + name: node.name,
40 + size: node.size,
41 + mime: node.mime,
42 + modified: node.modified,
43 + };
44 + if (strategy === 'video') {
45 + const probe = await probeMedia(node.blob_sha);
46 + out.probe = probe;
47 + out.webSafe = isWebSafeVideo(probe, node.mime);
48 + if (!out.webSafe) {
49 + const state = transcodeState(node.blob_sha);
50 + out.transcode = state.state;
51 + if (state.state === 'none') {
52 + startTranscode(node.blob_sha); // fire and forget; client polls
53 + out.transcode = 'queued';
54 + }
55 + }
56 + }
57 + if (strategy === 'audio') {
58 + out.probe = await probeMedia(node.blob_sha);
59 + }
60 + if (strategy === 'office') {
61 + out.office = (await sofficeBin()) ? officeState(node.blob_sha).state : 'unsupported';
62 + if (out.office === 'none') {
63 + convertToPdf(node.blob_sha, extOf(node.name)).catch(() => {});
64 + out.office = 'queued';
65 + }
66 + }
67 + if (strategy === 'heic') {
68 + out.heicSupported = await heicSupported();
69 + }
70 + out.base = base;
71 + return out;
72 +}
73 +
74 +let heicOk = null;
75 +async function heicSupported() {
76 + if (heicOk !== null) return heicOk;
77 + heicOk = Boolean(sharp.format?.heif?.input?.buffer || sharp.format?.heif?.input?.file);
78 + return heicOk;
79 +}
80 +
81 +/** GET …/text — shiki-highlighted HTML for code/text files. */
82 +export async function handleText(node, req, reply) {
83 + if (node.size > TEXT_LIMIT) return apiError(reply, 413, 'too_large', 'File too large for text preview');
84 + const raw = await readFile(blobPath(node.blob_sha), 'utf8');
85 + const { html, lang, clipped } = await highlightCode(raw, node.name);
86 + return reply.send({ html, lang, clipped, raw: raw.length < 512 * 1024 ? raw : null });
87 +}
88 +
89 +/** GET …/markdown — rendered GFM + raw source. */
90 +export async function handleMarkdown(node, req, reply) {
91 + if (node.size > TEXT_LIMIT) return apiError(reply, 413, 'too_large', 'File too large');
92 + const raw = await readFile(blobPath(node.blob_sha), 'utf8');
93 + return reply.send({ html: renderMarkdown(raw), raw });
94 +}
95 +
96 +/** GET …/raw — plain bytes as text/plain (csv/json/notebook parsing client-side). */
97 +export async function handleRawText(node, req, reply) {
98 + const path = blobPath(node.blob_sha);
99 + const size = (await stat(path)).size;
100 + if (size > 64 * 1024 * 1024) return apiError(reply, 413, 'too_large', 'File too large');
101 + reply.header('X-Content-Type-Options', 'nosniff');
102 + reply.header('Content-Length', size);
103 + reply.type('text/plain; charset=utf-8');
104 + return reply.send(createReadStream(path));
105 +}
106 +
107 +/** GET …/archive — member listing. */
108 +export async function handleArchive(node, req, reply) {
109 + const listing = await listArchive(node.blob_sha, node.name);
110 + if (!listing) return apiError(reply, 422, 'unsupported', 'Archive format needs the 7z binary');
111 + return reply.send(listing);
112 +}
113 +
114 +/** GET …/archive/member?path=… — stream one member (inline preview or download). */
115 +export async function handleArchiveMember(node, req, reply) {
116 + const memberPath = String(req.query.path ?? '');
117 + const member = await extractMember(node.blob_sha, node.name, memberPath);
118 + if (!member) return apiError(reply, 404, 'not_found', 'Member not found or too large');
119 + const filename = memberPath.split('/').pop() || 'member';
120 + const download = req.query.download === '1';
121 + reply.header('X-Content-Type-Options', 'nosniff');
122 + reply.header(
123 + 'Content-Disposition',
124 + `${download ? 'attachment' : 'inline'}; filename*=UTF-8''${encodeURIComponent(filename)}`,
125 + );
126 + if (member.size) reply.header('Content-Length', member.size);
127 + const mime = await import('mime').then((m) => m.default.getType(filename));
128 + const safe = mime && /^(image\/(?!svg)|application\/pdf|text\/plain)/.test(mime);
129 + reply.type(download ? (mime ?? 'application/octet-stream') : safe ? mime : 'text/plain; charset=utf-8');
130 + return reply.send(member.stream);
131 +}
132 +
133 +/** GET …/pdf — LibreOffice-converted PDF for office docs (Range-aware). */
134 +export async function handleOfficePdf(node, req, reply) {
135 + const pdf = await convertToPdf(node.blob_sha, extOf(node.name)).catch(() => null);
136 + if (!pdf || !existsSync(pdf)) {
137 + return apiError(reply, 422, 'conversion_unavailable', 'LibreOffice conversion unavailable');
138 + }
139 + const size = (await stat(pdf)).size;
140 + reply.header('Accept-Ranges', 'bytes');
141 + reply.header('X-Content-Type-Options', 'nosniff');
142 + reply.header('Cache-Control', 'private, max-age=3600');
143 + reply.type('application/pdf');
144 + const range = parseRange(req.headers.range, size);
145 + if (range) {
146 + reply.code(206);
147 + reply.header('Content-Range', `bytes ${range.start}-${range.end}/${size}`);
148 + reply.header('Content-Length', range.end - range.start + 1);
149 + return reply.send(createReadStream(pdf, range));
150 + }
151 + reply.header('Content-Length', size);
152 + return reply.send(createReadStream(pdf));
153 +}
154 +
155 +/** GET …/video — transcode status; …/video/file streams the cached mp4. */
156 +export async function handleVideoStatus(node, req, reply) {
157 + const state = transcodeState(node.blob_sha);
158 + if (state.state === 'none') {
159 + startTranscode(node.blob_sha);
160 + return reply.send({ state: 'queued' });
161 + }
162 + return reply.send({ state: state.state });
163 +}
164 +
165 +export async function handleVideoFile(node, req, reply) {
166 + const state = transcodeState(node.blob_sha);
167 + if (state.state !== 'ready') return apiError(reply, 409, 'not_ready', 'Transcode not ready');
168 + const size = (await stat(state.path)).size;
169 + reply.header('Accept-Ranges', 'bytes');
170 + reply.type('video/mp4');
171 + const range = parseRange(req.headers.range, size);
172 + if (range) {
173 + reply.code(206);
174 + reply.header('Content-Range', `bytes ${range.start}-${range.end}/${size}`);
175 + reply.header('Content-Length', range.end - range.start + 1);
176 + return reply.send(createReadStream(state.path, range));
177 + }
178 + reply.header('Content-Length', size);
179 + return reply.send(createReadStream(state.path));
180 +}
181 +
182 +/** GET …/peaks — waveform peaks for the audio player. */
183 +export async function handlePeaks(node, req, reply) {
184 + const peaks = await getAudioPeaks(node.blob_sha);
185 + return reply.send({ peaks: peaks ?? [] });
186 +}
187 +
188 +/** GET …/exif — image metadata panel (dimensions, camera, date, GPS). */
189 +export async function handleExif(node, req, reply) {
190 + try {
191 + const meta = await sharp(blobPath(node.blob_sha), { failOn: 'none' }).metadata();
192 + const out = {
193 + width: meta.width ?? null,
194 + height: meta.height ?? null,
195 + format: meta.format ?? null,
196 + space: meta.space ?? null,
197 + density: meta.density ?? null,
198 + pages: meta.pages ?? null,
199 + };
200 + if (meta.exif) {
201 + try {
202 + const exif = exifReader(meta.exif);
203 + out.camera = [exif?.Image?.Make, exif?.Image?.Model].filter(Boolean).join(' ') || null;
204 + out.lens = exif?.Photo?.LensModel ?? null;
205 + out.iso = exif?.Photo?.ISOSpeedRatings ?? null;
206 + out.exposure = exif?.Photo?.ExposureTime ?? null;
207 + out.fnumber = exif?.Photo?.FNumber ?? null;
208 + out.focal = exif?.Photo?.FocalLength ?? null;
209 + out.taken = exif?.Photo?.DateTimeOriginal ?? exif?.Image?.DateTime ?? null;
210 + const gps = exif?.GPSInfo;
211 + if (gps?.GPSLatitude && gps?.GPSLongitude) {
212 + const toDec = (dms, ref) =>
213 + (dms[0] + dms[1] / 60 + dms[2] / 3600) * (ref === 'S' || ref === 'W' ? -1 : 1);
214 + out.gps = {
215 + lat: toDec(gps.GPSLatitude, gps.GPSLatitudeRef),
216 + lon: toDec(gps.GPSLongitude, gps.GPSLongitudeRef),
217 + };
218 + }
219 + } catch { /* EXIF parse is best-effort */ }
220 + }
221 + return reply.send(out);
222 + } catch {
223 + return reply.send({});
224 + }
225 +}
226 +
227 +/** GET …/heic — HEIC converted to JPEG on the fly (when libheif available). */
228 +export async function handleHeic(node, req, reply) {
229 + if (!(await heicSupported())) return apiError(reply, 422, 'unsupported', 'HEIC not supported');
230 + reply.type('image/jpeg');
231 + reply.header('Cache-Control', 'private, max-age=3600');
232 + const buf = await sharp(blobPath(node.blob_sha), { failOn: 'none' })
233 + .rotate()
234 + .jpeg({ quality: 88 })
235 + .toBuffer();
236 + return reply.send(buf);
237 +}
238 +
239 +/** GET /thumb/:id — cached webp thumbnail (or 404 → client shows type icon). */
240 +export async function handleThumb(node, req, reply, kind) {
241 + let effectiveKind = kind;
242 + let sha = node.blob_sha;
243 + if (kind === 'office') {
244 + const state = officeState(sha);
245 + if (state.state !== 'ready') return apiError(reply, 404, 'no_thumb', 'No thumbnail');
246 + // Thumbnail the converted PDF: getThumb reads from blobPath, so pdf kind
247 + // is handled by a direct pdftoppm call on the cache file instead.
248 + effectiveKind = 'pdf-cache';
249 + }
250 + const file = effectiveKind === 'pdf-cache'
251 + ? await getOfficeThumb(sha, req.query.size)
252 + : await getThumb(sha, effectiveKind, req.query.size);
253 + if (!file || !existsSync(file)) return apiError(reply, 404, 'no_thumb', 'No thumbnail');
254 + const size = (await stat(file)).size;
255 + reply.type('image/webp');
256 + reply.header('Cache-Control', 'private, max-age=86400');
257 + reply.header('Content-Length', size);
258 + return reply.send(createReadStream(file));
259 +}
260 +
261 +async function getOfficeThumb(sha, size) {
262 + const { getThumbFromFile } = await import('./office-thumb.mjs');
263 + return getThumbFromFile(sha, size);
264 +}
265 +
266 +/** GET /stream/:id or /dl/:id (svg goes through the sandboxed sender). */
267 +export async function handleFileSend(node, req, reply, { download }) {
268 + const { strategy } = previewStrategy(node);
269 + if (!download && strategy === 'svg') {
270 + const { sendSandboxedSvg } = await import('../web/http-helpers.mjs');
271 + return sendSandboxedSvg(req, reply, { sha: node.blob_sha, filename: node.name });
272 + }
273 + return sendBlob(req, reply, {
274 + sha: node.blob_sha,
275 + mime: node.mime,
276 + filename: node.name,
277 + download,
278 + });
279 +}
added src/preview/office-thumb.mjs +51 −0
@@ -0,0 +1,51 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/preview/office-thumb.mjs
8 + * Purpose : Thumbnail page 1 of a LibreOffice-converted PDF from cache
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { execFile } from 'node:child_process';
14 +import { existsSync } from 'node:fs';
15 +import { rename, rm } from 'node:fs/promises';
16 +import path from 'node:path';
17 +import { promisify } from 'node:util';
18 +import sharp from 'sharp';
19 +import { config } from '../config.mjs';
20 +import { enqueue } from './queue.mjs';
21 +import { hasBin } from './thumbs.mjs';
22 +
23 +const execFileP = promisify(execFile);
24 +
25 +/** Thumbnail an office doc via its cached converted PDF (webp, cached). */
26 +export async function getThumbFromFile(sha, size = 256) {
27 + const sz = Number(size) === 512 ? 512 : 256;
28 + const out = path.join(config.cacheDir, 'thumbs', `${sha}.${sz}.webp`);
29 + if (existsSync(out)) return out;
30 + const pdf = path.join(config.cacheDir, 'office', `${sha}.pdf`);
31 + if (!existsSync(pdf) || !(await hasBin('pdftoppm'))) return null;
32 +
33 + return enqueue(`thumb:${sha}:${sz}`, async () => {
34 + if (existsSync(out)) return out;
35 + const tmpBase = `${out}.tmp-${process.pid}`;
36 + try {
37 + await execFileP('pdftoppm', ['-png', '-f', '1', '-l', '1', '-scale-to', String(sz), pdf, tmpBase], {
38 + timeout: 60_000,
39 + });
40 + const png = `${tmpBase}-1.png`;
41 + if (!existsSync(png)) return null;
42 + await sharp(png).webp({ quality: 80 }).toFile(`${tmpBase}.webp`);
43 + await rm(png, { force: true });
44 + await rename(`${tmpBase}.webp`, out);
45 + return out;
46 + } catch {
47 + await rm(`${tmpBase}.webp`, { force: true });
48 + return null;
49 + }
50 + });
51 +}
added src/preview/office.mjs +82 −0
@@ -0,0 +1,82 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/preview/office.mjs
8 + * Purpose : LibreOffice headless → PDF conversion, queued + cached by sha
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { execFile } from 'node:child_process';
14 +import { existsSync } from 'node:fs';
15 +import { mkdtemp, readdir, rename, rm, symlink } from 'node:fs/promises';
16 +import os from 'node:os';
17 +import path from 'node:path';
18 +import { promisify } from 'node:util';
19 +import { config } from '../config.mjs';
20 +import { blobPath } from '../storage/blobs.mjs';
21 +import { enqueue, jobStatus } from './queue.mjs';
22 +import { hasBin } from './thumbs.mjs';
23 +
24 +const execFileP = promisify(execFile);
25 +const OFFICE_TIMEOUT = 5 * 60_000;
26 +
27 +export const OFFICE_EXTS = new Set(['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp', 'rtf']);
28 +
29 +const officePdf = (sha) => path.join(config.cacheDir, 'office', `${sha}.pdf`);
30 +
31 +/** Locate the LibreOffice binary (macOS app bundle or PATH). */
32 +export async function sofficeBin() {
33 + const mac = '/Applications/LibreOffice.app/Contents/MacOS/soffice';
34 + if (existsSync(mac)) return mac;
35 + if (await hasBin('soffice')) return 'soffice';
36 + if (await hasBin('libreoffice')) return 'libreoffice';
37 + return null;
38 +}
39 +
40 +/** Conversion status: ready / processing / queued / unavailable / none. */
41 +export function officeState(sha) {
42 + if (existsSync(officePdf(sha))) return { state: 'ready', path: officePdf(sha) };
43 + const job = jobStatus(`office:${sha}`);
44 + if (job?.status === 'running') return { state: 'processing' };
45 + if (job?.status === 'queued') return { state: 'queued' };
46 + if (job?.status === 'error') return { state: 'unavailable' };
47 + return { state: 'none' };
48 +}
49 +
50 +/**
51 + * Convert an office document blob to PDF (queued, cached).
52 + * @param {string} ext original extension so soffice picks the right filter
53 + * @returns {Promise<string|null>} pdf path, or null when LibreOffice is absent
54 + */
55 +export async function convertToPdf(sha, ext) {
56 + const out = officePdf(sha);
57 + if (existsSync(out)) return out;
58 + const bin = await sofficeBin();
59 + if (!bin) return null;
60 +
61 + return enqueue(`office:${sha}`, async () => {
62 + if (existsSync(out)) return out;
63 + const workDir = await mkdtemp(path.join(os.tmpdir(), 'spbdrive-office-'));
64 + try {
65 + // soffice needs a real extension to choose an import filter.
66 + const input = path.join(workDir, `doc.${ext}`);
67 + await symlink(blobPath(sha), input);
68 + await execFileP(
69 + bin,
70 + ['--headless', '--norestore', `-env:UserInstallation=file://${workDir}/lo-profile`,
71 + '--convert-to', 'pdf', '--outdir', workDir, input],
72 + { timeout: OFFICE_TIMEOUT },
73 + );
74 + const produced = (await readdir(workDir)).find((f) => f.endsWith('.pdf'));
75 + if (!produced) throw new Error('LibreOffice produced no PDF');
76 + await rename(path.join(workDir, produced), out);
77 + return out;
78 + } finally {
79 + await rm(workDir, { recursive: true, force: true });
80 + }
81 + });
82 +}
added src/preview/queue.mjs +68 −0
@@ -0,0 +1,68 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/preview/queue.mjs
8 + * Purpose : Tiny in-process job queue (concurrency 2) for transcodes/conversions
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +const CONCURRENCY = 2;
14 +
15 +/** @type {Map<string, {promise: Promise<any>, status: string}>} */
16 +const jobs = new Map();
17 +const waiting = [];
18 +let running = 0;
19 +
20 +function pump() {
21 + while (running < CONCURRENCY && waiting.length > 0) {
22 + const next = waiting.shift();
23 + running += 1;
24 + next.entry.status = 'running';
25 + Promise.resolve()
26 + .then(next.fn)
27 + .then(
28 + (val) => { next.entry.status = 'done'; next.resolve(val); },
29 + (err) => { next.entry.status = 'error'; next.entry.error = String(err?.message ?? err); next.reject(err); },
30 + )
31 + .finally(() => {
32 + running -= 1;
33 + setTimeout(() => jobs.delete(next.key), 60_000);
34 + pump();
35 + });
36 + }
37 +}
38 +
39 +/**
40 + * Enqueue work keyed by a cache key — duplicate keys share one promise,
41 + * so a conversion runs at most once per unique blob.
42 + * @param {string} key e.g. `office:<sha>`
43 + * @param {() => Promise<any>} fn
44 + * @returns {Promise<any>}
45 + */
46 +export function enqueue(key, fn) {
47 + const existing = jobs.get(key);
48 + if (existing) return existing.promise;
49 + const entry = { status: 'queued', error: null };
50 + entry.promise = new Promise((resolve, reject) => {
51 + waiting.push({ key, fn, resolve, reject, entry });
52 + });
53 + entry.promise.catch(() => {}); // observed by callers; avoid unhandled warnings
54 + jobs.set(key, entry);
55 + pump();
56 + return entry.promise;
57 +}
58 +
59 +/** Status for a key: 'queued' | 'running' | 'done' | 'error' | null. */
60 +export function jobStatus(key) {
61 + const entry = jobs.get(key);
62 + return entry ? { status: entry.status, error: entry.error ?? null } : null;
63 +}
64 +
65 +/** Current queue depth (for /healthz). */
66 +export function queueDepth() {
67 + return waiting.length + running;
68 +}
added src/preview/router.mjs +86 −0
@@ -0,0 +1,86 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/preview/router.mjs
8 + * Purpose : MIME/extension → preview strategy dispatcher
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { OFFICE_EXTS } from './office.mjs';
14 +import { archiveKind } from './archive.mjs';
15 +import { EXT_LANG } from './code.mjs';
16 +
17 +const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'bmp', 'ico']);
18 +const VIDEO_EXTS = new Set(['mp4', 'webm', 'mov', 'mkv', 'avi', 'm4v', 'mpg', 'mpeg', 'wmv', 'flv', 'ts']);
19 +const AUDIO_EXTS = new Set(['mp3', 'wav', 'flac', 'ogg', 'oga', 'm4a', 'aac', 'opus', 'wma', 'aiff', 'aif']);
20 +const FONT_EXTS = new Set(['ttf', 'otf', 'woff', 'woff2']);
21 +const STRUCTURED_EXTS = new Set(['json', 'yaml', 'yml', 'toml', 'xml', 'plist']);
22 +const MODEL3D_EXTS = new Set(['stl', 'glb', 'gltf', 'obj']);
23 +
24 +/** Lowercased extension without the dot ('' if none). */
25 +export function extOf(name) {
26 + const base = String(name).toLowerCase();
27 + const special = base.endsWith('.tar.gz') ? 'tar.gz' : null;
28 + if (special) return special;
29 + const dot = base.lastIndexOf('.');
30 + return dot > 0 ? base.slice(dot + 1) : '';
31 +}
32 +
33 +/**
34 + * Decide how a file previews.
35 + * @param {{name: string, mime?: string, size?: number}} node
36 + * @returns {{strategy: string, ext: string}}
37 + */
38 +export function previewStrategy(node) {
39 + const name = node.name ?? '';
40 + const mime = node.mime ?? '';
41 + const ext = extOf(name);
42 +
43 + if (ext === 'svg' || mime === 'image/svg+xml') return { strategy: 'svg', ext };
44 + if (ext === 'heic' || ext === 'heif') return { strategy: 'heic', ext };
45 + if (IMAGE_EXTS.has(ext) || mime.startsWith('image/')) return { strategy: 'image', ext };
46 + if (VIDEO_EXTS.has(ext) || mime.startsWith('video/')) return { strategy: 'video', ext };
47 + if (AUDIO_EXTS.has(ext) || mime.startsWith('audio/')) return { strategy: 'audio', ext };
48 + if (ext === 'pdf' || mime === 'application/pdf') return { strategy: 'pdf', ext };
49 + if (OFFICE_EXTS.has(ext)) return { strategy: 'office', ext };
50 + if (ext === 'md' || ext === 'markdown') return { strategy: 'markdown', ext };
51 + if (ext === 'csv' || ext === 'tsv') return { strategy: 'csv', ext };
52 + if (ext === 'ipynb') return { strategy: 'notebook', ext };
53 + if (STRUCTURED_EXTS.has(ext)) return { strategy: 'structured', ext };
54 + if (archiveKind(name)) return { strategy: 'archive', ext };
55 + if (FONT_EXTS.has(ext)) return { strategy: 'font', ext };
56 + if (ext === 'eml') return { strategy: 'email', ext };
57 + if (ext === 'epub') return { strategy: 'epub', ext };
58 + if (MODEL3D_EXTS.has(ext)) return { strategy: 'model3d', ext };
59 + if (ext === 'html' || ext === 'htm') return { strategy: 'code', ext };
60 + if (EXT_LANG[ext] || mime.startsWith('text/')) return { strategy: 'code', ext };
61 + return { strategy: 'fallback', ext };
62 +}
63 +
64 +/** Which thumbnail pipeline (if any) applies to a file. */
65 +export function thumbKind(node) {
66 + const { strategy } = previewStrategy(node);
67 + if (['image', 'heic'].includes(strategy)) return 'image';
68 + if (strategy === 'video') return 'video';
69 + if (strategy === 'pdf') return 'pdf';
70 + if (strategy === 'office') return 'office'; // via converted PDF when cached
71 + return null;
72 +}
73 +
74 +/** Icon family for the crisp per-extension SVG icon set. */
75 +export function iconFamily(node) {
76 + if (node.type === 'folder') return 'folder';
77 + const { strategy, ext } = previewStrategy(node);
78 + const map = {
79 + image: 'image', svg: 'image', heic: 'image', video: 'video', audio: 'audio',
80 + pdf: 'pdf', office: /x?ls|ods/.test(ext) ? 'sheet' : /pptx?|odp/.test(ext) ? 'slides' : 'doc',
81 + markdown: 'text', csv: 'sheet', notebook: 'code', structured: 'code',
82 + archive: 'archive', font: 'font', email: 'mail', epub: 'book',
83 + model3d: 'cube', code: 'code', fallback: 'file',
84 + };
85 + return map[strategy] ?? 'file';
86 +}
added src/preview/thumbs.mjs +122 −0
@@ -0,0 +1,122 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/preview/thumbs.mjs
8 + * Purpose : Thumbnail generation — sharp for images, ffmpeg poster for video,
9 + * pdftoppm for PDF page 1; cached by (blob sha, size)
10 + * License : MIT © Simon-Pierre Boucher
11 + * ─────────────────────────────────────────────
12 + */
13 +
14 +import { execFile } from 'node:child_process';
15 +import { existsSync } from 'node:fs';
16 +import { rename, rm } from 'node:fs/promises';
17 +import path from 'node:path';
18 +import { promisify } from 'node:util';
19 +import sharp from 'sharp';
20 +import { config } from '../config.mjs';
21 +import { blobPath } from '../storage/blobs.mjs';
22 +import { enqueue } from './queue.mjs';
23 +
24 +const execFileP = promisify(execFile);
25 +const SIZES = new Set([256, 512]);
26 +const EXEC_TIMEOUT = 60_000;
27 +
28 +const thumbFile = (sha, size) => path.join(config.cacheDir, 'thumbs', `${sha}.${size}.webp`);
29 +
30 +/** True if a binary exists on PATH (cached). */
31 +const binCache = new Map();
32 +export async function hasBin(name) {
33 + if (binCache.has(name)) return binCache.get(name);
34 + try {
35 + await execFileP('which', [name]);
36 + binCache.set(name, true);
37 + } catch {
38 + binCache.set(name, false);
39 + }
40 + return binCache.get(name);
41 +}
42 +
43 +/**
44 + * Return the cached thumbnail path for a blob, generating it if needed.
45 + * @param {'image'|'video'|'pdf'} kind
46 + * @returns {Promise<string|null>} webp path, or null if not generatable
47 + */
48 +export async function getThumb(sha, kind, size = 256) {
49 + const sz = SIZES.has(Number(size)) ? Number(size) : 256;
50 + const out = thumbFile(sha, sz);
51 + if (existsSync(out)) return out;
52 +
53 + return enqueue(`thumb:${sha}:${sz}`, async () => {
54 + if (existsSync(out)) return out;
55 + const src = blobPath(sha);
56 + const tmp = `${out}.tmp-${process.pid}`;
57 + try {
58 + if (kind === 'image') {
59 + await sharp(src, { failOn: 'none', limitInputPixels: 512 * 1024 * 1024 })
60 + .rotate()
61 + .resize(sz, sz, { fit: 'inside', withoutEnlargement: true })
62 + .webp({ quality: 80 })
63 + .toFile(tmp);
64 + } else if (kind === 'video') {
65 + if (!(await hasBin('ffmpeg'))) return null;
66 + // Grab a frame ~10% in; fall back to first frame for tiny clips.
67 + await execFileP(
68 + 'ffmpeg',
69 + ['-y', '-ss', '00:00:01', '-i', src, '-frames:v', '1',
70 + '-vf', `scale='min(${sz},iw)':-2`, '-f', 'webp', tmp],
71 + { timeout: EXEC_TIMEOUT },
72 + ).catch(() =>
73 + execFileP('ffmpeg', ['-y', '-i', src, '-frames:v', '1',
74 + '-vf', `scale='min(${sz},iw)':-2`, '-f', 'webp', tmp], { timeout: EXEC_TIMEOUT }),
75 + );
76 + } else if (kind === 'pdf') {
77 + if (!(await hasBin('pdftoppm'))) return null;
78 + const base = `${tmp}-pg`;
79 + await execFileP('pdftoppm', ['-png', '-f', '1', '-l', '1', '-scale-to', String(sz), src, base], {
80 + timeout: EXEC_TIMEOUT,
81 + });
82 + const png = `${base}-1.png`;
83 + if (!existsSync(png)) return null;
84 + await sharp(png).webp({ quality: 80 }).toFile(tmp);
85 + await rm(png, { force: true });
86 + } else {
87 + return null;
88 + }
89 + if (!existsSync(tmp)) return null;
90 + await rename(tmp, out);
91 + return out;
92 + } catch {
93 + await rm(tmp, { force: true });
94 + return null;
95 + }
96 + });
97 +}
98 +
99 +/** Probe media duration/dimensions with ffprobe (null if unavailable). */
100 +export async function probeMedia(sha) {
101 + if (!(await hasBin('ffprobe'))) return null;
102 + try {
103 + const { stdout } = await execFileP(
104 + 'ffprobe',
105 + ['-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', blobPath(sha)],
106 + { timeout: 30_000 },
107 + );
108 + const info = JSON.parse(stdout);
109 + const video = info.streams?.find((s) => s.codec_type === 'video');
110 + const audio = info.streams?.find((s) => s.codec_type === 'audio');
111 + return {
112 + duration: Number(info.format?.duration ?? 0),
113 + width: video?.width ?? null,
114 + height: video?.height ?? null,
115 + videoCodec: video?.codec_name ?? null,
116 + audioCodec: audio?.codec_name ?? null,
117 + container: info.format?.format_name ?? null,
118 + };
119 + } catch {
120 + return null;
121 + }
122 +}
added src/preview/transcode.mjs +123 −0
@@ -0,0 +1,123 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/preview/transcode.mjs
8 + * Purpose : ffmpeg web-safe transcodes (H.264/AAC mp4) + audio waveform peaks
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { execFile } from 'node:child_process';
14 +import { existsSync } from 'node:fs';
15 +import { readFile, rename, rm, writeFile } from 'node:fs/promises';
16 +import path from 'node:path';
17 +import { promisify } from 'node:util';
18 +import { config } from '../config.mjs';
19 +import { blobPath } from '../storage/blobs.mjs';
20 +import { enqueue, jobStatus } from './queue.mjs';
21 +import { hasBin, probeMedia } from './thumbs.mjs';
22 +
23 +const execFileP = promisify(execFile);
24 +const TRANSCODE_TIMEOUT = 30 * 60_000; // hostile/huge inputs get killed after 30 min
25 +
26 +const transcodeFile = (sha) => path.join(config.cacheDir, 'transcode', `${sha}.mp4`);
27 +const peaksFile = (sha) => path.join(config.cacheDir, 'peaks', `${sha}.json`);
28 +
29 +/** Codecs browsers play natively — no transcode needed. */
30 +export function isWebSafeVideo(probe, mime) {
31 + if (mime === 'video/mp4' || mime === 'video/webm') {
32 + const v = probe?.videoCodec;
33 + const a = probe?.audioCodec;
34 + if (!probe) return true; // no ffprobe — optimistically stream as-is
35 + const vOk = !v || ['h264', 'vp8', 'vp9', 'av1'].includes(v);
36 + const aOk = !a || ['aac', 'mp3', 'opus', 'vorbis'].includes(a);
37 + return vOk && aOk;
38 + }
39 + return false;
40 +}
41 +
42 +/**
43 + * Video transcode status for the UI.
44 + * @returns {{state: 'ready'|'processing'|'queued'|'unavailable'|'none', path?: string}}
45 + */
46 +export function transcodeState(sha) {
47 + const out = transcodeFile(sha);
48 + if (existsSync(out)) return { state: 'ready', path: out };
49 + const job = jobStatus(`transcode:${sha}`);
50 + if (job?.status === 'running') return { state: 'processing' };
51 + if (job?.status === 'queued') return { state: 'queued' };
52 + if (job?.status === 'error') return { state: 'unavailable' };
53 + return { state: 'none' };
54 +}
55 +
56 +/**
57 + * Kick off (or join) a background H.264/AAC transcode for a blob.
58 + * Resolves with the mp4 path when done; null when ffmpeg is missing.
59 + */
60 +export async function startTranscode(sha) {
61 + const out = transcodeFile(sha);
62 + if (existsSync(out)) return out;
63 + if (!(await hasBin('ffmpeg'))) return null;
64 +
65 + return enqueue(`transcode:${sha}`, async () => {
66 + if (existsSync(out)) return out;
67 + const tmp = `${out}.tmp-${process.pid}.mp4`;
68 + try {
69 + await execFileP(
70 + 'ffmpeg',
71 + ['-y', '-i', blobPath(sha),
72 + '-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
73 + '-vf', "scale='min(1920,iw)':-2",
74 + '-c:a', 'aac', '-b:a', '160k',
75 + '-movflags', '+faststart',
76 + tmp],
77 + { timeout: TRANSCODE_TIMEOUT, maxBuffer: 8 * 1024 * 1024 },
78 + );
79 + await rename(tmp, out);
80 + return out;
81 + } catch (err) {
82 + await rm(tmp, { force: true });
83 + throw err;
84 + }
85 + });
86 +}
87 +
88 +/**
89 + * Pre-computed waveform peaks for the audio player (cached JSON).
90 + * @returns {Promise<number[]|null>} ~800 normalized peaks, or null
91 + */
92 +export async function getAudioPeaks(sha) {
93 + const cache = peaksFile(sha);
94 + if (existsSync(cache)) return JSON.parse(await readFile(cache, 'utf8'));
95 + if (!(await hasBin('ffmpeg'))) return null;
96 +
97 + return enqueue(`peaks:${sha}`, async () => {
98 + if (existsSync(cache)) return JSON.parse(await readFile(cache, 'utf8'));
99 + const probe = await probeMedia(sha);
100 + const duration = probe?.duration || 1;
101 + const target = 800;
102 + // Resample to mono s16le at a rate that yields ~target samples-per-bucket work.
103 + const rate = 4000;
104 + const { stdout } = await execFileP(
105 + 'ffmpeg',
106 + ['-i', blobPath(sha), '-ac', '1', '-ar', String(rate), '-f', 's16le', '-'],
107 + { timeout: 10 * 60_000, encoding: 'buffer', maxBuffer: 512 * 1024 * 1024 },
108 + );
109 + const samples = new Int16Array(stdout.buffer, stdout.byteOffset, Math.floor(stdout.length / 2));
110 + const bucket = Math.max(1, Math.floor(samples.length / target));
111 + const peaks = [];
112 + for (let i = 0; i < samples.length; i += bucket) {
113 + let max = 0;
114 + for (let j = i; j < Math.min(i + bucket, samples.length); j += 1) {
115 + const v = Math.abs(samples[j]);
116 + if (v > max) max = v;
117 + }
118 + peaks.push(Number((max / 32768).toFixed(3)));
119 + }
120 + await writeFile(cache, JSON.stringify(peaks));
121 + return peaks;
122 + }).catch(() => null);
123 +}
added src/search/extract.mjs +0 −0

Binary file not shown.

added src/search/index-sync.mjs +62 −0
@@ -0,0 +1,62 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/search/index-sync.mjs
8 + * Purpose : Keep the FTS5 index in sync with node names/tags/content
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { getDb } from '../db/db.mjs';
14 +
15 +/** Current tag names for a node, space-joined for the FTS tags column. */
16 +function tagsText(nodeId) {
17 + return getDb()
18 + .prepare(
19 + `SELECT GROUP_CONCAT(t.name, ' ') AS tags FROM node_tags nt
20 + JOIN tags t ON t.id = nt.tag_id WHERE nt.node_id = ?`,
21 + )
22 + .get(nodeId)?.tags ?? '';
23 +}
24 +
25 +/** Insert or refresh the FTS row for a node's name (keeps existing content). */
26 +export function upsertFtsName(nodeId, name) {
27 + const db = getDb();
28 + const existing = db.prepare('SELECT content FROM fts WHERE rowid = ?').get(nodeId);
29 + db.prepare('DELETE FROM fts WHERE rowid = ?').run(nodeId);
30 + db.prepare('INSERT INTO fts (rowid, name, tags, content) VALUES (?, ?, ?, ?)').run(
31 + nodeId, name, tagsText(nodeId), existing?.content ?? '',
32 + );
33 +}
34 +
35 +/** Refresh only the tags column for a node. */
36 +export function updateFtsTags(nodeId) {
37 + const db = getDb();
38 + const node = db.prepare('SELECT name FROM nodes WHERE id = ?').get(nodeId);
39 + if (node) upsertFtsName(nodeId, node.name);
40 +}
41 +
42 +/** Store extracted text content for a node (search-inside-files). */
43 +export function setFtsContent(nodeId, content, contentSha) {
44 + const db = getDb();
45 + const node = db.prepare('SELECT name FROM nodes WHERE id = ?').get(nodeId);
46 + if (!node) return;
47 + db.prepare('DELETE FROM fts WHERE rowid = ?').run(nodeId);
48 + db.prepare('INSERT INTO fts (rowid, name, tags, content) VALUES (?, ?, ?, ?)').run(
49 + nodeId, node.name, tagsText(nodeId), (content ?? '').slice(0, 1_000_000),
50 + );
51 + db.prepare(
52 + `INSERT INTO fts_state (node_id, content_sha) VALUES (?, ?)
53 + ON CONFLICT(node_id) DO UPDATE SET content_sha = excluded.content_sha`,
54 + ).run(nodeId, contentSha ?? null);
55 +}
56 +
57 +/** Remove a node from the index entirely. */
58 +export function deleteFtsEntry(nodeId) {
59 + const db = getDb();
60 + db.prepare('DELETE FROM fts WHERE rowid = ?').run(nodeId);
61 + db.prepare('DELETE FROM fts_state WHERE node_id = ?').run(nodeId);
62 +}
added src/search/search.mjs +99 −0
@@ -0,0 +1,99 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/search/search.mjs
8 + * Purpose : FTS5 queries with filters (type, tag, folder, dates, size, starred)
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { getDb } from '../db/db.mjs';
14 +
15 +const TYPE_BUCKETS = {
16 + image: "n.mime LIKE 'image/%'",
17 + video: "n.mime LIKE 'video/%'",
18 + audio: "n.mime LIKE 'audio/%'",
19 + doc: "(n.mime = 'application/pdf' OR n.mime LIKE 'text/%' OR n.mime LIKE '%word%' OR n.mime LIKE '%sheet%' OR n.mime LIKE '%presentation%' OR n.mime LIKE '%opendocument%')",
20 + archive: "(n.mime LIKE '%zip%' OR n.mime LIKE '%tar%' OR n.mime LIKE '%7z%' OR n.mime LIKE '%rar%' OR n.mime LIKE '%gzip%')",
21 + folder: "n.type = 'folder'",
22 +};
23 +
24 +/** Escape user input for an FTS5 MATCH — quote each term, keep prefix `*`. */
25 +function ftsQuery(q) {
26 + const terms = String(q).trim().split(/\s+/).filter(Boolean).slice(0, 12);
27 + if (terms.length === 0) return null;
28 + return terms
29 + .map((t) => {
30 + const prefix = t.endsWith('*');
31 + const clean = t.replace(/["']/g, '');
32 + if (!clean) return null;
33 + return `"${clean.replace(/\*/g, '')}"${prefix ? '*' : ''}`;
34 + })
35 + .filter(Boolean)
36 + .join(' ');
37 +}
38 +
39 +/**
40 + * Full-text search over names, tags and extracted content.
41 + * @param {string} q
42 + * @param {{type?: string, tag?: string, folderId?: number, starred?: boolean,
43 + * shared?: boolean, after?: number, before?: number,
44 + * minSize?: number, maxSize?: number, limit?: number}} filters
45 + * @returns {Array<object>} node rows + snippet
46 + */
47 +export function searchNodes(q, filters = {}) {
48 + const db = getDb();
49 + const match = ftsQuery(q);
50 + if (!match) return [];
51 +
52 + const where = ['n.trashed_at IS NULL'];
53 + const params = { match, limit: Math.min(filters.limit ?? 100, 500) };
54 +
55 + if (filters.type && TYPE_BUCKETS[filters.type]) where.push(TYPE_BUCKETS[filters.type]);
56 + if (filters.starred) where.push('n.starred = 1');
57 + if (filters.after) { where.push('n.modified >= @after'); params.after = filters.after; }
58 + if (filters.before) { where.push('n.modified <= @before'); params.before = filters.before; }
59 + if (filters.minSize) { where.push('n.size >= @minSize'); params.minSize = filters.minSize; }
60 + if (filters.maxSize) { where.push('n.size <= @maxSize'); params.maxSize = filters.maxSize; }
61 + if (filters.tag) {
62 + where.push('EXISTS (SELECT 1 FROM node_tags nt JOIN tags t ON t.id = nt.tag_id WHERE nt.node_id = n.id AND t.name = @tag)');
63 + params.tag = filters.tag;
64 + }
65 + if (filters.shared) {
66 + where.push('EXISTS (SELECT 1 FROM shares s WHERE s.node_id = n.id AND s.revoked_at IS NULL)');
67 + }
68 +
69 + let rows = db
70 + .prepare(
71 + `SELECT n.*, snippet(fts, 2, '<mark>', '</mark>', ' … ', 12) AS snippet,
72 + bm25(fts, 8.0, 4.0, 1.0) AS rank
73 + FROM fts JOIN nodes n ON n.id = fts.rowid
74 + WHERE fts MATCH @match AND ${where.join(' AND ')}
75 + ORDER BY rank LIMIT @limit`,
76 + )
77 + .all(params);
78 +
79 + // Folder-scope filter walks ancestry in JS (cheap at these result sizes).
80 + if (filters.folderId) {
81 + const inScope = (id) => {
82 + let cur = db.prepare('SELECT id, parent_id FROM nodes WHERE id = ?').get(id);
83 + while (cur) {
84 + if (cur.id === Number(filters.folderId)) return true;
85 + cur = cur.parent_id
86 + ? db.prepare('SELECT id, parent_id FROM nodes WHERE id = ?').get(cur.parent_id)
87 + : null;
88 + }
89 + return false;
90 + };
91 + rows = rows.filter((r) => inScope(r.id));
92 + }
93 + return rows;
94 +}
95 +
96 +/** Optimize the FTS index (daily maintenance). */
97 +export function vacuumFts() {
98 + getDb().prepare("INSERT INTO fts(fts) VALUES ('optimize')").run();
99 +}
added src/server.mjs +146 −0
@@ -0,0 +1,146 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/server.mjs
8 + * Purpose : Fastify bootstrap — plugins, auth hook, static/vendor assets,
9 + * healthz, daily maintenance, graceful shutdown
10 + * License : MIT © Simon-Pierre Boucher
11 + * ─────────────────────────────────────────────
12 + */
13 +
14 +import path from 'node:path';
15 +import { fileURLToPath } from 'node:url';
16 +import Fastify from 'fastify';
17 +import fastifyCookie from '@fastify/cookie';
18 +import fastifyFormbody from '@fastify/formbody';
19 +import fastifyStatic from '@fastify/static';
20 +import { config, ensureDataDirs } from './config.mjs';
21 +import { ensureAuthBootstrap } from './auth/password.mjs';
22 +import { SESSION_COOKIE, getApiToken, getSession, pruneSessions } from './auth/session.mjs';
23 +import { getDb } from './db/db.mjs';
24 +import { gcBlobs, storageStats } from './storage/blobs.mjs';
25 +import { purgeTrash } from './storage/nodes.mjs';
26 +import { pruneUploads } from './storage/upload.mjs';
27 +import { pruneShares } from './shares/shares.mjs';
28 +import { vacuumFts } from './search/search.mjs';
29 +import { scheduleMissingExtractions } from './search/extract.mjs';
30 +import { queueDepth } from './preview/queue.mjs';
31 +import { registerApiV1 } from './api/v1.mjs';
32 +import { registerWebRoutes } from './web/routes.mjs';
33 +
34 +const HERE = path.dirname(fileURLToPath(import.meta.url));
35 +const startedAt = Date.now();
36 +
37 +async function main() {
38 + ensureDataDirs();
39 + getDb();
40 + await ensureAuthBootstrap();
41 +
42 + const app = Fastify({
43 + logger: {
44 + level: config.logLevel,
45 + redact: {
46 + paths: [
47 + 'req.headers.authorization', 'req.headers.cookie',
48 + 'res.headers["set-cookie"]',
49 + ],
50 + censor: '[redacted]',
51 + },
52 + },
53 + trustProxy: config.trustProxy,
54 + bodyLimit: 32 * 1024 * 1024,
55 + });
56 +
57 + await app.register(fastifyCookie);
58 + await app.register(fastifyFormbody);
59 +
60 + // Upload chunks arrive as raw octet streams; handlers read req.raw directly.
61 + app.addContentTypeParser('application/octet-stream', (req, payload, done) => done(null, null));
62 +
63 + // ── Authentication decorator (session cookie or Bearer API token) ────
64 + app.decorateRequest('authed', false);
65 + app.decorateRequest('authKind', '');
66 + app.addHook('onRequest', async (req) => {
67 + const bearer = req.headers.authorization?.match(/^Bearer\s+(.+)$/i)?.[1];
68 + if (bearer && getApiToken(bearer)) {
69 + req.authed = true;
70 + req.authKind = 'token';
71 + return;
72 + }
73 + const session = getSession(req.cookies?.[SESSION_COOKIE]);
74 + if (session) {
75 + req.authed = true;
76 + req.authKind = 'session';
77 + }
78 + });
79 +
80 + // ── Static assets (immutable app code + self-hosted vendor bundles) ──
81 + await app.register(fastifyStatic, {
82 + root: path.join(HERE, 'web', 'assets'),
83 + prefix: '/assets/',
84 + maxAge: '1h',
85 + cacheControl: true,
86 + });
87 + const vendor = (prefix, ...segments) =>
88 + app.register(fastifyStatic, {
89 + root: path.resolve(HERE, '..', 'node_modules', ...segments),
90 + prefix, decorateReply: false, maxAge: '7d',
91 + });
92 + await vendor('/vendor/pdfjs/', 'pdfjs-dist', 'build');
93 + await vendor('/vendor/wavesurfer/', 'wavesurfer.js', 'dist');
94 + await vendor('/vendor/xlsx/', 'xlsx', 'dist');
95 + await vendor('/vendor/fonts/inter/', '@fontsource', 'inter', 'files');
96 + await vendor('/vendor/fonts/jetbrains-mono/', '@fontsource', 'jetbrains-mono', 'files');
97 +
98 + // ── Health ────────────────────────────────────────────────────────────
99 + app.get('/healthz', async () => {
100 + const stats = storageStats();
101 + return {
102 + ok: true,
103 + uptimeSec: Math.floor((Date.now() - startedAt) / 1000),
104 + nodes: stats.nodeCount,
105 + usedBytes: stats.usedBytes,
106 + queueDepth: queueDepth(),
107 + };
108 + });
109 +
110 + registerApiV1(app);
111 + registerWebRoutes(app);
112 +
113 + // ── Daily maintenance ────────────────────────────────────────────────
114 + const maintenance = async () => {
115 + try {
116 + const purged = purgeTrash(config.trashRetentionDays);
117 + const gcd = await gcBlobs();
118 + const shares = pruneShares();
119 + const sessions = pruneSessions();
120 + const uploads = await pruneUploads();
121 + vacuumFts();
122 + const queued = scheduleMissingExtractions();
123 + app.log.info({ purged, gcd, shares, sessions, uploads, queued }, 'maintenance done');
124 + } catch (err) {
125 + app.log.error({ err }, 'maintenance failed');
126 + }
127 + };
128 + setTimeout(maintenance, 30_000).unref();
129 + setInterval(maintenance, 24 * 3_600_000).unref();
130 +
131 + await app.listen({ port: config.port, host: config.host });
132 + app.log.info(`SPB Drive listening on http://${config.host}:${config.port} → ${config.publicUrl}`);
133 +
134 + const shutdown = async (signal) => {
135 + app.log.info({ signal }, 'shutting down');
136 + await app.close();
137 + process.exit(0);
138 + };
139 + process.on('SIGINT', () => shutdown('SIGINT'));
140 + process.on('SIGTERM', () => shutdown('SIGTERM'));
141 +}
142 +
143 +main().catch((err) => {
144 + console.error('Fatal boot error:', err);
145 + process.exit(1);
146 +});
added src/shares/shares.mjs +180 −0
@@ -0,0 +1,180 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/shares/shares.mjs
8 + * Purpose : Share links — base58 tokens, expiry, passwords, limits, logging
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { randomBytes, timingSafeEqual } from 'node:crypto';
14 +import argon2 from 'argon2';
15 +import { getDb, logActivity } from '../db/db.mjs';
16 +import { mustGetNode } from '../storage/nodes.mjs';
17 +
18 +// 58^10 ≈ 4.3e17 ≈ 58.6 bits of entropy — matches the security checklist.
19 +const BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
20 +const TOKEN_LEN = 10;
21 +
22 +function makeToken() {
23 + const bytes = randomBytes(TOKEN_LEN * 2);
24 + let out = '';
25 + for (let i = 0; out.length < TOKEN_LEN && i < bytes.length; i += 1) {
26 + // Rejection sampling keeps the distribution uniform across the alphabet.
27 + if (bytes[i] < 232) out += BASE58[bytes[i] % 58];
28 + }
29 + while (out.length < TOKEN_LEN) out += BASE58[randomBytes(1)[0] % 58];
30 + return out;
31 +}
32 +
33 +/**
34 + * Create a share for a node.
35 + * @param {{expiresAt?: number|null, password?: string|null, maxDownloads?: number|null,
36 + * allowDownload?: boolean, label?: string|null, ip?: string}} opts
37 + */
38 +export async function createShare(nodeId, opts = {}) {
39 + const node = mustGetNode(nodeId);
40 + if (node.trashed_at) throw Object.assign(new Error('Cannot share a trashed item'), { statusCode: 400 });
41 + const token = makeToken();
42 + const passwordHash = opts.password
43 + ? await argon2.hash(opts.password, { type: argon2.argon2id })
44 + : null;
45 + const info = getDb()
46 + .prepare(
47 + `INSERT INTO shares (token, node_id, created, expires_at, password_hash, max_downloads, allow_download, label)
48 + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
49 + )
50 + .run(
51 + token, nodeId, Date.now(), opts.expiresAt ?? null, passwordHash,
52 + opts.maxDownloads ?? null, opts.allowDownload === false ? 0 : 1, opts.label ?? null,
53 + );
54 + logActivity('share.create', { nodeId, detail: token, ip: opts.ip ?? '' });
55 + return getShareById(Number(info.lastInsertRowid));
56 +}
57 +
58 +/** Update share options (expiry, password, limits, label). */
59 +export async function updateShare(id, opts = {}) {
60 + const db = getDb();
61 + const share = getShareById(id);
62 + if (!share) return null;
63 + const passwordHash = opts.password === undefined
64 + ? share.password_hash
65 + : opts.password
66 + ? await argon2.hash(opts.password, { type: argon2.argon2id })
67 + : null;
68 + db.prepare(
69 + `UPDATE shares SET expires_at = ?, password_hash = ?, max_downloads = ?, allow_download = ?, label = ?
70 + WHERE id = ?`,
71 + ).run(
72 + opts.expiresAt !== undefined ? opts.expiresAt : share.expires_at,
73 + passwordHash,
74 + opts.maxDownloads !== undefined ? opts.maxDownloads : share.max_downloads,
75 + opts.allowDownload !== undefined ? (opts.allowDownload ? 1 : 0) : share.allow_download,
76 + opts.label !== undefined ? opts.label : share.label,
77 + id,
78 + );
79 + return getShareById(id);
80 +}
81 +
82 +export function getShareById(id) {
83 + return getDb().prepare('SELECT * FROM shares WHERE id = ?').get(id) ?? null;
84 +}
85 +
86 +/**
87 + * Constant-time token lookup.
88 + * @returns {object|null} the share row (regardless of validity)
89 + */
90 +export function getShareByToken(token) {
91 + if (typeof token !== 'string' || token.length !== TOKEN_LEN) return null;
92 + // Indexed fetch, then constant-time comparison of the candidate.
93 + const row = getDb().prepare('SELECT * FROM shares WHERE token = ?').get(token);
94 + if (!row) return null;
95 + const a = Buffer.from(row.token);
96 + const b = Buffer.from(token);
97 + if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
98 + return row;
99 +}
100 +
101 +/**
102 + * Validity check used by every public route.
103 + * @returns {{ok: true} | {ok: false, reason: 'revoked'|'expired'|'exhausted'|'missing'}}
104 + */
105 +export function shareValidity(share) {
106 + if (!share) return { ok: false, reason: 'missing' };
107 + if (share.revoked_at) return { ok: false, reason: 'revoked' };
108 + if (share.expires_at && share.expires_at < Date.now()) return { ok: false, reason: 'expired' };
109 + if (share.max_downloads !== null && share.downloads >= share.max_downloads) {
110 + return { ok: false, reason: 'exhausted' };
111 + }
112 + return { ok: true };
113 +}
114 +
115 +/** Check a visitor-supplied password against the share. */
116 +export async function verifySharePassword(share, password) {
117 + if (!share.password_hash) return true;
118 + try {
119 + return await argon2.verify(share.password_hash, password ?? '');
120 + } catch {
121 + return false;
122 + }
123 +}
124 +
125 +/** Record a visit or download (also mirrored into the activity log). */
126 +export function recordShareEvent(shareId, kind, { ip = '', ua = '' } = {}) {
127 + const db = getDb();
128 + db.prepare('INSERT INTO share_events (share_id, ts, kind, ip, ua) VALUES (?, ?, ?, ?, ?)')
129 + .run(shareId, Date.now(), kind, ip, ua.slice(0, 300));
130 + db.prepare(
131 + kind === 'download'
132 + ? 'UPDATE shares SET downloads = downloads + 1 WHERE id = ?'
133 + : 'UPDATE shares SET visits = visits + 1 WHERE id = ?',
134 + ).run(shareId);
135 + const share = getShareById(shareId);
136 + logActivity(`share.${kind}`, { nodeId: share?.node_id, detail: share?.token, ip });
137 +}
138 +
139 +/** All shares with node info, newest first (share manager). */
140 +export function listShares() {
141 + return getDb()
142 + .prepare(
143 + `SELECT s.*, n.name AS node_name, n.type AS node_type, n.size AS node_size, n.mime AS node_mime
144 + FROM shares s JOIN nodes n ON n.id = s.node_id
145 + ORDER BY s.created DESC`,
146 + )
147 + .all();
148 +}
149 +
150 +/** Shares attached to one node (info panel). */
151 +export function sharesForNode(nodeId) {
152 + return getDb().prepare('SELECT * FROM shares WHERE node_id = ? AND revoked_at IS NULL ORDER BY created DESC').all(nodeId);
153 +}
154 +
155 +/** Recent visit/download events for one share. */
156 +export function shareEvents(shareId, limit = 100) {
157 + return getDb()
158 + .prepare('SELECT ts, kind, ip, ua FROM share_events WHERE share_id = ? ORDER BY ts DESC LIMIT ?')
159 + .all(shareId, limit);
160 +}
161 +
162 +/** Revoke immediately (public link dies on the next request). */
163 +export function revokeShare(id, { ip = '' } = {}) {
164 + const share = getShareById(id);
165 + if (!share) return false;
166 + getDb().prepare('UPDATE shares SET revoked_at = ? WHERE id = ?').run(Date.now(), id);
167 + logActivity('share.revoke', { nodeId: share.node_id, detail: share.token, ip });
168 + return true;
169 +}
170 +
171 +/** Delete revoked/expired shares older than 90 days (daily job). */
172 +export function pruneShares() {
173 + const cutoff = Date.now() - 90 * 86_400_000;
174 + return getDb()
175 + .prepare(
176 + `DELETE FROM shares WHERE (revoked_at IS NOT NULL AND revoked_at < ?)
177 + OR (expires_at IS NOT NULL AND expires_at < ?)`,
178 + )
179 + .run(cutoff, cutoff).changes;
180 +}
added src/storage/blobs.mjs +128 −0
@@ -0,0 +1,128 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/storage/blobs.mjs
8 + * Purpose : Content-addressed blob store — sha256 CAS, refcounts, GC
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { createHash, randomBytes } from 'node:crypto';
14 +import { createReadStream, createWriteStream, existsSync } from 'node:fs';
15 +import { mkdir, rename, rm, stat } from 'node:fs/promises';
16 +import path from 'node:path';
17 +import { pipeline } from 'node:stream/promises';
18 +import { config } from '../config.mjs';
19 +import { getDb } from '../db/db.mjs';
20 +
21 +/** Absolute on-disk path for a blob sha. */
22 +export function blobPath(sha) {
23 + return path.join(config.filesDir, sha.slice(0, 2), sha.slice(2, 4), sha);
24 +}
25 +
26 +/**
27 + * Store a readable stream in the CAS. Streams to a temp file while hashing,
28 + * then moves into place — identical content dedupes naturally.
29 + * @param {NodeJS.ReadableStream} stream
30 + * @returns {Promise<{sha: string, size: number, deduped: boolean}>}
31 + */
32 +export async function putBlob(stream) {
33 + const tmp = path.join(config.cacheDir, 'tmp', `put-${randomBytes(8).toString('hex')}`);
34 + const hash = createHash('sha256');
35 + let size = 0;
36 +
37 + await pipeline(
38 + stream,
39 + async function* (source) {
40 + for await (const chunk of source) {
41 + hash.update(chunk);
42 + size += chunk.length;
43 + yield chunk;
44 + }
45 + },
46 + createWriteStream(tmp, { mode: 0o600 }),
47 + );
48 +
49 + const sha = hash.digest('hex');
50 + const dest = blobPath(sha);
51 + const deduped = existsSync(dest);
52 + if (deduped) {
53 + await rm(tmp, { force: true });
54 + } else {
55 + await mkdir(path.dirname(dest), { recursive: true, mode: 0o700 });
56 + await rename(tmp, dest);
57 + }
58 +
59 + getDb()
60 + .prepare(
61 + `INSERT INTO blobs (sha, size, refcount, created) VALUES (?, ?, 0, ?)
62 + ON CONFLICT(sha) DO NOTHING`,
63 + )
64 + .run(sha, size, Date.now());
65 + return { sha, size, deduped };
66 +}
67 +
68 +/** Increment a blob's refcount (one node now points at it). */
69 +export function refBlob(sha) {
70 + getDb().prepare('UPDATE blobs SET refcount = refcount + 1 WHERE sha = ?').run(sha);
71 +}
72 +
73 +/** Decrement a blob's refcount. Physical deletion happens in gcBlobs(). */
74 +export function unrefBlob(sha) {
75 + getDb().prepare('UPDATE blobs SET refcount = MAX(refcount - 1, 0) WHERE sha = ?').run(sha);
76 +}
77 +
78 +/** Readable stream over a blob (optionally a byte range). */
79 +export function blobStream(sha, { start, end } = {}) {
80 + return createReadStream(blobPath(sha), start !== undefined ? { start, end } : {});
81 +}
82 +
83 +/** Blob size from disk (throws if missing). */
84 +export async function blobSize(sha) {
85 + return (await stat(blobPath(sha))).size;
86 +}
87 +
88 +/**
89 + * Delete blobs with refcount 0 (and their cache artifacts).
90 + * @returns {Promise<number>} number of blobs removed
91 + */
92 +export async function gcBlobs() {
93 + const db = getDb();
94 + const orphans = db.prepare('SELECT sha FROM blobs WHERE refcount <= 0').all();
95 + for (const { sha } of orphans) {
96 + await rm(blobPath(sha), { force: true });
97 + for (const sub of ['thumbs', 'transcode', 'office', 'peaks', 'text']) {
98 + const dir = path.join(config.cacheDir, sub);
99 + for (const suffix of ['', '.256.webp', '.512.webp', '.pdf', '.mp4', '.json', '.txt']) {
100 + await rm(path.join(dir, `${sha}${suffix}`), { force: true });
101 + }
102 + }
103 + db.prepare('DELETE FROM blobs WHERE sha = ?').run(sha);
104 + }
105 + return orphans.length;
106 +}
107 +
108 +/** Aggregate storage stats for /healthz and the storage meter. */
109 +export function storageStats() {
110 + const db = getDb();
111 + const blobs = db.prepare('SELECT COUNT(*) AS n, COALESCE(SUM(size), 0) AS bytes FROM blobs WHERE refcount > 0').get();
112 + const nodes = db.prepare("SELECT COUNT(*) AS n FROM nodes WHERE trashed_at IS NULL AND id != 1").get();
113 + const byType = db
114 + .prepare(
115 + `SELECT CASE
116 + WHEN mime LIKE 'image/%' THEN 'images'
117 + WHEN mime LIKE 'video/%' THEN 'video'
118 + WHEN mime LIKE 'audio/%' THEN 'audio'
119 + WHEN mime LIKE 'application/pdf' OR mime LIKE '%word%' OR mime LIKE '%sheet%'
120 + OR mime LIKE '%presentation%' OR mime LIKE 'text/%' THEN 'docs'
121 + WHEN mime LIKE '%zip%' OR mime LIKE '%tar%' OR mime LIKE '%7z%' OR mime LIKE '%rar%' THEN 'archives'
122 + ELSE 'other' END AS bucket,
123 + COALESCE(SUM(size), 0) AS bytes, COUNT(*) AS n
124 + FROM nodes WHERE type = 'file' AND trashed_at IS NULL GROUP BY bucket`,
125 + )
126 + .all();
127 + return { blobCount: blobs.n, usedBytes: blobs.bytes, nodeCount: nodes.n, byType };
128 +}
added src/storage/nodes.mjs +346 −0
@@ -0,0 +1,346 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/storage/nodes.mjs
8 + * Purpose : Virtual tree operations — mkdir, move, copy, rename, trash, purge
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { getDb, logActivity } from '../db/db.mjs';
14 +import { refBlob, unrefBlob } from './blobs.mjs';
15 +import { upsertFtsName, deleteFtsEntry } from '../search/index-sync.mjs';
16 +
17 +export const ROOT_ID = 1;
18 +const NAME_RE = /^[^/\\\0]{1,255}$/;
19 +
20 +/** Throw unless a client-supplied name is safe (no slashes, no dot-dirs). */
21 +export function validateName(name) {
22 + if (typeof name !== 'string' || !NAME_RE.test(name) || name === '.' || name === '..') {
23 + const err = new Error('Invalid name');
24 + err.statusCode = 400;
25 + throw err;
26 + }
27 + return name;
28 +}
29 +
30 +/** Fetch a node row by id (null if absent). */
31 +export function getNode(id) {
32 + return getDb().prepare('SELECT * FROM nodes WHERE id = ?').get(id) ?? null;
33 +}
34 +
35 +/** Fetch a node or throw 404. */
36 +export function mustGetNode(id) {
37 + const node = getNode(id);
38 + if (!node) {
39 + const err = new Error('Not found');
40 + err.statusCode = 404;
41 + throw err;
42 + }
43 + return node;
44 +}
45 +
46 +const SORTS = {
47 + name: 'name COLLATE NOCASE',
48 + size: 'size',
49 + modified: 'modified',
50 + type: 'mime',
51 +};
52 +
53 +/** Non-trashed children of a folder, folders always first. */
54 +export function childrenOf(parentId, { sort = 'name', dir = 'asc' } = {}) {
55 + const col = SORTS[sort] ?? SORTS.name;
56 + const d = dir === 'desc' ? 'DESC' : 'ASC';
57 + return getDb()
58 + .prepare(
59 + `SELECT * FROM nodes WHERE parent_id = ? AND trashed_at IS NULL
60 + ORDER BY (type = 'folder') DESC, ${col} ${d}`,
61 + )
62 + .all(parentId);
63 +}
64 +
65 +/** Breadcrumb chain from root to the node (inclusive). */
66 +export function pathOf(id) {
67 + const chain = [];
68 + let node = getNode(id);
69 + while (node) {
70 + chain.unshift(node);
71 + node = node.parent_id ? getNode(node.parent_id) : null;
72 + }
73 + return chain;
74 +}
75 +
76 +/** Human path string like /Photos/2026/img.jpg */
77 +export function pathString(id) {
78 + return `/${pathOf(id).slice(1).map((n) => n.name).join('/')}`;
79 +}
80 +
81 +/** Resolve an absolute virtual path to a node (null if missing). */
82 +export function resolvePath(p) {
83 + const parts = String(p ?? '/').split('/').filter(Boolean);
84 + let node = mustGetNode(ROOT_ID);
85 + for (const part of parts) {
86 + node = getDb()
87 + .prepare('SELECT * FROM nodes WHERE parent_id = ? AND name = ? AND trashed_at IS NULL')
88 + .get(node.id, part);
89 + if (!node) return null;
90 + }
91 + return node;
92 +}
93 +
94 +/** Existing non-trashed child by name (case-sensitive). */
95 +export function childByName(parentId, name) {
96 + return getDb()
97 + .prepare('SELECT * FROM nodes WHERE parent_id = ? AND name = ? AND trashed_at IS NULL')
98 + .get(parentId, name) ?? null;
99 +}
100 +
101 +/** "photo.jpg" → "photo-2.jpg" (first free suffix) for keep-both conflicts. */
102 +export function uniqueName(parentId, name) {
103 + if (!childByName(parentId, name)) return name;
104 + const dot = name.lastIndexOf('.');
105 + const stem = dot > 0 ? name.slice(0, dot) : name;
106 + const ext = dot > 0 ? name.slice(dot) : '';
107 + for (let i = 2; i < 10_000; i += 1) {
108 + const candidate = `${stem}-${i}${ext}`;
109 + if (!childByName(parentId, candidate)) return candidate;
110 + }
111 + throw new Error('Could not find a free name');
112 +}
113 +
114 +/**
115 + * Create a folder. Returns the existing folder if one with that name exists
116 + * (mkdir -p semantics).
117 + */
118 +export function mkdir(parentId, name, { ip = '' } = {}) {
119 + validateName(name);
120 + const parent = mustGetNode(parentId);
121 + if (parent.type !== 'folder') throw Object.assign(new Error('Parent is not a folder'), { statusCode: 400 });
122 + const existing = childByName(parentId, name);
123 + if (existing?.type === 'folder') return existing;
124 + if (existing) throw Object.assign(new Error('A file with that name exists'), { statusCode: 409 });
125 + const now = Date.now();
126 + const info = getDb()
127 + .prepare(
128 + `INSERT INTO nodes (parent_id, name, type, created, modified) VALUES (?, ?, 'folder', ?, ?)`,
129 + )
130 + .run(parentId, name, now, now);
131 + const id = Number(info.lastInsertRowid);
132 + upsertFtsName(id, name);
133 + logActivity('folder.create', { nodeId: id, detail: name, ip });
134 + return getNode(id);
135 +}
136 +
137 +/**
138 + * Create a file node pointing at a stored blob.
139 + * @param {'keep-both'|'replace'|'skip'} conflict
140 + * @returns {object|null} the node, or null when skipped
141 + */
142 +export function createFileNode(parentId, name, { sha, size, mime, conflict = 'keep-both', ip = '' }) {
143 + validateName(name);
144 + mustGetNode(parentId);
145 + const existing = childByName(parentId, name);
146 + let finalName = name;
147 + if (existing) {
148 + if (conflict === 'skip') return null;
149 + if (conflict === 'replace' && existing.type === 'file') {
150 + trashNode(existing.id, { ip });
151 + } else {
152 + finalName = uniqueName(parentId, name);
153 + }
154 + }
155 + const now = Date.now();
156 + const info = getDb()
157 + .prepare(
158 + `INSERT INTO nodes (parent_id, name, type, blob_sha, size, mime, created, modified)
159 + VALUES (?, ?, 'file', ?, ?, ?, ?, ?)`,
160 + )
161 + .run(parentId, finalName, sha, size, mime ?? null, now, now);
162 + refBlob(sha);
163 + const id = Number(info.lastInsertRowid);
164 + upsertFtsName(id, finalName);
165 + logActivity('file.upload', { nodeId: id, detail: finalName, ip });
166 + return getNode(id);
167 +}
168 +
169 +/** Rename a node in place. */
170 +export function renameNode(id, newName, { ip = '' } = {}) {
171 + validateName(newName);
172 + const node = mustGetNode(id);
173 + if (id === ROOT_ID) throw Object.assign(new Error('Cannot rename root'), { statusCode: 400 });
174 + if (childByName(node.parent_id, newName) && newName !== node.name) {
175 + throw Object.assign(new Error('Name already taken'), { statusCode: 409 });
176 + }
177 + getDb().prepare('UPDATE nodes SET name = ?, modified = ? WHERE id = ?').run(newName, Date.now(), id);
178 + upsertFtsName(id, newName);
179 + logActivity('node.rename', { nodeId: id, detail: `${node.name} → ${newName}`, ip });
180 + return getNode(id);
181 +}
182 +
183 +/** True if `maybeDescendant` sits under `ancestorId` (or equals it). */
184 +export function isDescendant(maybeDescendant, ancestorId) {
185 + let node = getNode(maybeDescendant);
186 + while (node) {
187 + if (node.id === ancestorId) return true;
188 + node = node.parent_id ? getNode(node.parent_id) : null;
189 + }
190 + return false;
191 +}
192 +
193 +/** Move a node under a new parent (with cycle + conflict handling). */
194 +export function moveNode(id, newParentId, { conflict = 'keep-both', ip = '' } = {}) {
195 + const node = mustGetNode(id);
196 + const parent = mustGetNode(newParentId);
197 + if (id === ROOT_ID) throw Object.assign(new Error('Cannot move root'), { statusCode: 400 });
198 + if (parent.type !== 'folder') throw Object.assign(new Error('Target is not a folder'), { statusCode: 400 });
199 + if (isDescendant(newParentId, id)) {
200 + throw Object.assign(new Error('Cannot move a folder into itself'), { statusCode: 400 });
201 + }
202 + if (node.parent_id === newParentId) return node;
203 + let name = node.name;
204 + if (childByName(newParentId, name)) {
205 + if (conflict === 'skip') return null;
206 + if (conflict === 'replace') {
207 + const clash = childByName(newParentId, name);
208 + if (clash) trashNode(clash.id, { ip });
209 + } else {
210 + name = uniqueName(newParentId, name);
211 + }
212 + }
213 + getDb()
214 + .prepare('UPDATE nodes SET parent_id = ?, name = ?, modified = ? WHERE id = ?')
215 + .run(newParentId, name, Date.now(), id);
216 + logActivity('node.move', { nodeId: id, detail: pathString(id), ip });
217 + return getNode(id);
218 +}
219 +
220 +/** Deep-copy a node (files share blobs — refcounted, no bytes copied). */
221 +export function copyNode(id, destParentId, { ip = '' } = {}) {
222 + const node = mustGetNode(id);
223 + mustGetNode(destParentId);
224 + if (node.type === 'folder' && isDescendant(destParentId, id)) {
225 + throw Object.assign(new Error('Cannot copy a folder into itself'), { statusCode: 400 });
226 + }
227 + const db = getDb();
228 + const now = Date.now();
229 +
230 + const cloneInto = (src, parentId, name) => {
231 + const info = db
232 + .prepare(
233 + `INSERT INTO nodes (parent_id, name, type, blob_sha, size, mime, created, modified, starred, color, emoji)
234 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
235 + )
236 + .run(parentId, name, src.type, src.blob_sha, src.size, src.mime, now, now, src.starred, src.color, src.emoji);
237 + const newId = Number(info.lastInsertRowid);
238 + if (src.blob_sha) refBlob(src.blob_sha);
239 + upsertFtsName(newId, name);
240 + if (src.type === 'folder') {
241 + for (const child of childrenOf(src.id)) cloneInto(child, newId, child.name);
242 + }
243 + return newId;
244 + };
245 +
246 + const name = uniqueName(destParentId, node.name);
247 + const newId = cloneInto(node, destParentId, name);
248 + logActivity('node.copy', { nodeId: newId, detail: `${node.name} → ${pathString(destParentId)}`, ip });
249 + return getNode(newId);
250 +}
251 +
252 +/** Duplicate next to the original ("photo.jpg" → "photo-2.jpg"). */
253 +export function duplicateNode(id, opts = {}) {
254 + const node = mustGetNode(id);
255 + return copyNode(id, node.parent_id, opts);
256 +}
257 +
258 +/** Soft delete — remembers the original parent for restore. */
259 +export function trashNode(id, { ip = '' } = {}) {
260 + const node = mustGetNode(id);
261 + if (id === ROOT_ID) throw Object.assign(new Error('Cannot trash root'), { statusCode: 400 });
262 + if (node.trashed_at) return node;
263 + getDb()
264 + .prepare('UPDATE nodes SET trashed_at = ?, trash_orig_parent = parent_id WHERE id = ?')
265 + .run(Date.now(), id);
266 + logActivity('node.trash', { nodeId: id, detail: node.name, ip });
267 + return getNode(id);
268 +}
269 +
270 +/** Restore from trash to its original folder (root if that folder is gone). */
271 +export function restoreNode(id, { ip = '' } = {}) {
272 + const node = mustGetNode(id);
273 + if (!node.trashed_at) return node;
274 + let parentId = node.trash_orig_parent ?? ROOT_ID;
275 + const parent = getNode(parentId);
276 + if (!parent || parent.trashed_at) parentId = ROOT_ID;
277 + const name = uniqueName(parentId, node.name);
278 + getDb()
279 + .prepare('UPDATE nodes SET trashed_at = NULL, trash_orig_parent = NULL, parent_id = ?, name = ? WHERE id = ?')
280 + .run(parentId, name, id);
281 + logActivity('node.restore', { nodeId: id, detail: node.name, ip });
282 + return getNode(id);
283 +}
284 +
285 +/** Hard delete — recursively unrefs blobs and removes rows + FTS entries. */
286 +export function deleteForever(id, { ip = '' } = {}) {
287 + const node = mustGetNode(id);
288 + if (id === ROOT_ID) throw Object.assign(new Error('Cannot delete root'), { statusCode: 400 });
289 + const db = getDb();
290 +
291 + const wipe = (n) => {
292 + const kids = db.prepare('SELECT * FROM nodes WHERE parent_id = ?').all(n.id);
293 + for (const kid of kids) wipe(kid);
294 + if (n.blob_sha) unrefBlob(n.blob_sha);
295 + deleteFtsEntry(n.id);
296 + db.prepare('DELETE FROM nodes WHERE id = ?').run(n.id);
297 + };
298 +
299 + wipe(node);
300 + logActivity('node.delete_forever', { detail: node.name, ip });
301 +}
302 +
303 +/** Top-level items sitting in the trash. */
304 +export function listTrash() {
305 + return getDb()
306 + .prepare(
307 + `SELECT n.* FROM nodes n
308 + LEFT JOIN nodes p ON p.id = n.parent_id
309 + WHERE n.trashed_at IS NOT NULL AND (p.trashed_at IS NULL OR p.id IS NULL)
310 + ORDER BY n.trashed_at DESC`,
311 + )
312 + .all();
313 +}
314 +
315 +/** Hard-delete trash entries older than `days` (daily job). */
316 +export function purgeTrash(days) {
317 + const cutoff = Date.now() - days * 86_400_000;
318 + const old = getDb()
319 + .prepare('SELECT id FROM nodes WHERE trashed_at IS NOT NULL AND trashed_at < ?')
320 + .all(cutoff);
321 + for (const { id } of old) {
322 + if (getNode(id)) deleteForever(id);
323 + }
324 + return old.length;
325 +}
326 +
327 +/** Every non-trashed file underneath a folder, with subtree-relative paths. */
328 +export function listDescendantFiles(folderId, prefix = '') {
329 + const out = [];
330 + for (const child of childrenOf(folderId)) {
331 + const rel = prefix ? `${prefix}/${child.name}` : child.name;
332 + if (child.type === 'folder') out.push(...listDescendantFiles(child.id, rel));
333 + else out.push({ node: child, relPath: rel });
334 + }
335 + return out;
336 +}
337 +
338 +/** Last-touched files for the Recent view. */
339 +export function recentFiles(limit = 50) {
340 + return getDb()
341 + .prepare(
342 + `SELECT * FROM nodes WHERE type = 'file' AND trashed_at IS NULL
343 + ORDER BY modified DESC LIMIT ?`,
344 + )
345 + .all(limit);
346 +}
added src/storage/upload.mjs +130 −0
@@ -0,0 +1,130 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/storage/upload.mjs
8 + * Purpose : Chunked + resumable uploads (init / chunk / complete / status)
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { randomBytes } from 'node:crypto';
14 +import { createReadStream, createWriteStream, existsSync } from 'node:fs';
15 +import { mkdir, readdir, rm } from 'node:fs/promises';
16 +import path from 'node:path';
17 +import { pipeline } from 'node:stream/promises';
18 +import { config } from '../config.mjs';
19 +import { getDb } from '../db/db.mjs';
20 +import { putBlob } from './blobs.mjs';
21 +import { createFileNode, mustGetNode, validateName } from './nodes.mjs';
22 +
23 +const uploadDir = (id) => path.join(config.uploadsDir, id);
24 +
25 +/**
26 + * Start (or describe) an upload session.
27 + * @returns {{uploadId: string, chunkSize: number, nChunks: number, have: number[]}}
28 + */
29 +export async function initUpload({ parentId, name, size }) {
30 + validateName(name);
31 + mustGetNode(parentId);
32 + const sz = Number(size);
33 + if (!Number.isFinite(sz) || sz < 0) throw Object.assign(new Error('Bad size'), { statusCode: 400 });
34 +
35 + const id = randomBytes(16).toString('hex');
36 + const nChunks = Math.max(1, Math.ceil(sz / config.chunkSize));
37 + getDb()
38 + .prepare(
39 + `INSERT INTO uploads (id, parent_id, name, size, chunk_size, n_chunks, created)
40 + VALUES (?, ?, ?, ?, ?, ?, ?)`,
41 + )
42 + .run(id, parentId, name, sz, config.chunkSize, nChunks, Date.now());
43 + await mkdir(uploadDir(id), { recursive: true, mode: 0o700 });
44 + return { uploadId: id, chunkSize: config.chunkSize, nChunks, have: [] };
45 +}
46 +
47 +/** Look up an upload session row or throw 404. */
48 +export function getUpload(id) {
49 + const row = getDb().prepare('SELECT * FROM uploads WHERE id = ?').get(String(id));
50 + if (!row) throw Object.assign(new Error('Unknown upload'), { statusCode: 404 });
51 + return row;
52 +}
53 +
54 +/** Which chunk indexes are already on disk (for resume). */
55 +export async function chunksPresent(id) {
56 + getUpload(id);
57 + try {
58 + const entries = await readdir(uploadDir(id));
59 + return entries
60 + .filter((e) => /^\d+$/.test(e))
61 + .map(Number)
62 + .sort((a, b) => a - b);
63 + } catch {
64 + return [];
65 + }
66 +}
67 +
68 +/** Persist one chunk from a request stream. */
69 +export async function writeChunk(id, n, stream) {
70 + const up = getUpload(id);
71 + const idx = Number(n);
72 + if (!Number.isInteger(idx) || idx < 0 || idx >= up.n_chunks) {
73 + throw Object.assign(new Error('Chunk index out of range'), { statusCode: 400 });
74 + }
75 + await mkdir(uploadDir(id), { recursive: true, mode: 0o700 });
76 + const tmp = path.join(uploadDir(id), `${idx}.part`);
77 + await pipeline(stream, createWriteStream(tmp, { mode: 0o600 }));
78 + const { rename } = await import('node:fs/promises');
79 + await rename(tmp, path.join(uploadDir(id), String(idx)));
80 +}
81 +
82 +/**
83 + * Assemble chunks → CAS blob → file node. Cleans up the session.
84 + * @param {'keep-both'|'replace'|'skip'} conflict
85 + */
86 +export async function completeUpload(id, { conflict = 'keep-both', mime = null, ip = '' } = {}) {
87 + const up = getUpload(id);
88 + const have = await chunksPresent(id);
89 + if (have.length !== up.n_chunks) {
90 + throw Object.assign(
91 + new Error(`Missing chunks: have ${have.length}/${up.n_chunks}`),
92 + { statusCode: 409 },
93 + );
94 + }
95 +
96 + async function* concat() {
97 + for (let i = 0; i < up.n_chunks; i += 1) {
98 + const chunkFile = path.join(uploadDir(id), String(i));
99 + if (!existsSync(chunkFile)) throw new Error(`Chunk ${i} vanished`);
100 + yield* createReadStream(chunkFile);
101 + }
102 + }
103 +
104 + const { sha, size } = await putBlob(concat());
105 + if (size !== up.size) {
106 + throw Object.assign(
107 + new Error(`Size mismatch: expected ${up.size}, got ${size}`),
108 + { statusCode: 409 },
109 + );
110 + }
111 +
112 + const node = createFileNode(up.parent_id, up.name, { sha, size, mime, conflict, ip });
113 + await abortUpload(id);
114 + return node;
115 +}
116 +
117 +/** Drop an upload session and its chunk files. */
118 +export async function abortUpload(id) {
119 + getDb().prepare('DELETE FROM uploads WHERE id = ?').run(String(id));
120 + await rm(uploadDir(String(id)), { recursive: true, force: true });
121 +}
122 +
123 +/** Remove upload sessions older than 48 h (daily job). */
124 +export async function pruneUploads() {
125 + const stale = getDb()
126 + .prepare('SELECT id FROM uploads WHERE created < ?')
127 + .all(Date.now() - 48 * 3_600_000);
128 + for (const { id } of stale) await abortUpload(id);
129 + return stale.length;
130 +}
added src/web/http-helpers.mjs +113 −0
@@ -0,0 +1,113 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/web/http-helpers.mjs
8 + * Purpose : Range-aware blob responses, rate limiting, content-type policy
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { blobPath, blobStream } from '../storage/blobs.mjs';
14 +import { stat } from 'node:fs/promises';
15 +
16 +/** MIME types that are safe to render inline; everything else downloads. */
17 +const INLINE_SAFE = /^(image\/(?!svg)|video\/|audio\/|application\/pdf|text\/plain$)/;
18 +
19 +/** RFC7233 single-range parser. Returns null when absent/invalid. */
20 +export function parseRange(header, size) {
21 + if (!header || !header.startsWith('bytes=')) return null;
22 + const m = header.slice(6).split(',')[0].trim().match(/^(\d*)-(\d*)$/);
23 + if (!m || (m[1] === '' && m[2] === '')) return null;
24 + let start;
25 + let end;
26 + if (m[1] === '') {
27 + // suffix range: last N bytes
28 + const n = Number(m[2]);
29 + if (n === 0) return null;
30 + start = Math.max(0, size - n);
31 + end = size - 1;
32 + } else {
33 + start = Number(m[1]);
34 + end = m[2] === '' ? size - 1 : Number(m[2]);
35 + }
36 + if (Number.isNaN(start) || Number.isNaN(end) || start > end || start >= size) return null;
37 + return { start, end: Math.min(end, size - 1) };
38 +}
39 +
40 +/** RFC5987 filename* encoding for Content-Disposition. */
41 +function contentDisposition(kind, filename) {
42 + const ascii = filename.replace(/[^\x20-\x7e]/g, '_').replace(/"/g, "'");
43 + return `${kind}; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
44 +}
45 +
46 +/**
47 + * Stream a blob with full Range support (instant video seeking).
48 + * @param {{download?: boolean, mime?: string, filename?: string}} opts
49 + * download=true forces attachment; inline is only allowed for safe types
50 + * (uploaded HTML/SVG never executes same-origin — security checklist).
51 + */
52 +export async function sendBlob(req, reply, { sha, mime, filename, download = false }) {
53 + const size = (await stat(blobPath(sha))).size;
54 + const type = mime || 'application/octet-stream';
55 + const inline = !download && INLINE_SAFE.test(type);
56 + const servedType = inline ? type : type === 'text/html' || type === 'image/svg+xml'
57 + ? type // still declared, but forced to attachment below
58 + : type;
59 +
60 + reply.header('Accept-Ranges', 'bytes');
61 + reply.header('X-Content-Type-Options', 'nosniff');
62 + reply.header('Content-Disposition', contentDisposition(inline ? 'inline' : 'attachment', filename));
63 + reply.header('Cache-Control', 'private, max-age=3600');
64 + reply.type(servedType);
65 +
66 + const range = parseRange(req.headers.range, size);
67 + if (range) {
68 + reply.code(206);
69 + reply.header('Content-Range', `bytes ${range.start}-${range.end}/${size}`);
70 + reply.header('Content-Length', range.end - range.start + 1);
71 + return reply.send(blobStream(sha, range));
72 + }
73 + reply.header('Content-Length', size);
74 + return reply.send(blobStream(sha));
75 +}
76 +
77 +/**
78 + * Serve an SVG blob inline but fully sandboxed (CSP sandbox, no scripts run).
79 + */
80 +export async function sendSandboxedSvg(req, reply, { sha, filename }) {
81 + const size = (await stat(blobPath(sha))).size;
82 + reply.header('Content-Security-Policy', "sandbox; default-src 'none'; style-src 'unsafe-inline'");
83 + reply.header('X-Content-Type-Options', 'nosniff');
84 + reply.header('Content-Disposition', contentDisposition('inline', filename));
85 + reply.header('Cache-Control', 'private, max-age=3600');
86 + reply.header('Content-Length', size);
87 + reply.type('image/svg+xml');
88 + return reply.send(blobStream(sha));
89 +}
90 +
91 +/**
92 + * Fixed-window in-memory rate limiter.
93 + * @returns {(req, reply) => boolean} true when the request may proceed
94 + */
95 +export function makeRateLimiter({ windowMs = 60_000, max = 100 } = {}) {
96 + const hits = new Map();
97 + setInterval(() => hits.clear(), windowMs).unref();
98 + return (req, reply) => {
99 + const key = req.ip;
100 + const count = (hits.get(key) ?? 0) + 1;
101 + hits.set(key, count);
102 + if (count > max) {
103 + reply.code(429).send({ error: { code: 'rate_limited', message: 'Too many requests' } });
104 + return false;
105 + }
106 + return true;
107 + };
108 +}
109 +
110 +/** Standard API error payload. */
111 +export function apiError(reply, statusCode, code, message) {
112 + return reply.code(statusCode).send({ error: { code, message } });
113 +}
added src/web/routes.mjs +432 −0
@@ -0,0 +1,432 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Drive — Personal Cloud Drive
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : src/web/routes.mjs
8 + * Purpose : Web UI routes — login, app shell, media, public share pages
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
14 +import path from 'node:path';
15 +import { fileURLToPath } from 'node:url';
16 +import nunjucks from 'nunjucks';
17 +import archiver from 'archiver';
18 +import {
19 + checkLockout, clearLoginFailures, getKeys, recordLoginFailure, verifyPassword,
20 +} from '../auth/password.mjs';
21 +import { SESSION_COOKIE, createSession, destroySession } from '../auth/session.mjs';
22 +import { config } from '../config.mjs';
23 +import { blobPath } from '../storage/blobs.mjs';
24 +import { childrenOf, getNode, isDescendant, listDescendantFiles, pathOf } from '../storage/nodes.mjs';
25 +import {
26 + getShareByToken, recordShareEvent, shareValidity, verifySharePassword,
27 +} from '../shares/shares.mjs';
28 +import { previewStrategy, thumbKind, iconFamily } from '../preview/router.mjs';
29 +import {
30 + describePreview, handleArchive, handleArchiveMember, handleExif, handleFileSend,
31 + handleHeic, handleMarkdown, handleOfficePdf, handlePeaks, handleRawText,
32 + handleText, handleThumb, handleVideoFile, handleVideoStatus,
33 +} from '../preview/handlers.mjs';
34 +import { makeRateLimiter } from './http-helpers.mjs';
35 +
36 +const HERE = path.dirname(fileURLToPath(import.meta.url));
37 +const CSRF_COOKIE = 'spbdrive_csrf';
38 +
39 +const env = nunjucks.configure(path.join(HERE, 'views'), { autoescape: true, noCache: false });
40 +env.addFilter('filesize', (bytes) => {
41 + const n = Number(bytes ?? 0);
42 + if (n < 1024) return `${n} B`;
43 + const units = ['KB', 'MB', 'GB', 'TB'];
44 + let v = n / 1024;
45 + let i = 0;
46 + while (v >= 1024 && i < units.length - 1) { v /= 1024; i += 1; }
47 + return `${v.toFixed(v >= 100 ? 0 : 1)} ${units[i]}`;
48 +});
49 +
50 +const render = (template, ctx = {}) =>
51 + env.render(template, { publicUrl: config.publicUrl, ...ctx });
52 +
53 +/** Strict CSP for app + share pages (self-hosted everything). */
54 +const PAGE_CSP = [
55 + "default-src 'self'",
56 + "script-src 'self'",
57 + "style-src 'self' 'unsafe-inline'", // shiki + dynamic UI styles
58 + "img-src 'self' data: blob:",
59 + "media-src 'self' blob:",
60 + "font-src 'self'",
61 + "connect-src 'self'",
62 + "worker-src 'self' blob:",
63 + "frame-src 'self'", // pdf viewer iframe
64 + "object-src 'none'",
65 + "base-uri 'none'",
66 + "form-action 'self'",
67 +].join('; ');
68 +
69 +function pageHeaders(reply) {
70 + reply.header('Content-Security-Policy', PAGE_CSP);
71 + reply.header('X-Content-Type-Options', 'nosniff');
72 + reply.header('Referrer-Policy', 'no-referrer');
73 + reply.header('X-Frame-Options', 'SAMEORIGIN');
74 + reply.type('text/html; charset=utf-8');
75 +}
76 +
77 +function csrfToken(req, reply) {
78 + let token = req.cookies?.[CSRF_COOKIE];
79 + if (!token || !/^[a-f0-9]{32}$/.test(token)) {
80 + token = randomBytes(16).toString('hex');
81 + reply.setCookie(CSRF_COOKIE, token, { path: '/', secure: true, sameSite: 'lax' });
82 + }
83 + return token;
84 +}
85 +
86 +function checkCsrf(req) {
87 + const cookie = req.cookies?.[CSRF_COOKIE];
88 + const field = req.body?._csrf;
89 + return Boolean(cookie && field && cookie === field);
90 +}
91 +
92 +// Signed cookie proving a visitor passed a share's password gate.
93 +function shareGateValue(share) {
94 + return createHmac('sha256', getKeys().shareKey)
95 + .update(`${share.token}:${share.password_hash ?? ''}`)
96 + .digest('hex');
97 +}
98 +
99 +function shareGatePassed(req, share) {
100 + const cookie = req.cookies?.[`spbshare_${share.id}`];
101 + if (!cookie) return false;
102 + const expected = shareGateValue(share);
103 + const a = Buffer.from(cookie);
104 + const b = Buffer.from(expected);
105 + return a.length === b.length && timingSafeEqual(a, b);
106 +}
107 +
108 +/** Register web + media + share routes. */
109 +export function registerWebRoutes(app) {
110 + const shareLimiter = makeRateLimiter({ windowMs: 60_000, max: 100 });
111 +
112 + // ── Login / logout ──────────────────────────────────────────────────
113 + app.get('/login', async (req, reply) => {
114 + if (req.authed) return reply.redirect(req.query.next ?? '/app');
115 + pageHeaders(reply);
116 + return reply.send(render('login.njk', { csrf: csrfToken(req, reply), error: null, next: req.query.next ?? '/app' }));
117 + });
118 +
119 + app.post('/login', async (req, reply) => {
120 + pageHeaders(reply);
121 + const fail = (error, code = 401) => reply.code(code).send(
122 + render('login.njk', { csrf: csrfToken(req, reply), error, next: req.body?.next ?? '/app' }),
123 + );
124 + if (!checkCsrf(req)) return fail('Session expired — try again', 403);
125 + const lock = checkLockout(req.ip);
126 + if (lock.locked) {
127 + return fail(`Too many attempts. Locked for ${Math.ceil((lock.retryAfterMs ?? 0) / 60000) || 1} min.`, 429);
128 + }
129 + if (!(await verifyPassword(req.body?.password))) {
130 + recordLoginFailure(req.ip);
131 + return fail('Wrong password');
132 + }
133 + clearLoginFailures(req.ip);
134 + const session = createSession({
135 + remember: req.body?.remember === 'on',
136 + ip: req.ip,
137 + ua: req.headers['user-agent'] ?? '',
138 + });
139 + reply.setCookie(SESSION_COOKIE, session.token, {
140 + path: '/', httpOnly: true, secure: true, sameSite: 'lax', maxAge: session.maxAgeSec,
141 + });
142 + const next = String(req.body?.next ?? '/app');
143 + return reply.redirect(next.startsWith('/') && !next.startsWith('//') ? next : '/app');
144 + });
145 +
146 + app.get('/logout', async (req, reply) => {
147 + destroySession(req.cookies?.[SESSION_COOKIE]);
148 + reply.clearCookie(SESSION_COOKIE, { path: '/' });
149 + return reply.redirect('/login');
150 + });
151 +
152 + // ── App shell ───────────────────────────────────────────────────────
153 + const requireSession = (req, reply) => {
154 + if (!req.authed) {
155 + reply.redirect(`/login?next=${encodeURIComponent(req.url)}`);
156 + return false;
157 + }
158 + return true;
159 + };
160 +
161 + app.get('/', async (req, reply) => reply.redirect(req.authed ? '/app' : '/login'));
162 +
163 + app.get('/app', async (req, reply) => {
164 + if (!requireSession(req, reply)) return undefined;
165 + pageHeaders(reply);
166 + return reply.send(render('app.njk', {}));
167 + });
168 + app.get('/app/*', async (req, reply) => {
169 + if (!requireSession(req, reply)) return undefined;
170 + pageHeaders(reply);
171 + return reply.send(render('app.njk', {}));
172 + });
173 +
174 + // ── Authenticated media: /dl /stream /thumb ─────────────────────────
175 + const mediaNode = (req, reply) => {
176 + if (!req.authed) {
177 + reply.code(401).send({ error: { code: 'unauthorized', message: 'Login required' } });
178 + return null;
179 + }
180 + const node = getNode(Number(req.params.id));
181 + if (!node || node.type !== 'file' || !node.blob_sha) {
182 + reply.code(404).send({ error: { code: 'not_found', message: 'No such file' } });
183 + return null;
184 + }
185 + return node;
186 + };
187 +
188 + app.get('/dl/:id', async (req, reply) => {
189 + const node = mediaNode(req, reply);
190 + if (!node) return undefined;
191 + return handleFileSend(node, req, reply, { download: true });
192 + });
193 +
194 + app.get('/stream/:id', async (req, reply) => {
195 + const node = mediaNode(req, reply);
196 + if (!node) return undefined;
197 + return handleFileSend(node, req, reply, { download: false });
198 + });
199 +
200 + app.get('/thumb/:id', async (req, reply) => {
201 + const node = mediaNode(req, reply);
202 + if (!node) return undefined;
203 + const kind = thumbKind(node);
204 + if (!kind) return reply.code(404).send({ error: { code: 'no_thumb', message: 'No thumbnail' } });
205 + return handleThumb(node, req, reply, kind);
206 + });
207 +
208 + // ── Public share pages ──────────────────────────────────────────────
209 + const expiredPage = (reply, reason) => {
210 + pageHeaders(reply);
211 + // No filename in title — expired links leak nothing.
212 + return reply.code(reason === 'missing' ? 404 : 410).send(render('share-expired.njk', {}));
213 + };
214 +
215 + /**
216 + * Resolve + validate a share request. Returns null after replying when
217 + * anything is off (unknown token, expired, gated, rate-limited).
218 + */
219 + const shareCtx = async (req, reply, { wantPage = false } = {}) => {
220 + if (!shareLimiter(req, reply)) return null;
221 + const share = getShareByToken(String(req.params.token ?? ''));
222 + const validity = shareValidity(share);
223 + if (!validity.ok) {
224 + expiredPage(reply, validity.reason);
225 + return null;
226 + }
227 + const node = getNode(share.node_id);
228 + if (!node || node.trashed_at) {
229 + expiredPage(reply, 'missing');
230 + return null;
231 + }
232 + if (share.password_hash && !shareGatePassed(req, share)) {
233 + if (wantPage) {
234 + pageHeaders(reply);
235 + reply.send(render('share-gate.njk', { token: share.token, csrf: csrfToken(req, reply), error: null }));
236 + } else {
237 + reply.code(403).send({ error: { code: 'password_required', message: 'Password required' } });
238 + }
239 + return null;
240 + }
241 + return { share, node };
242 + };
243 +
244 + app.post('/s/:token/unlock', async (req, reply) => {
245 + if (!shareLimiter(req, reply)) return undefined;
246 + const share = getShareByToken(String(req.params.token ?? ''));
247 + const validity = shareValidity(share);
248 + if (!validity.ok) return expiredPage(reply, validity.reason);
249 + pageHeaders(reply);
250 + if (!checkCsrf(req)) {
251 + return reply.code(403).send(render('share-gate.njk', { token: share.token, csrf: csrfToken(req, reply), error: 'Try again' }));
252 + }
253 + if (!(await verifySharePassword(share, req.body?.password))) {
254 + return reply.code(401).send(render('share-gate.njk', { token: share.token, csrf: csrfToken(req, reply), error: 'Wrong password' }));
255 + }
256 + reply.setCookie(`spbshare_${share.id}`, shareGateValue(share), {
257 + path: `/s/${share.token}`, httpOnly: true, secure: true, sameSite: 'lax', maxAge: 86_400,
258 + });
259 + return reply.redirect(`/s/${share.token}`);
260 + });
261 +
262 + app.get('/s/:token', async (req, reply) => {
263 + const ctx = await shareCtx(req, reply, { wantPage: true });
264 + if (!ctx) return undefined;
265 + const { share, node } = ctx;
266 + recordShareEvent(share.id, 'visit', { ip: req.ip, ua: req.headers['user-agent'] ?? '' });
267 + pageHeaders(reply);
268 + const og = {
269 + title: node.name,
270 + url: `${config.publicUrl}/s/${share.token}`,
271 + image: node.type === 'file' && thumbKind(node)
272 + ? `${config.publicUrl}/s/${share.token}/thumb?size=512`
273 + : null,
274 + };
275 + if (node.type === 'folder') {
276 + return reply.send(render('share-folder.njk', { share, node, og, allowDownload: Boolean(share.allow_download) }));
277 + }
278 + return reply.send(render('share-file.njk', {
279 + share, node, og,
280 + strategy: previewStrategy(node).strategy,
281 + icon: iconFamily(node),
282 + allowDownload: Boolean(share.allow_download),
283 + }));
284 + });
285 +
286 + // Child resolution for folder shares — must stay inside the shared subtree.
287 + const shareChild = (ctx, childId, reply) => {
288 + if (childId === undefined || Number(childId) === ctx.node.id) return ctx.node;
289 + const child = getNode(Number(childId));
290 + if (!child || child.trashed_at || !isDescendant(child.id, ctx.node.id)) {
291 + reply.code(404).send({ error: { code: 'not_found', message: 'Not found' } });
292 + return null;
293 + }
294 + return child;
295 + };
296 +
297 + app.get('/s/:token/api/children', async (req, reply) => {
298 + const ctx = await shareCtx(req, reply);
299 + if (!ctx) return undefined;
300 + const folder = shareChild(ctx, req.query.id, reply);
301 + if (!folder) return undefined;
302 + if (folder.type !== 'folder') return reply.code(400).send({ error: { code: 'not_folder', message: 'Not a folder' } });
303 + const children = childrenOf(folder.id).map((n) => ({
304 + id: n.id, name: n.name, type: n.type, size: n.size, mime: n.mime, modified: n.modified,
305 + icon: iconFamily(n),
306 + hasThumb: n.type === 'file' && thumbKind(n) !== null,
307 + strategy: n.type === 'file' ? previewStrategy(n).strategy : null,
308 + }));
309 + const crumb = pathOf(folder.id);
310 + const rootIdx = crumb.findIndex((n) => n.id === ctx.node.id);
311 + return {
312 + children,
313 + path: crumb.slice(rootIdx).map((n) => ({ id: n.id, name: n.name || ctx.node.name })),
314 + allowDownload: Boolean(ctx.share.allow_download),
315 + };
316 + });
317 +
318 + const shareFile = async (req, reply, opts) => {
319 + const ctx = await shareCtx(req, reply);
320 + if (!ctx) return null;
321 + const node = shareChild(ctx, req.params.childId, reply);
322 + if (!node) return null;
323 + if (node.type !== 'file') {
324 + reply.code(400).send({ error: { code: 'not_file', message: 'Not a file' } });
325 + return null;
326 + }
327 + if (opts?.download && !ctx.share.allow_download) {
328 + reply.code(403).send({ error: { code: 'no_download', message: 'Download disabled for this share' } });
329 + return null;
330 + }
331 + return { ctx, node };
332 + };
333 +
334 + for (const [route, download] of [['/s/:token/dl', true], ['/s/:token/dl/:childId', true],
335 + ['/s/:token/stream', false], ['/s/:token/stream/:childId', false]]) {
336 + app.get(route, async (req, reply) => {
337 + const res = await shareFile(req, reply, { download });
338 + if (!res) return undefined;
339 + if (download) {
340 + recordShareEvent(res.ctx.share.id, 'download', { ip: req.ip, ua: req.headers['user-agent'] ?? '' });
341 + }
342 + return handleFileSend(res.node, req, reply, { download });
343 + });
344 + }
345 +
346 + for (const route of ['/s/:token/thumb', '/s/:token/thumb/:childId']) {
347 + app.get(route, async (req, reply) => {
348 + const res = await shareFile(req, reply);
349 + if (!res) return undefined;
350 + const kind = thumbKind(res.node);
351 + if (!kind) return reply.code(404).send({ error: { code: 'no_thumb', message: 'No thumbnail' } });
352 + return handleThumb(res.node, req, reply, kind);
353 + });
354 + }
355 +
356 + app.get('/s/:token/zip', async (req, reply) => {
357 + const ctx = await shareCtx(req, reply);
358 + if (!ctx) return undefined;
359 + if (!ctx.share.allow_download) {
360 + return reply.code(403).send({ error: { code: 'no_download', message: 'Download disabled' } });
361 + }
362 + if (ctx.node.type !== 'folder') return reply.code(400).send({ error: { code: 'not_folder', message: 'Not a folder' } });
363 + recordShareEvent(ctx.share.id, 'download', { ip: req.ip, ua: req.headers['user-agent'] ?? '' });
364 + reply.header('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(`${ctx.node.name || 'folder'}.zip`)}`);
365 + reply.type('application/zip');
366 + const archive = archiver('zip', { zlib: { level: 6 } });
367 + for (const { node: file, relPath } of listDescendantFiles(ctx.node.id)) {
368 + archive.file(blobPath(file.blob_sha), { name: relPath });
369 + }
370 + archive.finalize();
371 + return reply.send(archive);
372 + });
373 +
374 + // Share preview endpoints — same handlers as the app API.
375 + app.get('/s/:token/preview', async (req, reply) => {
376 + const res = await shareFile(req, reply);
377 + if (!res) return undefined;
378 + return describePreview(res.node, `/s/${res.ctx.share.token}/preview`);
379 + });
380 + app.get('/s/:token/preview/:childId', async (req, reply) => {
381 + const res = await shareFile(req, reply);
382 + if (!res) return undefined;
383 + return describePreview(res.node, `/s/${res.ctx.share.token}/preview/${res.node.id}`);
384 + });
385 +
386 + const sharePreviewRoutes = {
387 + '/text': handleText,
388 + '/markdown': handleMarkdown,
389 + '/raw': handleRawText,
390 + '/archive': handleArchive,
391 + '/archive/member': handleArchiveMember,
392 + '/pdf': handleOfficePdf,
393 + '/video': handleVideoStatus,
394 + '/video/file': handleVideoFile,
395 + '/peaks': handlePeaks,
396 + '/exif': handleExif,
397 + '/heic': handleHeic,
398 + };
399 + for (const [suffix, handler] of Object.entries(sharePreviewRoutes)) {
400 + for (const base of ['/s/:token/preview', '/s/:token/preview/:childId']) {
401 + app.get(`${base}${suffix}`, async (req, reply) => {
402 + const res = await shareFile(req, reply);
403 + if (!res) return undefined;
404 + return handler(res.node, req, reply);
405 + });
406 + }
407 + }
408 +
409 + // ── Error pages ─────────────────────────────────────────────────────
410 + app.setNotFoundHandler((req, reply) => {
411 + if (req.url.startsWith('/api/') || req.headers.accept?.includes('application/json')) {
412 + return reply.code(404).send({ error: { code: 'not_found', message: 'Not found' } });
413 + }
414 + pageHeaders(reply);
415 + return reply.code(404).send(render('error.njk', { code: 404, message: 'This page does not exist.' }));
416 + });
417 +
418 + app.setErrorHandler((err, req, reply) => {
419 + const status = err.statusCode && err.statusCode >= 400 ? err.statusCode : 500;
420 + if (status >= 500) req.log.error({ err }, 'request failed');
421 + if (req.url.startsWith('/api/') || req.headers.accept?.includes('application/json')) {
422 + return reply.code(status).send({
423 + error: { code: err.code ?? 'internal', message: status >= 500 ? 'Internal error' : err.message },
424 + });
425 + }
426 + pageHeaders(reply);
427 + return reply.code(status).send(render('error.njk', {
428 + code: status,
429 + message: status >= 500 ? 'Something broke on our side.' : err.message,
430 + }));
431 + });
432 +}
433