commit b2ffbe865ecb6ccfcf849a09a9391cf5786b4b44 Author: Pen Anderson Date: Tue May 5 09:33:51 2026 -0500 Prototype commit diff --git a/.forgejo/workflows/check.yml b/.forgejo/workflows/check.yml new file mode 100644 index 0000000..c55a37a --- /dev/null +++ b/.forgejo/workflows/check.yml @@ -0,0 +1,57 @@ +name: check + +on: + push: + branches: ['**'] + tags: ['v*'] + workflow_dispatch: + +jobs: + frontend: + runs-on: nix:host + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + + - name: Install npm dependencies + run: nix develop --command bash -c "cd web && npm ci" + + - name: svelte-check + run: nix develop --command bash -c "cd web && npm run check" + + - name: Production build + run: nix develop --command bash -c "cd web && npm run build" + + - name: Confirm SW + manifest emitted + run: | + test -f web/build/index.html + test -f web/build/service-worker.js + test -f web/build/manifest.webmanifest + test -f web/build/robots.txt + + server: + runs-on: nix:host + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + + - name: go vet + run: nix develop --command bash -c "cd server && go vet ./..." + + - name: go build + run: nix develop --command bash -c "cd server && go build ./..." + + - name: Confirm strict mode refuses to start without secrets + run: | + set +e + nix develop --command bash -c "cd server && DOCENT_PASSWORD=t go run . -strict -addr :0" 2>&1 | tee /tmp/strict.log + status=${PIPESTATUS[0]} + if [ "$status" -eq 0 ]; then + echo "expected -strict to refuse to start without DOCENT_COOKIE_SECRET" >&2 + exit 1 + fi + grep -q "DOCENT_QR_KEY must be set in -strict mode\|DOCENT_COOKIE_SECRET must be set in -strict mode" /tmp/strict.log diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c000ddc --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Node +node_modules/ +.svelte-kit/ +build/ +dist/ +.vite/ + +# Go +server/docent +server/docent.exe + +# Local environment +.env +.env.local +.env.*.local + +# Editor +.vscode/ +.idea/ +*.swp +*~ + +# OS +.DS_Store +Thumbs.db + +# Direnv +.direnv/ + +# Build artifacts from content pipeline +content/build/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6ce7e03 --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +SHELL := /usr/bin/env bash + +.PHONY: help +help: + @echo "make build — build server + frontend locally (sanity check)" + @echo "make check — type-check + go vet" + @echo "make tag V=patch|minor|major — bump version tag and push" + @echo "make prefetch — print sha256 + npmDepsHash + vendorHash for the homelab module" + +.PHONY: build +build: + cd web && npm install && npm run build + cd server && go build -o docent ./... + +.PHONY: check +check: + cd web && npm run check + cd server && go vet ./... + +.PHONY: tag +tag: + @if [ -z "$(V)" ]; then echo "usage: make tag V=patch|minor|major" >&2; exit 1; fi + @latest=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.0"); \ + IFS=. read -r maj min pat <<< "$$latest"; \ + case "$(V)" in \ + major) maj=$$((maj+1)); min=0; pat=0 ;; \ + minor) min=$$((min+1)); pat=0 ;; \ + patch) pat=$$((pat+1)) ;; \ + *) echo "V must be patch|minor|major" >&2; exit 1 ;; \ + esac; \ + new="v$${maj}.$${min}.$${pat}"; \ + echo "tagging $$new"; \ + git tag -a "$$new" -m "$$new"; \ + git push --tags + +.PHONY: prefetch +prefetch: + @latest=$$(git describe --tags --abbrev=0); \ + url=$$(git remote get-url origin); \ + echo "rev = $$latest"; \ + echo "src hash:"; \ + nix-prefetch-git "$$url" --rev "$$latest" --quiet | sed -n 's/.*"sha256": "\(.*\)".*/ \1/p'; \ + echo ""; \ + echo "Run \`nix build .#docent-frontend\` and \`nix build .#docent-server\` once to populate"; \ + echo "npmDepsHash and vendorHash respectively (will fail on mismatch and print correct hash)." diff --git a/content/stops.yaml b/content/stops.yaml new file mode 100644 index 0000000..136e373 --- /dev/null +++ b/content/stops.yaml @@ -0,0 +1,30 @@ +# Source of truth for exhibit content. +# A build step generates web/src/lib/stops.json from this file. +# +# Each stop has: +# id — integer, used in URLs and as the asset basename +# title — short label, shown in the grid and on the detail page +# audio — filename in content/audio/, transcoded to web/static/audio/ +# image — filename in content/images/, transcoded to web/static/images/ +# caption — short line shown beneath the title +# description — longer text shown below the audio player (markdown allowed) + +exhibit: + title: "TBD" + subtitle: "TBD" + +stops: + - id: 1 + title: "Welcome" + audio: 01-welcome.wav + image: 01-welcome.png + caption: "An introduction to the exhibit." + description: | + Placeholder for the full description shown on the stop detail page. + + - id: 2 + title: "Stop two placeholder" + audio: 02.wav + image: 02.png + caption: "" + description: "" diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..dedd329 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1777578337, + "narHash": "sha256-Ad49moKWeXtKBJNy2ebiTQUEgdLyvGmTeykAQ9xM+Z4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "15f4ee454b1dce334612fa6843b3e05cf546efab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..14060c1 --- /dev/null +++ b/flake.nix @@ -0,0 +1,28 @@ +{ + description = "Docent — museum audio guide PWA"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + nodejs_22 + go + ffmpeg + imagemagick + yq-go + ]; + }; + } + ); +} diff --git a/server/auth.go b/server/auth.go new file mode 100644 index 0000000..6243483 --- /dev/null +++ b/server/auth.go @@ -0,0 +1,97 @@ +package main + +import ( + "crypto/hmac" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "encoding/json" + "net/http" + "strings" + "time" +) + +const ( + cookieName = "docent_session" + cookieMaxAge = 30 * 24 * time.Hour +) + +type session struct { + Exp int64 `json:"exp"` +} + +func (s *server) signCookieValue() (string, error) { + payload := session{Exp: time.Now().Add(cookieMaxAge).Unix()} + data, err := json.Marshal(payload) + if err != nil { + return "", err + } + enc := base64.RawURLEncoding.EncodeToString(data) + mac := hmac.New(sha256.New, s.secret) + mac.Write([]byte(enc)) + sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) + return enc + "." + sig, nil +} + +func (s *server) verifyCookieValue(value string) bool { + parts := strings.SplitN(value, ".", 2) + if len(parts) != 2 { + return false + } + mac := hmac.New(sha256.New, s.secret) + mac.Write([]byte(parts[0])) + expected := base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) + if !hmac.Equal([]byte(expected), []byte(parts[1])) { + return false + } + data, err := base64.RawURLEncoding.DecodeString(parts[0]) + if err != nil { + return false + } + var sess session + if err := json.Unmarshal(data, &sess); err != nil { + return false + } + return time.Now().Unix() < sess.Exp +} + +func (s *server) setSessionCookie(w http.ResponseWriter) { + value, err := s.signCookieValue() + if err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + http.SetCookie(w, &http.Cookie{ + Name: cookieName, + Value: value, + Path: "/", + MaxAge: int(cookieMaxAge.Seconds()), + HttpOnly: true, + Secure: s.secure, + SameSite: http.SameSiteLaxMode, + }) +} + +func (s *server) clearSessionCookie(w http.ResponseWriter) { + http.SetCookie(w, &http.Cookie{ + Name: cookieName, + Value: "", + Path: "/", + MaxAge: -1, + HttpOnly: true, + Secure: s.secure, + SameSite: http.SameSiteLaxMode, + }) +} + +func (s *server) isAuthed(r *http.Request) bool { + c, err := r.Cookie(cookieName) + if err != nil { + return false + } + return s.verifyCookieValue(c.Value) +} + +func constantTimeEqualString(a, b string) bool { + return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 +} diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..f5d6223 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,3 @@ +module pentacle.games/docent + +go 1.23 diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..d12c9e1 --- /dev/null +++ b/server/main.go @@ -0,0 +1,213 @@ +package main + +import ( + "crypto/rand" + "encoding/base64" + "encoding/json" + "flag" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "time" +) + +type server struct { + staticDir string + password string + qrKey string + secret []byte + secure bool + loginLimiter *rateLimiter +} + +func main() { + addr := flag.String("addr", ":8181", "listen address") + staticDir := flag.String("static", "../web/build", "static asset directory") + insecure := flag.Bool("insecure", false, "drop Secure flag from cookie (dev only)") + strict := flag.Bool("strict", false, "production mode: refuse to start without DOCENT_COOKIE_SECRET and DOCENT_QR_KEY") + flag.Parse() + + password := os.Getenv("DOCENT_PASSWORD") + if password == "" { + log.Fatal("DOCENT_PASSWORD must be set") + } + qrKey := os.Getenv("DOCENT_QR_KEY") + if qrKey == "" { + if *strict { + log.Fatal("DOCENT_QR_KEY must be set in -strict mode") + } + log.Println("warning: DOCENT_QR_KEY not set; QR-code auto-login disabled") + } + + cookieSecret := os.Getenv("DOCENT_COOKIE_SECRET") + if *strict && cookieSecret == "" { + log.Fatal("DOCENT_COOKIE_SECRET must be set in -strict mode") + } + secret, err := loadOrGenSecret(cookieSecret) + if err != nil { + log.Fatalf("DOCENT_COOKIE_SECRET: %v", err) + } + + abs, err := filepath.Abs(*staticDir) + if err != nil { + log.Fatalf("static dir: %v", err) + } + if _, err := os.Stat(abs); err != nil { + log.Printf("warning: static dir %s not found yet (build the SvelteKit app first)", abs) + } + + s := &server{ + staticDir: abs, + password: password, + qrKey: qrKey, + secret: secret, + secure: !*insecure, + loginLimiter: newRateLimiter(8, time.Minute), + } + + mux := http.NewServeMux() + mux.HandleFunc("POST /api/login", s.handleLogin) + mux.HandleFunc("POST /api/key", s.handleKey) + mux.HandleFunc("POST /api/logout", s.handleLogout) + mux.HandleFunc("GET /api/me", s.handleMe) + mux.Handle("GET /audio/", s.requireAuth(http.HandlerFunc(s.serveStatic))) + mux.Handle("GET /images/", s.requireAuth(http.HandlerFunc(s.serveStatic))) + mux.HandleFunc("GET /", s.serveSPA) + + log.Printf("docent listening on %s, serving %s", *addr, abs) + if err := http.ListenAndServe(*addr, mux); err != nil { + log.Fatal(err) + } +} + +func loadOrGenSecret(env string) ([]byte, error) { + if env != "" { + b, err := base64.StdEncoding.DecodeString(env) + if err != nil { + return nil, err + } + if len(b) < 32 { + return nil, errShortSecret + } + return b, nil + } + log.Println("warning: DOCENT_COOKIE_SECRET not set, generating ephemeral secret (sessions reset on restart)") + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return nil, err + } + return b, nil +} + +var errShortSecret = &cookieError{"DOCENT_COOKIE_SECRET must decode to at least 32 bytes"} + +type cookieError struct{ msg string } + +func (e *cookieError) Error() string { return e.msg } + +func (s *server) handleLogin(w http.ResponseWriter, r *http.Request) { + if !s.loginLimiter.allow(clientIP(r)) { + w.Header().Set("Retry-After", "60") + http.Error(w, "too many attempts", http.StatusTooManyRequests) + return + } + var body struct { + Password string `json:"password"` + } + err := json.NewDecoder(r.Body).Decode(&body) + if err != nil || !constantTimeEqualString(body.Password, s.password) { + time.Sleep(250 * time.Millisecond) + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + s.setSessionCookie(w) + w.WriteHeader(http.StatusNoContent) +} + +func (s *server) handleKey(w http.ResponseWriter, r *http.Request) { + if !s.loginLimiter.allow(clientIP(r)) { + w.Header().Set("Retry-After", "60") + http.Error(w, "too many attempts", http.StatusTooManyRequests) + return + } + var body struct { + Key string `json:"key"` + } + err := json.NewDecoder(r.Body).Decode(&body) + // An empty qrKey means the QR auto-login is disabled — never accept any value. + if err != nil || s.qrKey == "" || !constantTimeEqualString(body.Key, s.qrKey) { + time.Sleep(250 * time.Millisecond) + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + s.setSessionCookie(w) + w.WriteHeader(http.StatusNoContent) +} + +func (s *server) handleLogout(w http.ResponseWriter, r *http.Request) { + s.clearSessionCookie(w) + w.WriteHeader(http.StatusNoContent) +} + +func (s *server) handleMe(w http.ResponseWriter, r *http.Request) { + if !s.isAuthed(r) { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"ok":true}`)) +} + +func (s *server) requireAuth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !s.isAuthed(r) { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r) + }) +} + +// serveStatic serves files relative to staticDir, refusing path traversal. +func (s *server) serveStatic(w http.ResponseWriter, r *http.Request) { + clean := filepath.Clean(r.URL.Path) + if strings.Contains(clean, "..") { + http.Error(w, "not found", http.StatusNotFound) + return + } + full := filepath.Join(s.staticDir, clean) + if !strings.HasPrefix(full, s.staticDir) { + http.Error(w, "not found", http.StatusNotFound) + return + } + info, err := os.Stat(full) + if err != nil || info.IsDir() { + http.Error(w, "not found", http.StatusNotFound) + return + } + http.ServeFile(w, r, full) +} + +// serveSPA serves files from staticDir, falling back to index.html for client routes. +func (s *server) serveSPA(w http.ResponseWriter, r *http.Request) { + clean := filepath.Clean(r.URL.Path) + if clean == "/" { + clean = "/index.html" + } + full := filepath.Join(s.staticDir, clean) + if !strings.HasPrefix(full, s.staticDir) { + http.Error(w, "not found", http.StatusNotFound) + return + } + + if info, err := os.Stat(full); err == nil && !info.IsDir() { + http.ServeFile(w, r, full) + return + } + + // Unknown path → SPA fallback. + w.Header().Set("Cache-Control", "no-cache") + http.ServeFile(w, r, filepath.Join(s.staticDir, "index.html")) +} diff --git a/server/ratelimit.go b/server/ratelimit.go new file mode 100644 index 0000000..fd46d4e --- /dev/null +++ b/server/ratelimit.go @@ -0,0 +1,80 @@ +package main + +import ( + "net" + "net/http" + "strings" + "sync" + "time" +) + +type rateLimiter struct { + mu sync.Mutex + visitors map[string]*visitor + rate int + window time.Duration +} + +type visitor struct { + count int + reset time.Time +} + +func newRateLimiter(rate int, window time.Duration) *rateLimiter { + rl := &rateLimiter{ + visitors: make(map[string]*visitor), + rate: rate, + window: window, + } + go rl.cleanup() + return rl +} + +func (rl *rateLimiter) allow(key string) bool { + rl.mu.Lock() + defer rl.mu.Unlock() + now := time.Now() + v, ok := rl.visitors[key] + if !ok || now.After(v.reset) { + rl.visitors[key] = &visitor{count: 1, reset: now.Add(rl.window)} + return true + } + if v.count >= rl.rate { + return false + } + v.count++ + return true +} + +func (rl *rateLimiter) cleanup() { + for { + time.Sleep(5 * time.Minute) + rl.mu.Lock() + now := time.Now() + for k, v := range rl.visitors { + if now.After(v.reset) { + delete(rl.visitors, k) + } + } + rl.mu.Unlock() + } +} + +// clientIP extracts the client address from X-Forwarded-For (set by nginx), +// falling back to X-Real-IP and finally RemoteAddr. +func clientIP(r *http.Request) string { + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + if comma := strings.Index(xff, ","); comma > 0 { + return strings.TrimSpace(xff[:comma]) + } + return strings.TrimSpace(xff) + } + if rip := r.Header.Get("X-Real-IP"); rip != "" { + return rip + } + host, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return r.RemoteAddr + } + return host +} diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..844eacd --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,1536 @@ +{ + "name": "docent-web", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "docent-web", + "version": "0.0.1", + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.5", + "@sveltejs/kit": "^2.7.0", + "@sveltejs/vite-plugin-svelte": "^4.0.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.5.0", + "vite": "^5.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", + "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.59.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.59.1.tgz", + "integrity": "sha512-d8OON70AphLdDesuTIl//M2O6fRTIicX8aYv8vhCiYEhTTI2OboKqey0Hu1A4VFhqwgqtq0vKDmPFGkw8kKmgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.6.4", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3 || ^6.0.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz", + "integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", + "debug": "^4.3.7", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.12", + "vitefu": "^1.0.3" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0-next.96 || ^5.0.0", + "vite": "^5.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz", + "integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", + "svelte": "^5.0.0-next.96 || ^5.0.0", + "vite": "^5.0.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/devalue": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.0.tgz", + "integrity": "sha512-2zA9pFEsnp7vWBZbXF5JAgAq0fsUIt/1XPbRiAmRV3lp/2C3upzH+sADiyy66aFCihoLEsrQHxNM5w1gIDfsBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.6.tgz", + "integrity": "sha512-WN0clHt0a4mzC780UBVVBpsj4vSSjOFNRd2WjYtduB9HeKxm1sjHMNUwLEHVjI3FdCQD/Hurgz9ftbKEzP79Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svelte": { + "version": "5.55.5", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.5.tgz", + "integrity": "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.6.4", + "esm-env": "^1.2.1", + "esrap": "^2.2.4", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.8.tgz", + "integrity": "sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..ecc0606 --- /dev/null +++ b/web/package.json @@ -0,0 +1,21 @@ +{ + "name": "docent-web", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.5", + "@sveltejs/kit": "^2.7.0", + "@sveltejs/vite-plugin-svelte": "^4.0.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.5.0", + "vite": "^5.4.0" + } +} diff --git a/web/src/app.css b/web/src/app.css new file mode 100644 index 0000000..4c58f6c --- /dev/null +++ b/web/src/app.css @@ -0,0 +1,72 @@ +:root { + --bg: #faf7f2; + --bg-elev: #ffffff; + --ink: #2a2520; + --ink-mute: #6b6157; + --accent: #6e3a1f; + --rule: #e8e0d4; + --radius: 14px; + --radius-lg: 22px; + --shadow: 0 1px 2px rgba(42, 37, 32, 0.06), 0 8px 24px rgba(42, 37, 32, 0.08); + + --font-sans: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + --font-serif: ui-serif, Georgia, 'Times New Roman', serif; + + color-scheme: light; +} + +* { + box-sizing: border-box; +} + +html, body { + margin: 0; + padding: 0; + background: var(--bg); + color: var(--ink); + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: transparent; + overscroll-behavior-y: contain; +} + +body { + min-height: 100dvh; +} + +#app { + min-height: 100dvh; + display: flex; + flex-direction: column; +} + +h1, h2, h3 { + font-family: var(--font-serif); + font-weight: 600; + letter-spacing: -0.01em; + margin: 0; +} + +a { + color: var(--accent); + text-decoration: none; +} + +button { + font: inherit; + color: inherit; + background: none; + border: none; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +input { + font: inherit; + color: inherit; +} + +img { + display: block; + max-width: 100%; +} diff --git a/web/src/app.html b/web/src/app.html new file mode 100644 index 0000000..cdc6961 --- /dev/null +++ b/web/src/app.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + Mill Run + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/web/src/lib/auth.svelte.ts b/web/src/lib/auth.svelte.ts new file mode 100644 index 0000000..b1fc11f --- /dev/null +++ b/web/src/lib/auth.svelte.ts @@ -0,0 +1,72 @@ +import { goto } from '$app/navigation'; +import { browser } from '$app/environment'; + +export type AuthState = 'unknown' | 'signed-in' | 'signed-out'; + +let state: AuthState = $state('unknown'); + +export function authState() { + return state; +} + +export function setAuth(next: AuthState) { + state = next; +} + +export async function checkAuth(): Promise { + if (!browser) return 'unknown'; + try { + const res = await fetch('/api/me', { credentials: 'same-origin' }); + state = res.ok ? 'signed-in' : 'signed-out'; + } catch { + state = 'signed-out'; + } + return state; +} + +export async function login(password: string): Promise { + const res = await fetch('/api/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }), + credentials: 'same-origin' + }); + if (res.ok) { + state = 'signed-in'; + return true; + } + return false; +} + +export async function loginWithKey(key: string): Promise { + const res = await fetch('/api/key', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ key }), + credentials: 'same-origin' + }); + if (res.ok) { + state = 'signed-in'; + return true; + } + return false; +} + +export async function logout() { + await fetch('/api/logout', { method: 'POST', credentials: 'same-origin' }); + state = 'signed-out'; + goto('/login'); +} + +export type KeyResult = 'success' | 'failed' | 'absent'; + +export async function consumeKeyParam(): Promise { + if (!browser) return 'absent'; + const url = new URL(window.location.href); + const key = url.searchParams.get('key'); + if (!key) return 'absent'; + url.searchParams.delete('key'); + window.history.replaceState({}, '', url.toString()); + const ok = await loginWithKey(key); + return ok ? 'success' : 'failed'; +} diff --git a/web/src/lib/precache.svelte.ts b/web/src/lib/precache.svelte.ts new file mode 100644 index 0000000..22c281c --- /dev/null +++ b/web/src/lib/precache.svelte.ts @@ -0,0 +1,51 @@ +import { browser } from '$app/environment'; +import { stops } from './stops'; + +export type PrecacheState = 'idle' | 'running' | 'complete' | 'unsupported'; + +let state: PrecacheState = $state('idle'); +let done = $state(0); +let total = $state(0); + +export function precacheState() { + return { state, done, total }; +} + +export async function precacheAll(): Promise { + if (!browser) return; + if (!('serviceWorker' in navigator)) { + state = 'unsupported'; + return; + } + if (state === 'running' || state === 'complete') return; + + const reg = await navigator.serviceWorker.ready.catch(() => null); + const target = reg?.active ?? navigator.serviceWorker.controller; + if (!target) { + state = 'unsupported'; + return; + } + + const urls = stops.flatMap((s) => [s.audio, s.image]); + + state = 'running'; + done = 0; + total = urls.length; + + const onMessage = (event: MessageEvent) => { + const data = event.data; + if (!data || typeof data !== 'object') return; + if (data.type === 'precache-progress') { + done = data.done; + total = data.total; + } else if (data.type === 'precache-done') { + done = data.done; + total = data.total; + state = 'complete'; + navigator.serviceWorker.removeEventListener('message', onMessage); + } + }; + navigator.serviceWorker.addEventListener('message', onMessage); + + target.postMessage({ type: 'precache-stops', urls }); +} diff --git a/web/src/lib/stops.json b/web/src/lib/stops.json new file mode 100644 index 0000000..f1d313b --- /dev/null +++ b/web/src/lib/stops.json @@ -0,0 +1,24 @@ +{ + "exhibit": { + "title": "Mill Run", + "subtitle": "" + }, + "stops": [ + { + "id": 1, + "title": "Welcome", + "audio": "/audio/01.opus", + "image": "/images/01.webp", + "caption": "An introduction to the exhibit.", + "description": "Placeholder description." + }, + { + "id": 2, + "title": "Stop two placeholder", + "audio": "/audio/02.opus", + "image": "/images/02.webp", + "caption": "", + "description": "" + } + ] +} diff --git a/web/src/lib/stops.ts b/web/src/lib/stops.ts new file mode 100644 index 0000000..99b9914 --- /dev/null +++ b/web/src/lib/stops.ts @@ -0,0 +1,22 @@ +import stopsJson from './stops.json'; + +export type Stop = { + id: number; + title: string; + audio: string; + image: string; + caption?: string; + description?: string; +}; + +export type Exhibit = { + title: string; + subtitle: string; +}; + +export const exhibit: Exhibit = stopsJson.exhibit; +export const stops: Stop[] = stopsJson.stops; + +export function stopById(id: number): Stop | undefined { + return stops.find((s) => s.id === id); +} diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte new file mode 100644 index 0000000..14b740b --- /dev/null +++ b/web/src/routes/+layout.svelte @@ -0,0 +1,72 @@ + + +{#if booted} + {@render children()} +{:else} +
+ +
+{/if} + + diff --git a/web/src/routes/+layout.ts b/web/src/routes/+layout.ts new file mode 100644 index 0000000..83addb7 --- /dev/null +++ b/web/src/routes/+layout.ts @@ -0,0 +1,2 @@ +export const ssr = false; +export const prerender = false; diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte new file mode 100644 index 0000000..ea6ac8b --- /dev/null +++ b/web/src/routes/+page.svelte @@ -0,0 +1,133 @@ + + +
+

{exhibit.title}

+ {#if exhibit.subtitle} +

{exhibit.subtitle}

+ {/if} +
+ +{#if pre.state === 'running'} +
+ + + Loading exhibit for offline listening… {pre.done}/{pre.total} + +
+{/if} + +
+ {#each stops as stop (stop.id)} + +
+ +
+
+ {String(stop.id).padStart(2, '0')} + {stop.title} +
+
+ {/each} +
+ + diff --git a/web/src/routes/login/+page.svelte b/web/src/routes/login/+page.svelte new file mode 100644 index 0000000..e4e5da1 --- /dev/null +++ b/web/src/routes/login/+page.svelte @@ -0,0 +1,130 @@ + + +
+
+

{exhibit.title}

+

Enter the password posted at the exhibit to begin.

+
+ + + + {#if error} + + {/if} +
+
+
+ + diff --git a/web/src/routes/stop/[id]/+page.svelte b/web/src/routes/stop/[id]/+page.svelte new file mode 100644 index 0000000..ac14bd3 --- /dev/null +++ b/web/src/routes/stop/[id]/+page.svelte @@ -0,0 +1,373 @@ + + +{#if !stop} +
+

Stop not found.

+ Back to all stops +
+{:else} +
+ + + + {String(stop.id).padStart(2, '0')} +
+ +
+ + {#if volumeWritable} +
+ + +
+ {/if} + + + +

{stop.title}

+ {#if stop.caption} +

{stop.caption}

+ {/if} + + {#if stop.description} +
+ {#each stop.description.split(/\n\n+/) as para} +

{para}

+ {/each} +
+ {/if} +
+ + +{/if} + + diff --git a/web/src/service-worker.ts b/web/src/service-worker.ts new file mode 100644 index 0000000..97aeda5 --- /dev/null +++ b/web/src/service-worker.ts @@ -0,0 +1,159 @@ +/// +/// +/// +/// + +// Production service worker for Docent. +// +// Behavior: +// - Install: precache the SvelteKit shell (build + small static files). +// - Activate: drop caches that don't match the current version. +// - Fetch: +// /api/* → network only (auth state must not be cached) +// navigation → cached index.html as offline fallback +// /audio /img → stale-while-revalidate from ASSET_CACHE +// everything → cache-first from SHELL_CACHE +// - Messages: +// { type: 'precache-stops', urls: string[] } +// Bulk-cache a list of URLs into ASSET_CACHE, posting progress back to +// the sender via { type: 'precache-progress', done, total } and a final +// { type: 'precache-done' } when finished. +// +// In dev (`serviceWorker.register: false` + the layout's dev-mode unregister), +// this file is bundled but never registered. + +import { build, files, version } from '$service-worker'; + +const sw = self as unknown as ServiceWorkerGlobalScope; + +const SHELL_CACHE = `docent-shell-${version}`; +const ASSET_CACHE = `docent-assets-v1`; // version-stable so audio survives shell upgrades + +const shellAssets = [ + ...build, + ...files.filter((f) => !f.startsWith('/audio/') && !f.startsWith('/images/')) +]; + +sw.addEventListener('install', (event) => { + event.waitUntil( + (async () => { + const cache = await caches.open(SHELL_CACHE); + await cache.addAll(shellAssets); + await sw.skipWaiting(); + })() + ); +}); + +sw.addEventListener('activate', (event) => { + event.waitUntil( + (async () => { + const keys = await caches.keys(); + await Promise.all( + keys + .filter((k) => k !== SHELL_CACHE && k !== ASSET_CACHE) + .map((k) => caches.delete(k)) + ); + await sw.clients.claim(); + })() + ); +}); + +sw.addEventListener('fetch', (event) => { + const { request } = event; + if (request.method !== 'GET') return; + + const url = new URL(request.url); + if (url.origin !== location.origin) return; + + // Never cache API responses — auth state must always be authoritative. + if (url.pathname.startsWith('/api/')) return; + + const isContent = url.pathname.startsWith('/audio/') || url.pathname.startsWith('/images/'); + + event.respondWith( + (async () => { + // Navigation: prefer cached shell; fall back to network. + if (request.mode === 'navigate') { + const shell = await caches.open(SHELL_CACHE); + const cached = (await shell.match('/')) || (await shell.match('/index.html')); + if (cached) return cached; + try { + return await fetch(request); + } catch (err) { + return new Response('offline', { status: 503, statusText: 'offline' }); + } + } + + const cacheName = isContent ? ASSET_CACHE : SHELL_CACHE; + const cache = await caches.open(cacheName); + const cached = await cache.match(request); + if (cached) { + if (isContent) { + // Stale-while-revalidate: return cached, refresh in background. + event.waitUntil(refresh(cache, request)); + } + return cached; + } + + try { + const res = await fetch(request); + if (res.ok && res.type === 'basic') { + cache.put(request, res.clone()); + } + return res; + } catch (err) { + return new Response('offline', { status: 503, statusText: 'offline' }); + } + })() + ); +}); + +async function refresh(cache: Cache, request: Request) { + try { + const res = await fetch(request, { credentials: 'same-origin' }); + if (res.ok && res.type === 'basic') await cache.put(request, res); + } catch { + // Offline; keep the cached version. + } +} + +sw.addEventListener('message', (event) => { + const data = event.data; + if (data?.type !== 'precache-stops' || !Array.isArray(data.urls)) return; + + const urls: string[] = data.urls; + const source = event.source; + event.waitUntil(precacheStops(urls, source)); +}); + +async function precacheStops(urls: string[], source: Client | ServiceWorker | MessagePort | null) { + const cache = await caches.open(ASSET_CACHE); + let done = 0; + const total = urls.length; + for (const url of urls) { + try { + const cached = await cache.match(url); + if (!cached) { + const res = await fetch(url, { credentials: 'same-origin' }); + if (res.ok && res.type === 'basic') await cache.put(url, res); + } + } catch { + // Network error — skip and continue. UI shows the count. + } + done++; + try { + (source as Client | null)?.postMessage?.({ + type: 'precache-progress', + done, + total + }); + } catch { + // Sender went away. + } + } + try { + (source as Client | null)?.postMessage?.({ type: 'precache-done', done, total }); + } catch { + // Sender went away. + } +} diff --git a/web/static/manifest.webmanifest b/web/static/manifest.webmanifest new file mode 100644 index 0000000..58597e9 --- /dev/null +++ b/web/static/manifest.webmanifest @@ -0,0 +1,30 @@ +{ + "name": "Mill Run", + "short_name": "Mill Run", + "description": "Audio-guided exhibit", + "start_url": "/", + "display": "standalone", + "orientation": "portrait", + "background_color": "#faf7f2", + "theme_color": "#2a2520", + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/web/static/robots.txt b/web/static/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/web/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/web/svelte.config.js b/web/svelte.config.js new file mode 100644 index 0000000..05357a3 --- /dev/null +++ b/web/svelte.config.js @@ -0,0 +1,21 @@ +import adapter from '@sveltejs/adapter-static'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +export default { + preprocess: vitePreprocess(), + kit: { + adapter: adapter({ + pages: 'build', + assets: 'build', + fallback: 'index.html', + precompress: false, + strict: true + }), + serviceWorker: { + register: false + }, + alias: { + $content: './src/lib/content' + } + } +}; diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..a8f10c8 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..acade8f --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,39 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()], + server: { + host: '0.0.0.0', + port: 5173, + strictPort: true, + allowedHosts: ['docent-dev.pentacle.games', 'localhost'], + hmr: { + protocol: 'wss', + host: 'docent-dev.pentacle.games', + clientPort: 443 + }, + watch: { + usePolling: true, + interval: 500 + }, + fs: { + strict: true, + allow: ['./src', './static', './node_modules', './.svelte-kit'], + deny: [ + '.env', + '.env.*', + '**/.git/**', + '**/secrets/**', + '**/*.{pem,crt,key,age}', + '../server/**', + '../content/**' + ] + }, + proxy: { + '/api': 'http://localhost:8181', + '/audio': 'http://localhost:8181', + '/images': 'http://localhost:8181' + } + } +});