From ca225135c1aee246288e5e64e142b5e21f02ba6a Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 9 Jun 2026 16:05:17 -0700 Subject: [PATCH 01/46] docker: add Docker support with two auth options - Dockerfile: multi-stage build (downloads obsidian + obsidian-mobile renderer bundles in stage 1, installs server deps in stage 2) - docker-compose.yml: base setup, vault persisted via volume - docker-compose.auth-key.yml: API-key auth overlay (AUTH_KEY env var) - docker-compose.auth-basic.yml: nginx HTTP Basic Auth overlay - nginx.conf: nginx reverse proxy with WebSocket support for /api/watch - .dockerignore: excludes vendor/, node_modules/, .git/, etc. - src/server/middleware/auth.js: Express middleware for API-key auth (login page, cookie session, Bearer token support) - src/server/index.js: load auth middleware when AUTH_KEY is set --- .dockerignore | 12 ++++ Dockerfile | 33 +++++++++ docker-compose.auth-basic.yml | 26 ++++++++ docker-compose.auth-key.yml | 15 +++++ docker-compose.yml | 23 +++++++ nginx.conf | 29 ++++++++ src/server/index.js | 9 +++ src/server/middleware/auth.js | 122 ++++++++++++++++++++++++++++++++++ 8 files changed, 269 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.auth-basic.yml create mode 100644 docker-compose.auth-key.yml create mode 100644 docker-compose.yml create mode 100644 nginx.conf create mode 100644 src/server/middleware/auth.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..20c9eaf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +vendor/ +node_modules/ +.git/ +.tmp/ +docs/ +.codenomad/ +.DS_Store +.idea/ +.vscode/ +*.log +npm-debug.log* +nginx.htpasswd diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8396138 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# ── Stage 1: download Obsidian renderer files ───────────────────────────────── +# Both scripts use only Node built-ins (https, zlib, crypto, fs) — no npm install needed. +FROM node:20-alpine AS obsidian-dl +WORKDIR /app +COPY scripts/ scripts/ +RUN node scripts/update-obsidian.js \ + && node scripts/update-obsidian-mobile.js + +# ── Stage 2: production image ────────────────────────────────────────────────── +FROM node:20-alpine +WORKDIR /app + +# App source and default vault +COPY src/ src/ +COPY user-data/ user-data/ + +# Pre-downloaded Obsidian renderer bundles (not in git, built in stage 1) +COPY --from=obsidian-dl /app/vendor/ vendor/ + +# Install server dependencies (production only, no devDeps) +RUN cd src/server && npm ci --omit=dev + +EXPOSE 3000 + +# HOST must be 0.0.0.0 inside a container — 127.0.0.1 would refuse outside connections +ENV HOST=0.0.0.0 +ENV PORT=3000 +ENV VAULT_PATH=user-data/demo-vault +# Optional auth (see docker-compose.auth-key.yml): +# ENV AUTH_KEY=change-me + +# Run from repo root so PROJECT_ROOT resolves correctly via __dirname +CMD ["node", "src/server/index.js"] diff --git a/docker-compose.auth-basic.yml b/docker-compose.auth-basic.yml new file mode 100644 index 0000000..6ee82b9 --- /dev/null +++ b/docker-compose.auth-basic.yml @@ -0,0 +1,26 @@ +# Auth Option 2 — HTTP Basic Auth via nginx +# ───────────────────────────────────────────────────────────────────────────── +# nginx sits in front of the app and enforces HTTP Basic Auth. +# Handles WebSocket upgrade for /api/watch automatically. +# +# Setup (one time): +# htpasswd -c nginx.htpasswd # creates the password file +# +# Usage: +# docker compose -f docker-compose.yml -f docker-compose.auth-basic.yml up + +services: + obsidian-web: + # Remove direct host port — nginx is the only public entrypoint + ports: [] + + nginx: + image: nginx:alpine + ports: + - "${PORT:-3000}:80" + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + - ./nginx.htpasswd:/etc/nginx/htpasswd:ro + depends_on: + - obsidian-web + restart: unless-stopped diff --git a/docker-compose.auth-key.yml b/docker-compose.auth-key.yml new file mode 100644 index 0000000..53fcb56 --- /dev/null +++ b/docker-compose.auth-key.yml @@ -0,0 +1,15 @@ +# Auth Option 1 — API Key +# ───────────────────────────────────────────────────────────────────────────── +# The server checks AUTH_KEY on every request. +# Unauthenticated browsers get a login page; API calls get a 401. +# +# Usage: +# AUTH_KEY=your-secret docker compose -f docker-compose.yml -f docker-compose.auth-key.yml up +# +# Or set AUTH_KEY in a .env file at the repo root and run: +# docker compose -f docker-compose.yml -f docker-compose.auth-key.yml up + +services: + obsidian-web: + environment: + AUTH_KEY: "${AUTH_KEY:?AUTH_KEY must be set (e.g. export AUTH_KEY=your-secret)}" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..503647d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +services: + obsidian-web: + build: . + ports: + - "${PORT:-3000}:3000" + volumes: + # Vault and registry persist on the host — safe across rebuilds + - ./user-data:/app/user-data + environment: + HOST: "0.0.0.0" + PORT: "3000" + VAULT_PATH: "user-data/demo-vault" + # VAULT_REGISTRY: "user-data/registry.json" + # WATCH_POLLING: "true" # enable for NFS / SMB / FUSE-mounted vaults + restart: unless-stopped + +# ── Auth options ─────────────────────────────────────────────────────────────── +# Option 1 — API Key (built into the server, no extra container): +# docker compose -f docker-compose.yml -f docker-compose.auth-key.yml up +# +# Option 2 — HTTP Basic Auth via nginx (requires nginx.htpasswd): +# htpasswd -c nginx.htpasswd +# docker compose -f docker-compose.yml -f docker-compose.auth-basic.yml up diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..8e0fbe1 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,29 @@ +server { + listen 80; + server_name _; + + # HTTP Basic Auth — generate nginx.htpasswd with: + # htpasswd -c nginx.htpasswd + auth_basic "Obsidian Web"; + auth_basic_user_file /etc/nginx/htpasswd; + + # WebSocket endpoint — must set Upgrade + Connection headers + location /api/watch { + proxy_pass http://obsidian-web:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 3600s; + } + + # Everything else + location / { + proxy_pass http://obsidian-web:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + client_max_body_size 100M; + } +} diff --git a/src/server/index.js b/src/server/index.js index 0c1c3f6..98f50a2 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -24,6 +24,7 @@ const { warmUpBootstrapCache } = require('./api/bootstrap'); const createProxyRouter = require('./api/proxy'); const attachWatchServer = require('./api/watch'); const VaultRegistry = require('./vault-registry'); +const { createAuthMiddleware } = require('./middleware/auth'); function createApp(appConfig = config) { const app = express(); @@ -34,6 +35,14 @@ function createApp(appConfig = config) { // on Accept-Encoding: browsers get brotli, curl/other tools get gzip. app.use(compression({ level: 6 })); + // Optional API-key auth — enabled by setting AUTH_KEY env var. + // See docker-compose.auth-key.yml for usage. + const authMiddleware = createAuthMiddleware(); + if (authMiddleware) { + app.use(authMiddleware); + console.log('[auth] API-key authentication enabled'); + } + // Request logging - very chatty, but invaluable while we are still // figuring out what Obsidian asks for during boot. app.use((req, res, next) => { diff --git a/src/server/middleware/auth.js b/src/server/middleware/auth.js new file mode 100644 index 0000000..2b1795a --- /dev/null +++ b/src/server/middleware/auth.js @@ -0,0 +1,122 @@ +'use strict'; + +/** + * Optional API-key authentication middleware. + * Activated when AUTH_KEY env var is set. + * + * The key can be supplied via: + * - Cookie: obsidian-web-key= + * - Query param + login: /__auth?key= + * - Authorization header: Bearer (for API clients) + * + * Browser requests go to a login page; /api/* gets a 401 JSON response. + */ + +const AUTH_KEY = process.env.AUTH_KEY || ''; +const COOKIE = 'obsidian-web-key'; +const MAX_AGE = 7 * 24 * 60 * 60; // 7 days (seconds) + +function parseCookies(req) { + return Object.fromEntries( + (req.headers.cookie || '').split(';') + .map(c => c.trim().split('=')) + .filter(p => p.length >= 2) + .map(([k, ...v]) => [k.trim(), v.join('=')]) + ); +} + +function isAuthenticated(req) { + if (parseCookies(req)[COOKIE] === AUTH_KEY) return true; + if (req.query?.key === AUTH_KEY) return true; + const auth = req.headers.authorization || ''; + if (auth.startsWith('Bearer ') && auth.slice(7) === AUTH_KEY) return true; + return false; +} + +const LOGIN_HTML = ` + + + + + Obsidian Web — Sign in + + + +
+

🔒 Obsidian Web

+

Invalid key — try again.

+
+ + + +
+
+ + +`; + +/** + * Returns an Express middleware if AUTH_KEY is set, otherwise null. + */ +function createAuthMiddleware() { + if (!AUTH_KEY) return null; + + return function authMiddleware(req, res, next) { + // ── Auth callback: verify key, set cookie, redirect ── + if (req.path === '/__auth') { + const key = req.query?.key || ''; + const dest = req.query?.next || '/'; + if (key === AUTH_KEY) { + res.setHeader('Set-Cookie', + `${COOKIE}=${AUTH_KEY}; Path=/; Max-Age=${MAX_AGE}; HttpOnly; SameSite=Strict`); + return res.redirect(dest); + } + return res.redirect(`/__login?error=1&next=${encodeURIComponent(dest)}`); + } + + // ── Login page: always accessible ── + if (req.path === '/__login') { + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + return res.end(LOGIN_HTML); + } + + // ── Authenticated: pass through ── + if (isAuthenticated(req)) return next(); + + // ── Unauthenticated ── + const wantsJson = req.path.startsWith('/api/') || + (req.headers.accept || '').includes('application/json'); + if (wantsJson) { + return res.status(401).json({ error: 'Unauthorized — provide a valid Bearer token or cookie' }); + } + res.redirect(`/__login?next=${encodeURIComponent(req.originalUrl)}`); + }; +} + +module.exports = { createAuthMiddleware }; From ad8333a1de92ffdc82039b2f7fd1654e10766c43 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 9 Jun 2026 16:33:47 -0700 Subject: [PATCH 02/46] docker: consolidate auth options into single docker-compose.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both auth options are now in one file — easier for Dockhand and other Docker Compose users to work with a single file: - Option 1 (API Key): uncomment AUTH_KEY in the env section, or set it in Dockhand's stack environment editor - Option 2 (Basic Auth): docker compose --profile auth-nginx up (requires nginx.htpasswd generated with htpasswd) Removes docker-compose.auth-key.yml and docker-compose.auth-basic.yml. --- docker-compose.auth-basic.yml | 26 ------------------------ docker-compose.auth-key.yml | 15 -------------- docker-compose.yml | 37 +++++++++++++++++++++++++++-------- 3 files changed, 29 insertions(+), 49 deletions(-) delete mode 100644 docker-compose.auth-basic.yml delete mode 100644 docker-compose.auth-key.yml diff --git a/docker-compose.auth-basic.yml b/docker-compose.auth-basic.yml deleted file mode 100644 index 6ee82b9..0000000 --- a/docker-compose.auth-basic.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Auth Option 2 — HTTP Basic Auth via nginx -# ───────────────────────────────────────────────────────────────────────────── -# nginx sits in front of the app and enforces HTTP Basic Auth. -# Handles WebSocket upgrade for /api/watch automatically. -# -# Setup (one time): -# htpasswd -c nginx.htpasswd # creates the password file -# -# Usage: -# docker compose -f docker-compose.yml -f docker-compose.auth-basic.yml up - -services: - obsidian-web: - # Remove direct host port — nginx is the only public entrypoint - ports: [] - - nginx: - image: nginx:alpine - ports: - - "${PORT:-3000}:80" - volumes: - - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro - - ./nginx.htpasswd:/etc/nginx/htpasswd:ro - depends_on: - - obsidian-web - restart: unless-stopped diff --git a/docker-compose.auth-key.yml b/docker-compose.auth-key.yml deleted file mode 100644 index 53fcb56..0000000 --- a/docker-compose.auth-key.yml +++ /dev/null @@ -1,15 +0,0 @@ -# Auth Option 1 — API Key -# ───────────────────────────────────────────────────────────────────────────── -# The server checks AUTH_KEY on every request. -# Unauthenticated browsers get a login page; API calls get a 401. -# -# Usage: -# AUTH_KEY=your-secret docker compose -f docker-compose.yml -f docker-compose.auth-key.yml up -# -# Or set AUTH_KEY in a .env file at the repo root and run: -# docker compose -f docker-compose.yml -f docker-compose.auth-key.yml up - -services: - obsidian-web: - environment: - AUTH_KEY: "${AUTH_KEY:?AUTH_KEY must be set (e.g. export AUTH_KEY=your-secret)}" diff --git a/docker-compose.yml b/docker-compose.yml index 503647d..78f39a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: ports: - "${PORT:-3000}:3000" volumes: - # Vault and registry persist on the host — safe across rebuilds + # Vault and registry persist on the host — safe across container rebuilds - ./user-data:/app/user-data environment: HOST: "0.0.0.0" @@ -12,12 +12,33 @@ services: VAULT_PATH: "user-data/demo-vault" # VAULT_REGISTRY: "user-data/registry.json" # WATCH_POLLING: "true" # enable for NFS / SMB / FUSE-mounted vaults + + # ── Auth Option 1: API Key ───────────────────────────────────────────── + # Uncomment and set AUTH_KEY to enable a login page + cookie session. + # In Dockhand you can set this in the stack's environment editor instead. + # AUTH_KEY: "change-me" restart: unless-stopped -# ── Auth options ─────────────────────────────────────────────────────────────── -# Option 1 — API Key (built into the server, no extra container): -# docker compose -f docker-compose.yml -f docker-compose.auth-key.yml up -# -# Option 2 — HTTP Basic Auth via nginx (requires nginx.htpasswd): -# htpasswd -c nginx.htpasswd -# docker compose -f docker-compose.yml -f docker-compose.auth-basic.yml up + # ── Auth Option 2: HTTP Basic Auth via nginx ──────────────────────────────── + # Puts nginx in front of the app with username/password protection. + # Also handles WebSocket upgrade for real-time vault sync (/api/watch). + # + # Setup (run once on the host): + # htpasswd -c nginx.htpasswd + # + # To start with this option: + # docker compose --profile auth-nginx up + # + # When using this profile, comment out the `ports` on obsidian-web above + # so the app is only reachable through nginx, not directly. + nginx: + profiles: ["auth-nginx"] + image: nginx:alpine + ports: + - "${PORT:-3000}:80" + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + - ./nginx.htpasswd:/etc/nginx/htpasswd:ro + depends_on: + - obsidian-web + restart: unless-stopped From c2c4e9be0cfa6ebc6c5784b671f7674a2769c572 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 9 Jun 2026 16:39:48 -0700 Subject: [PATCH 03/46] docker: add unzip to build stage for obsidian-mobile script --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 8396138..792816c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,8 @@ # Both scripts use only Node built-ins (https, zlib, crypto, fs) — no npm install needed. FROM node:20-alpine AS obsidian-dl WORKDIR /app +# unzip is required by update-obsidian-mobile.js (extracts assets from the APK) +RUN apk add --no-cache unzip COPY scripts/ scripts/ RUN node scripts/update-obsidian.js \ && node scripts/update-obsidian-mobile.js From 9d08213156129f2fb5136b6919f5459a02f7df54 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 9 Jun 2026 16:41:55 -0700 Subject: [PATCH 04/46] docker: move vendor download to entrypoint (fixes build-time network error) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docker build environments have no internet access, so downloading Obsidian renderer files at build time (EAI_AGAIN / DNS failure) doesn't work. Instead: - entrypoint.sh checks for vendor/obsidian/app.js on startup and runs the download scripts only if missing (~30s on first start) - obsidian_vendor Docker volume caches the result — subsequent starts are instant - Dockerfile becomes a single stage with no network dependency at build time --- Dockerfile | 33 +++++++++++++-------------------- docker-compose.yml | 7 +++++++ entrypoint.sh | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 792816c..37209b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,35 +1,28 @@ -# ── Stage 1: download Obsidian renderer files ───────────────────────────────── -# Both scripts use only Node built-ins (https, zlib, crypto, fs) — no npm install needed. -FROM node:20-alpine AS obsidian-dl -WORKDIR /app -# unzip is required by update-obsidian-mobile.js (extracts assets from the APK) -RUN apk add --no-cache unzip -COPY scripts/ scripts/ -RUN node scripts/update-obsidian.js \ - && node scripts/update-obsidian-mobile.js - -# ── Stage 2: production image ────────────────────────────────────────────────── FROM node:20-alpine WORKDIR /app -# App source and default vault +# unzip is required by scripts/update-obsidian-mobile.js (extracts APK assets) +RUN apk add --no-cache unzip + +# App source, scripts, and default vault COPY src/ src/ +COPY scripts/ scripts/ COPY user-data/ user-data/ +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh -# Pre-downloaded Obsidian renderer bundles (not in git, built in stage 1) -COPY --from=obsidian-dl /app/vendor/ vendor/ - -# Install server dependencies (production only, no devDeps) +# Install server dependencies (production only) RUN cd src/server && npm ci --omit=dev EXPOSE 3000 -# HOST must be 0.0.0.0 inside a container — 127.0.0.1 would refuse outside connections +# HOST must be 0.0.0.0 inside a container ENV HOST=0.0.0.0 ENV PORT=3000 ENV VAULT_PATH=user-data/demo-vault -# Optional auth (see docker-compose.auth-key.yml): -# ENV AUTH_KEY=change-me -# Run from repo root so PROJECT_ROOT resolves correctly via __dirname +# Obsidian renderer bundles are downloaded at first start into the +# obsidian_vendor volume (see docker-compose.yml). Network is available +# at runtime but not during `docker build`, so we can't download here. +ENTRYPOINT ["/entrypoint.sh"] CMD ["node", "src/server/index.js"] diff --git a/docker-compose.yml b/docker-compose.yml index 78f39a8..b508b99 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,8 @@ services: volumes: # Vault and registry persist on the host — safe across container rebuilds - ./user-data:/app/user-data + # Obsidian renderer bundles — downloaded on first start, cached across restarts + - obsidian_vendor:/app/vendor environment: HOST: "0.0.0.0" PORT: "3000" @@ -19,6 +21,11 @@ services: # AUTH_KEY: "change-me" restart: unless-stopped +volumes: + # Named volume so the downloaded Obsidian bundles survive container rebuilds. + # Delete with `docker volume rm obsidian_vendor` to force a fresh download. + obsidian_vendor: + # ── Auth Option 2: HTTP Basic Auth via nginx ──────────────────────────────── # Puts nginx in front of the app with username/password protection. # Also handles WebSocket upgrade for real-time vault sync (/api/watch). diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..3874859 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -e + +# Download Obsidian renderer bundles on first start (or if the volume is empty). +# Results are cached in the `obsidian_vendor` Docker volume — subsequent starts +# are instant because the files are already there. + +if [ ! -f /app/vendor/obsidian/app.js ]; then + echo "[docker] Downloading Obsidian desktop renderer (first run — this takes ~30s)..." + node scripts/update-obsidian.js +fi + +if [ ! -f /app/vendor/obsidian-mobile/app.js ]; then + echo "[docker] Downloading Obsidian mobile renderer (first run — this takes ~30s)..." + node scripts/update-obsidian-mobile.js +fi + +exec "$@" From 1191ee0816716357f5ec78eaf74b95a5320c461c Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 9 Jun 2026 16:43:00 -0700 Subject: [PATCH 05/46] docker: fix docker-compose.yml structure (nginx inside services, not volumes) --- docker-compose.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b508b99..c844e20 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,11 +21,6 @@ services: # AUTH_KEY: "change-me" restart: unless-stopped -volumes: - # Named volume so the downloaded Obsidian bundles survive container rebuilds. - # Delete with `docker volume rm obsidian_vendor` to force a fresh download. - obsidian_vendor: - # ── Auth Option 2: HTTP Basic Auth via nginx ──────────────────────────────── # Puts nginx in front of the app with username/password protection. # Also handles WebSocket upgrade for real-time vault sync (/api/watch). @@ -49,3 +44,8 @@ volumes: depends_on: - obsidian-web restart: unless-stopped + +volumes: + # Named volume so the downloaded Obsidian bundles survive container rebuilds. + # Delete with `docker volume rm obsidian_vendor` to force a fresh download. + obsidian_vendor: From b939fb90d8d69c83cc59b3a3042875461406376a Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 9 Jun 2026 16:48:44 -0700 Subject: [PATCH 06/46] docker: fix EXDEV rename error by symlinking .tmp into vendor volume MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The update scripts extract to /app/.tmp/ then rename() into /app/vendor/. rename() fails with EXDEV when src and dst are on different filesystems — which they are in Docker (.tmp = container layer, vendor = named volume). Fix: at startup, mkdir /app/vendor/.tmp and symlink /app/.tmp -> /app/vendor/.tmp so both paths live on the same volume filesystem. --- entrypoint.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 3874859..0d1bdd2 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,9 +1,14 @@ #!/bin/sh set -e -# Download Obsidian renderer bundles on first start (or if the volume is empty). -# Results are cached in the `obsidian_vendor` Docker volume — subsequent starts -# are instant because the files are already there. +# The update scripts extract files to /app/.tmp/ then atomically rename() them +# into /app/vendor/. rename() across different filesystems raises EXDEV, and +# /app/.tmp (container layer) vs /app/vendor (Docker volume) are different +# devices. Fix: redirect .tmp into the vendor volume so both paths share the +# same filesystem and rename() succeeds. +mkdir -p /app/vendor/.tmp +rm -rf /app/.tmp +ln -sf /app/vendor/.tmp /app/.tmp if [ ! -f /app/vendor/obsidian/app.js ]; then echo "[docker] Downloading Obsidian desktop renderer (first run — this takes ~30s)..." From 9d2b7c275f55ff2dd51b5799eb4c8a1832592009 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 9 Jun 2026 17:11:49 -0700 Subject: [PATCH 07/46] feat: custom vault/plugins mount, .env auth key, mobile safe-area fix docker-compose.yml: - commented examples for mounting your own vault folder and plugins dir - AUTH_KEY now loaded from .env file (gitignored) or Dockhand env editor - env_file with required:false so .env is optional auth: - .env.example shows how to configure AUTH_KEY locally - .gitignore: add .env so real keys are never committed mobile: - index.html: add env(safe-area-inset-top) padding on html element so content is not hidden under the status bar / notch on iOS and Android --- .env.example | 9 ++++++++ .gitignore | 3 +++ docker-compose.yml | 45 +++++++++++++++++++++--------------- src/client-mobile/index.html | 7 ++++++ 4 files changed, 46 insertions(+), 18 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..96e1833 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Copy this file to .env and fill in your values. +# .env is gitignored — never commit real secrets to the repo. + +# API key auth — set this to enable the login page. +# Can also be set directly in Dockhand's environment editor. +AUTH_KEY=change-me + +# Optional: override the default port +# PORT=3000 diff --git a/.gitignore b/.gitignore index ae885b8..2378298 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Vendor — extracted Obsidian bundles (regeneratable via scripts/update-obsidian.js) vendor/ +# Local environment — copy .env.example to .env, never commit real secrets +.env + # Node node_modules/ npm-debug.log* diff --git a/docker-compose.yml b/docker-compose.yml index c844e20..262a40d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,35 +4,46 @@ services: ports: - "${PORT:-3000}:3000" volumes: - # Vault and registry persist on the host — safe across container rebuilds + # ── Vault ────────────────────────────────────────────────────────────── + # Option A (default): use the built-in demo vault - ./user-data:/app/user-data - # Obsidian renderer bundles — downloaded on first start, cached across restarts + + # Option B: mount YOUR OWN vault folder from the host. + # 1. Uncomment the line below, replacing the left side with your path. + # 2. Set VAULT_PATH (in environment below) to match the right side. + # - /path/to/your/vault:/app/vaults/my-vault + + # ── Plugins ──────────────────────────────────────────────────────────── + # Mount a folder of extra Obsidian plugins into the container. + # Uncomment and point at your local .obsidian/plugins directory. + # - /path/to/your/.obsidian/plugins:/app/user-data/demo-vault/.obsidian/plugins + + # ── Obsidian renderer bundles (downloaded on first start) ─────────────── - obsidian_vendor:/app/vendor + environment: HOST: "0.0.0.0" PORT: "3000" + + # Path to the active vault — relative to the project root (/app inside + # the container), or absolute. Change this if you use Option B above. VAULT_PATH: "user-data/demo-vault" + # VAULT_REGISTRY: "user-data/registry.json" # WATCH_POLLING: "true" # enable for NFS / SMB / FUSE-mounted vaults # ── Auth Option 1: API Key ───────────────────────────────────────────── - # Uncomment and set AUTH_KEY to enable a login page + cookie session. - # In Dockhand you can set this in the stack's environment editor instead. - # AUTH_KEY: "change-me" + # Set AUTH_KEY via a .env file or Dockhand's environment editor. + # Never commit a real key here — this file is public. + # AUTH_KEY: "${AUTH_KEY}" + env_file: + - path: .env + required: false # silently ignored if .env doesn't exist restart: unless-stopped # ── Auth Option 2: HTTP Basic Auth via nginx ──────────────────────────────── - # Puts nginx in front of the app with username/password protection. - # Also handles WebSocket upgrade for real-time vault sync (/api/watch). - # - # Setup (run once on the host): - # htpasswd -c nginx.htpasswd - # - # To start with this option: - # docker compose --profile auth-nginx up - # - # When using this profile, comment out the `ports` on obsidian-web above - # so the app is only reachable through nginx, not directly. + # Setup: htpasswd -c nginx.htpasswd + # Start: docker compose --profile auth-nginx up nginx: profiles: ["auth-nginx"] image: nginx:alpine @@ -46,6 +57,4 @@ services: restart: unless-stopped volumes: - # Named volume so the downloaded Obsidian bundles survive container rebuilds. - # Delete with `docker volume rm obsidian_vendor` to force a fresh download. obsidian_vendor: diff --git a/src/client-mobile/index.html b/src/client-mobile/index.html index fdbfc9e..28d6c59 100644 --- a/src/client-mobile/index.html +++ b/src/client-mobile/index.html @@ -6,6 +6,13 @@ Obsidian Web
-

🔒 Obsidian Web

-

Invalid key — try again.

-
- - - +

🔐 Obsidian Web

+

Enter the 6-digit code from your authenticator app

+ + + +
+ + + + + + +
+

+
`; -/** - * Returns an Express middleware if AUTH_KEY is set, otherwise null. - */ +async function buildSetupPage(secret) { + const otpauth = authenticator.keyuri('obsidian-web', 'Obsidian Web', secret); + const qrSvg = await QRCode.toString(otpauth, { type: 'svg', width: 220, margin: 2 }); + return ` + + + + + Obsidian Web — Authenticator Setup + + + +
+

🔐 Authenticator Setup

+

Scan the QR code with your authenticator app, or enter the secret manually.

+ +
${qrSvg}
+ +

Manual entry code

+
${secret}
+ +

Compatible apps

+ + +
+ Keep this page private.
+ Once you've scanned or copied the code, close this tab. + The secret is stored in your TOTP_SECRET environment variable. +
+
+ +`; +} + +// ── Middleware factory ───────────────────────────────────────────────────────── + function createAuthMiddleware() { - if (!AUTH_KEY) return null; + if (!TOTP_SECRET) return null; + + return async function authMiddleware(req, res, next) { + + // ── Setup page ── requires ?token=TOTP_SECRET so only the owner can view it + if (req.path === '/__totp-setup') { + if ((req.query?.token || '') !== TOTP_SECRET) { + return res.status(403).end('Forbidden — add ?token=YOUR_TOTP_SECRET to the URL'); + } + const html = await buildSetupPage(TOTP_SECRET); + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + return res.end(html); + } - return function authMiddleware(req, res, next) { - // ── Auth callback: verify key, set cookie, redirect ── + // ── Auth endpoint ── verify code, set cookie if (req.path === '/__auth') { - const key = req.query?.key || ''; + const code = String(req.query?.code || '').replace(/\D/g, ''); const dest = req.query?.next || '/'; - if (key === AUTH_KEY) { + if (code.length === 6 && authenticator.verify({ token: code, secret: TOTP_SECRET })) { res.setHeader('Set-Cookie', - `${COOKIE}=${AUTH_KEY}; Path=/; Max-Age=${MAX_AGE}; HttpOnly; SameSite=Strict`); + `${COOKIE}=${SESSION_TOKEN}; Path=/; Max-Age=${MAX_AGE}; HttpOnly; SameSite=Strict`); return res.redirect(dest); } return res.redirect(`/__login?error=1&next=${encodeURIComponent(dest)}`); } - // ── Login page: always accessible ── + // ── Login page ── always accessible (no point hiding it) if (req.path === '/__login') { res.setHeader('Content-Type', 'text/html; charset=utf-8'); return res.end(LOGIN_HTML); } - // ── Authenticated: pass through ── + // ── Authenticated ── if (isAuthenticated(req)) return next(); // ── Unauthenticated ── const wantsJson = req.path.startsWith('/api/') || (req.headers.accept || '').includes('application/json'); if (wantsJson) { - return res.status(401).json({ error: 'Unauthorized — provide a valid Bearer token or cookie' }); + return res.status(401).json({ error: 'Unauthorized — valid TOTP session required' }); } res.redirect(`/__login?next=${encodeURIComponent(req.originalUrl)}`); }; diff --git a/src/server/package-lock.json b/src/server/package-lock.json index 29c4a64..60f3e90 100644 --- a/src/server/package-lock.json +++ b/src/server/package-lock.json @@ -1,1100 +1,1476 @@ -{ - "name": "obsidian-web-server", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "obsidian-web-server", - "version": "0.1.0", - "dependencies": { - "chokidar": "^3.6.0", - "compression": "^1.8.1", - "express": "^4.21.0", - "ws": "^8.18.0" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "license": "MIT" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/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==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - } - } -} +{ + "name": "obsidian-web-server", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "obsidian-web-server", + "version": "0.1.0", + "dependencies": { + "chokidar": "^3.6.0", + "compression": "^1.8.1", + "express": "^4.21.0", + "otplib": "^12.0.1", + "qrcode": "^1.5.4", + "ws": "^8.18.0" + } + }, + "node_modules/@otplib/core": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz", + "integrity": "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==", + "license": "MIT" + }, + "node_modules/@otplib/plugin-crypto": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz", + "integrity": "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==", + "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths", + "license": "MIT", + "dependencies": { + "@otplib/core": "^12.0.1" + } + }, + "node_modules/@otplib/plugin-thirty-two": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz", + "integrity": "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==", + "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths", + "license": "MIT", + "dependencies": { + "@otplib/core": "^12.0.1", + "thirty-two": "^1.0.2" + } + }, + "node_modules/@otplib/preset-default": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/preset-default/-/preset-default-12.0.1.tgz", + "integrity": "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==", + "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths", + "license": "MIT", + "dependencies": { + "@otplib/core": "^12.0.1", + "@otplib/plugin-crypto": "^12.0.1", + "@otplib/plugin-thirty-two": "^12.0.1" + } + }, + "node_modules/@otplib/preset-v11": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/preset-v11/-/preset-v11-12.0.1.tgz", + "integrity": "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==", + "license": "MIT", + "dependencies": { + "@otplib/core": "^12.0.1", + "@otplib/plugin-crypto": "^12.0.1", + "@otplib/plugin-thirty-two": "^12.0.1" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/otplib": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz", + "integrity": "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==", + "license": "MIT", + "dependencies": { + "@otplib/core": "^12.0.1", + "@otplib/preset-default": "^12.0.1", + "@otplib/preset-v11": "^12.0.1" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/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==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/thirty-two": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz", + "integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==", + "engines": { + "node": ">=0.2.6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/src/server/package.json b/src/server/package.json index b14ae32..6eb4049 100644 --- a/src/server/package.json +++ b/src/server/package.json @@ -1,18 +1,20 @@ -{ - "name": "obsidian-web-server", - "version": "0.1.0", - "description": "HTTP/WebSocket backend for Obsidian web wrapper", - "main": "index.js", - "type": "commonjs", - "scripts": { - "start": "node index.js", - "dev": "node --watch index.js", - "test": "node --test" - }, - "dependencies": { - "chokidar": "^3.6.0", - "compression": "^1.8.1", - "express": "^4.21.0", - "ws": "^8.18.0" - } -} +{ + "name": "obsidian-web-server", + "version": "0.1.0", + "description": "HTTP/WebSocket backend for Obsidian web wrapper", + "main": "index.js", + "type": "commonjs", + "scripts": { + "start": "node index.js", + "dev": "node --watch index.js", + "test": "node --test" + }, + "dependencies": { + "chokidar": "^3.6.0", + "compression": "^1.8.1", + "express": "^4.21.0", + "otplib": "^12.0.1", + "qrcode": "^1.5.4", + "ws": "^8.18.0" + } +} From e610b73877c2b5cc85b4f46bd7284b60e79f4867 Mon Sep 17 00:00:00 2001 From: s39n Date: Tue, 9 Jun 2026 19:55:34 -0700 Subject: [PATCH 15/46] fix: open-url recursion + proxy redirect following --- src/client/shims/electron.js | 14 ++++++++++++-- src/server/api/proxy.js | 12 +++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/client/shims/electron.js b/src/client/shims/electron.js index 899807a..67a0960 100644 --- a/src/client/shims/electron.js +++ b/src/client/shims/electron.js @@ -18,6 +18,13 @@ * are stubs that log so we can spot uses we missed. */ (function (global) { + // Save a reference to the real window.open BEFORE Obsidian patches it. + // Obsidian overrides window.open to route URLs through its own link handler, + // which calls ipcRenderer.send('open-url', ...) — so if our 'open-url' + // handler calls window.open, we get infinite recursion. Using this saved + // reference bypasses Obsidian's patch and opens a real new tab. + const _nativeWindowOpen = window.open.bind(window); + function warnUnimplemented(name) { return function () { console.warn('[obsidian-web] electron.' + name + ' called but not implemented:', arguments); @@ -360,8 +367,11 @@ } // 'open-url' opens in a new tab. + // Use _nativeWindowOpen (saved before Obsidian patches window.open) + // to avoid the infinite recursion: Obsidian's patched window.open + // routes back through ipcRenderer.send('open-url', ...). if (channel === 'open-url' && args[0]) { - window.open(args[0], '_blank', 'noopener'); + _nativeWindowOpen(args[0], '_blank', 'noopener'); return; } // Application-menu IPC channels - ignored on web. Obsidian renders @@ -408,7 +418,7 @@ const remote = { shell: { showItemInFolder: warnUnimplemented('shell.showItemInFolder'), - openExternal: (url) => { window.open(url, '_blank', 'noopener'); return Promise.resolve(); }, + openExternal: (url) => { _nativeWindowOpen(url, '_blank', 'noopener'); return Promise.resolve(); }, openPath: warnUnimplemented('shell.openPath'), }, dialog: { diff --git a/src/server/api/proxy.js b/src/server/api/proxy.js index bfb306f..fa4f400 100644 --- a/src/server/api/proxy.js +++ b/src/server/api/proxy.js @@ -48,7 +48,7 @@ function isAllowed(urlStr) { } } -function fetchUrl(urlStr, method, reqHeaders, body) { +function fetchUrl(urlStr, method, reqHeaders, body, redirectsLeft = 5) { return new Promise((resolve, reject) => { let parsed; try { parsed = new URL(urlStr); } catch (e) { return reject(e); } @@ -63,6 +63,16 @@ function fetchUrl(urlStr, method, reqHeaders, body) { }; const req = lib.request(options, (res) => { + // Follow redirects (GitHub releases redirect to CDN) + if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) { + res.resume(); // drain the response + if (redirectsLeft <= 0) return reject(new Error('too many redirects')); + const next = new URL(res.headers.location, urlStr).toString(); + // Don't check allow-list for redirect targets — caller already validated origin + fetchUrl(next, res.statusCode === 303 ? 'GET' : method, reqHeaders, body, redirectsLeft - 1) + .then(resolve).catch(reject); + return; + } const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => { From 86aa2c8ecc1c8bc04413f2713e934933862dd4ef Mon Sep 17 00:00:00 2001 From: s39n Date: Tue, 9 Jun 2026 20:03:20 -0700 Subject: [PATCH 16/46] fix: open-url recursion, proxy redirects, crypto.randomUUID polyfill --- src/client/boot.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/client/boot.js b/src/client/boot.js index ac8fea0..8b59edd 100644 --- a/src/client/boot.js +++ b/src/client/boot.js @@ -19,6 +19,16 @@ * the browser can download them in parallel but executes them in order. */ +// Polyfill crypto.randomUUID for non-secure contexts (plain HTTP on LAN). +// Browsers restrict this API to HTTPS/localhost; plugins like ion-sync need it. +if (typeof crypto !== 'undefined' && !crypto.randomUUID) { + crypto.randomUUID = function () { + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); + }; +} + // Ordered list of Obsidian's renderer scripts — mirrors the old + diff --git a/src/client/boot.js b/src/client/boot.js index e21fbdd..1d85e94 100644 --- a/src/client/boot.js +++ b/src/client/boot.js @@ -777,12 +777,25 @@ const OBSIDIAN_SCRIPTS = [ if (pollTimer) clearInterval(pollTimer); } - fetch('/api/bootstrap?vault=' + vaultParam + '&full=1') - .then(function (res) { - if (!res.ok) throw new Error('HTTP ' + res.status); - return res.json(); - }) - .then(function (data) { + // Bootstrap cache and the server-backed localStorage load in parallel. + // Both must be ready BEFORE Obsidian's scripts are injected: app.js + // reads localStorage (safeStorage tokens, app state) synchronously at + // startup. A localStorage failure is non-fatal — native storage stays. + Promise.all([ + fetch('/api/bootstrap?vault=' + vaultParam + '&full=1') + .then(function (res) { + if (!res.ok) throw new Error('HTTP ' + res.status); + return res.json(); + }), + (window.__owInstallRemoteLocalStorage + ? window.__owInstallRemoteLocalStorage() + : Promise.resolve() + ).catch(function (e) { + console.warn('[obsidian-web] remote localStorage unavailable, staying device-local:', e && e.message); + }), + ]) + .then(function (results) { + var data = results[0]; stopPolling(); var vault = data.electron && data.electron['vault']; if (!vault || !vault.id) { diff --git a/src/client/index.html b/src/client/index.html index c79f122..56024be 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -54,6 +54,7 @@ together whenever any client file changes and needs to reach the browser past its HTTP cache. One global version for all client files. --> + diff --git a/src/client/shims/remote-localstorage.js b/src/client/shims/remote-localstorage.js new file mode 100644 index 0000000..ad64b3d --- /dev/null +++ b/src/client/shims/remote-localstorage.js @@ -0,0 +1,178 @@ +'use strict'; + +/** + * Server-backed window.localStorage replacement. + * + * Why: Obsidian and its plugins store state in localStorage — most + * importantly the safeStorage keychain tokens (see electron.js shim). + * Browser localStorage is per-device AND per-origin, so secrets entered on + * one PC don't roam to another, and http://nas:3005 vs the cloudflared URL + * don't even share state on the same PC. This shim makes localStorage live + * server-side (user-data/.localstorage.json) so it follows the vault, + * matching real Electron semantics (one install = one shared store). + * + * Keys prefixed 'obsidian-web:' (layout mode, last vault id) are deliberately + * device-local and keep using the native localStorage. + * + * Install contract: boot.js calls window.__owInstallRemoteLocalStorage() + * BEFORE injecting Obsidian's scripts and waits for the returned promise. + * On failure the native localStorage stays in place (old behavior). + */ +(function () { + var LOCAL_PREFIX = 'obsidian-web:'; // stays per-device + var FLUSH_DELAY_MS = 300; + + window.__owInstallRemoteLocalStorage = function () { + var native = window.localStorage; + + return fetch('/api/localstorage') + .then(function (res) { + if (!res.ok) throw new Error('HTTP ' + res.status); + return res.json(); + }) + .then(function (serverData) { + var mem = {}; + Object.keys(serverData).forEach(function (k) { mem[k] = String(serverData[k]); }); + + // One-time migration: keys already in this device's native storage + // but missing on the server get uploaded, so the first PC that runs + // this seeds the server store with its existing tokens/state. + var seed = {}; + var seeded = 0; + for (var i = 0; i < native.length; i++) { + var k = native.key(i); + if (k.indexOf(LOCAL_PREFIX) === 0) continue; + if (!(k in mem)) { + var v = native.getItem(k); + mem[k] = v; + seed[k] = v; + seeded++; + } + } + + // ── Debounced write-through ────────────────────────────────────── + var pending = {}; // key → value|null + var hasPending = false; + var timer = null; + + function queue(key, value) { + pending[key] = value; + hasPending = true; + if (timer) clearTimeout(timer); + timer = setTimeout(flush, FLUSH_DELAY_MS); + } + + function flush() { + if (!hasPending) return; + var entries = pending; + pending = {}; + hasPending = false; + timer = null; + fetch('/api/localstorage', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ entries: entries }), + }).catch(function (e) { + console.warn('[obsidian-web] localStorage flush failed:', e && e.message); + }); + } + + // Last-chance flush when the tab closes. keepalive lets the PUT + // survive page unload (sendBeacon can't be used — it only POSTs). + window.addEventListener('pagehide', function () { + if (!hasPending) return; + var entries = pending; + pending = {}; + hasPending = false; + try { + fetch('/api/localstorage', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ entries: entries }), + keepalive: true, + }); + } catch (_) {} + }); + + // ── Storage-compatible facade ──────────────────────────────────── + var storageShim = { + getItem: function (key) { + key = String(key); + if (key.indexOf(LOCAL_PREFIX) === 0) return native.getItem(key); + return Object.prototype.hasOwnProperty.call(mem, key) ? mem[key] : null; + }, + setItem: function (key, value) { + key = String(key); value = String(value); + if (key.indexOf(LOCAL_PREFIX) === 0) return native.setItem(key, value); + mem[key] = value; + queue(key, value); + }, + removeItem: function (key) { + key = String(key); + if (key.indexOf(LOCAL_PREFIX) === 0) return native.removeItem(key); + if (Object.prototype.hasOwnProperty.call(mem, key)) { + delete mem[key]; + queue(key, null); + } + }, + clear: function () { + Object.keys(mem).forEach(function (k) { queue(k, null); }); + mem = {}; + // Native obsidian-web: keys are intentionally kept. + }, + key: function (n) { + var keys = Object.keys(mem); + return n >= 0 && n < keys.length ? keys[n] : null; + }, + }; + Object.defineProperty(storageShim, 'length', { + get: function () { return Object.keys(mem).length; }, + }); + + // Proxy catches direct property access (localStorage.foo = 'x' / + // localStorage['foo']) which some plugins use instead of setItem. + var proxy = new Proxy(storageShim, { + get: function (target, prop) { + if (prop in target) { + var v = target[prop]; + return typeof v === 'function' ? v.bind(target) : v; + } + if (typeof prop === 'symbol') return undefined; + var item = target.getItem(prop); + return item === null ? undefined : item; + }, + set: function (target, prop, value) { + if (typeof prop !== 'symbol') target.setItem(prop, value); + return true; + }, + deleteProperty: function (target, prop) { + if (typeof prop !== 'symbol') target.removeItem(prop); + return true; + }, + has: function (target, prop) { + return prop in target || target.getItem(prop) !== null; + }, + ownKeys: function () { + return Object.keys(mem); + }, + getOwnPropertyDescriptor: function (target, prop) { + if (typeof prop === 'symbol' || !(prop in mem)) return undefined; + return { value: mem[prop], writable: true, enumerable: true, configurable: true }; + }, + }); + + Object.defineProperty(window, 'localStorage', { + value: proxy, + configurable: true, + }); + + // Upload the migration seed after install so it can't race the map. + if (seeded > 0) { + Object.keys(seed).forEach(function (k) { queue(k, seed[k]); }); + console.log('[obsidian-web] remote localStorage: seeded ' + seeded + ' local keys to server'); + } + + console.log('[obsidian-web] remote localStorage installed (' + Object.keys(mem).length + ' keys)'); + }); + }; +})(); diff --git a/src/server/api/localstorage.js b/src/server/api/localstorage.js new file mode 100644 index 0000000..2641e15 --- /dev/null +++ b/src/server/api/localstorage.js @@ -0,0 +1,76 @@ +'use strict'; + +/** + * Server-backed localStorage store. + * + * Obsidian (and its plugins) keep state in window.localStorage — including + * the safeStorage keychain *tokens* that point at secrets in .keychain.json. + * Browser localStorage is scoped per device AND per origin, so credentials + * entered on one PC (or via one URL, e.g. LAN vs cloudflared tunnel) don't + * roam. The client shim (client/shims/remote-localstorage.js) replaces + * window.localStorage with a copy of this server-side store, making that + * state follow the server instead of the browser. + * + * This mirrors real Electron semantics: one Obsidian install = one shared + * localStorage across all vaults, persisted next to the rest of user-data. + * + * API: + * GET /api/localstorage → { key: value, ... } (full map) + * PUT /api/localstorage ← { entries: { key: value | null } } + * null deletes the key. Batched by the + * client's debounced write-through. + */ + +const express = require('express'); +const fsp = require('fs/promises'); +const path = require('path'); + +function createLocalStorageRouter(userDataPath) { + const router = express.Router(); + const storeFile = path.join(userDataPath, '.localstorage.json'); + + // Serialize writes so concurrent PUTs don't interleave load/save. + let writeChain = Promise.resolve(); + + async function load() { + try { + return JSON.parse(await fsp.readFile(storeFile, 'utf8')) || {}; + } catch (_) { + return {}; + } + } + + async function save(data) { + await fsp.mkdir(path.dirname(storeFile), { recursive: true }); + // Atomic: temp file + rename, same pattern as vault-registry. + const tmp = storeFile + '.tmp'; + await fsp.writeFile(tmp, JSON.stringify(data, null, 2), 'utf8'); + await fsp.rename(tmp, storeFile); + } + + router.get('/', async (req, res) => { + res.json(await load()); + }); + + router.put('/', express.json({ limit: '5mb' }), (req, res) => { + const entries = req.body && req.body.entries; + if (!entries || typeof entries !== 'object' || Array.isArray(entries)) { + return res.status(400).json({ error: 'entries object required' }); + } + writeChain = writeChain.then(async () => { + const data = await load(); + for (const [key, value] of Object.entries(entries)) { + if (value === null) delete data[key]; + else data[key] = String(value); + } + await save(data); + res.json({ ok: true }); + }).catch((err) => { + res.status(500).json({ error: err.message }); + }); + }); + + return router; +} + +module.exports = createLocalStorageRouter; diff --git a/src/server/index.js b/src/server/index.js index 53c3939..ec4a032 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -23,6 +23,7 @@ const createBootstrapRouter = require('./api/bootstrap'); const { warmUpBootstrapCache } = require('./api/bootstrap'); const createProxyRouter = require('./api/proxy'); const createKeytarRouter = require('./api/keytar'); +const createLocalStorageRouter = require('./api/localstorage'); const createPbkdf2Router = require('./api/pbkdf2'); const attachWatchServer = require('./api/watch'); const VaultRegistry = require('./vault-registry'); @@ -30,7 +31,13 @@ const { createAuthMiddleware } = require('./middleware/auth'); function createApp(appConfig = config) { const app = express(); - const vaultRegistry = new VaultRegistry(appConfig.registryPath); + const vaultRegistry = new VaultRegistry(appConfig.registryPath, { + // Restrict /api/vaults/open to paths under vaultsRoot (VAULTS_ROOT env, + // default user-data/). The configured boot vault is always allowed even + // if it lives elsewhere. Tests pass no vaultsRoot → unrestricted. + vaultsRoot: appConfig.vaultsRoot, + allowPaths: [appConfig.vaultPath], + }); // Compression — critical for /api/bootstrap (38MB uncompressed → ~6MB). // Brotli gives ~84% reduction, gzip ~79%. The middleware auto-selects based @@ -40,7 +47,7 @@ function createApp(appConfig = config) { // Optional TOTP auth — enabled by setting TOTP_SECRET env var. // Generate a secret: node -e "const {authenticator}=require('otplib');console.log(authenticator.generateSecret())" // Then visit /__totp-setup?token=YOUR_SECRET to scan the QR code. - const authMiddleware = createAuthMiddleware(); + const authMiddleware = createAuthMiddleware(appConfig); if (authMiddleware) { app.use(authMiddleware); console.log('[auth] TOTP authentication enabled — visit /__totp-setup?token=YOUR_SECRET to configure your authenticator app'); @@ -146,6 +153,7 @@ function createApp(appConfig = config) { // API routes. app.use('/api/keytar', createKeytarRouter(appConfig.userDataPath)); + app.use('/api/localstorage', createLocalStorageRouter(appConfig.userDataPath)); app.use('/api/pbkdf2', createPbkdf2Router()); app.use('/api/bootstrap', createBootstrapRouter(vaultRegistry, appConfig.vaultPath)); app.use('/api/proxy-request', createProxyRouter()); diff --git a/src/server/test/vaults-api.test.js b/src/server/test/vaults-api.test.js index 5550913..df9166d 100644 --- a/src/server/test/vaults-api.test.js +++ b/src/server/test/vaults-api.test.js @@ -292,3 +292,111 @@ test('starter route serves the wrapped Obsidian starter entry', async (t) => { assert.equal(response.status, 200); assert.match(await response.text(), /starter/); }); + +test('vaultsRoot restricts open to paths under the root', async (t) => { + const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), 'obsidian-web-')); + t.after(() => fsp.rm(tmp, { recursive: true, force: true })); + + const vaultsRoot = path.join(tmp, 'vaults'); + const insidePath = path.join(vaultsRoot, 'good-vault'); + const outsidePath = path.join(tmp, 'evil-vault'); + await fsp.mkdir(insidePath, { recursive: true }); + await fsp.mkdir(outsidePath); + + const bootVault = path.join(vaultsRoot, 'boot'); + await fsp.mkdir(bootVault); + const server = await startTestServer({ + clientPath: path.join(tmp, 'client'), + obsidianPath: path.join(tmp, 'obsidian'), + registryPath: path.join(tmp, 'vaults.json'), + vaultPath: bootVault, + vaultsRoot, + }); + t.after(server.close); + + // Inside the root → allowed. + const okRes = await fetch(server.baseUrl + '/api/vaults/open', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: insidePath, create: false }), + }); + assert.equal(okRes.status, 200); + assert.equal((await okRes.json()).ok, true); + + // Outside the root → rejected, even though the directory exists. + const badRes = await fetch(server.baseUrl + '/api/vaults/open', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: outsidePath, create: false }), + }); + assert.equal(badRes.status, 400); + const bad = await badRes.json(); + assert.equal(bad.ok, false); + assert.match(bad.error, /outside the allowed vaults root/); + + // Path traversal out of the root → rejected. + const traversalRes = await fetch(server.baseUrl + '/api/vaults/open', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: path.join(vaultsRoot, '..', 'evil-vault'), create: false }), + }); + assert.equal(traversalRes.status, 400); + + // The configured boot vault is allowed even when it equals the allowlist entry. + const bootRes = await fetch(server.baseUrl + '/api/vaults/open', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: bootVault, create: false }), + }); + assert.equal(bootRes.status, 200); +}); + +test('localstorage API stores, merges, and deletes keys', async (t) => { + const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), 'obsidian-web-')); + t.after(() => fsp.rm(tmp, { recursive: true, force: true })); + + const server = await startTestServer({ + clientPath: path.join(tmp, 'client'), + obsidianPath: path.join(tmp, 'obsidian'), + registryPath: path.join(tmp, 'vaults.json'), + userDataPath: tmp, + vaultPath: tmp, + }); + t.after(server.close); + + // Empty store initially. + let res = await fetch(server.baseUrl + '/api/localstorage'); + assert.equal(res.status, 200); + assert.deepEqual(await res.json(), {}); + + // Batch PUT sets keys. + res = await fetch(server.baseUrl + '/api/localstorage', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ entries: { alpha: '1', beta: 'two' } }), + }); + assert.equal(res.status, 200); + + // Second PUT merges and deletes (null). + res = await fetch(server.baseUrl + '/api/localstorage', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ entries: { beta: null, gamma: '3' } }), + }); + assert.equal(res.status, 200); + + res = await fetch(server.baseUrl + '/api/localstorage'); + assert.deepEqual(await res.json(), { alpha: '1', gamma: '3' }); + + // Malformed body rejected. + res = await fetch(server.baseUrl + '/api/localstorage', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nope: true }), + }); + assert.equal(res.status, 400); + + // Persisted to disk in userDataPath. + const onDisk = JSON.parse(await fsp.readFile(path.join(tmp, '.localstorage.json'), 'utf8')); + assert.deepEqual(onDisk, { alpha: '1', gamma: '3' }); +}); diff --git a/user-data/.gitignore b/user-data/.gitignore index 55ae369..7656ecd 100644 --- a/user-data/.gitignore +++ b/user-data/.gitignore @@ -1,5 +1,10 @@ # Runtime — contains local absolute paths to user's vaults registry.json +# Runtime — secrets and per-server state, never commit +.keychain.json +.localstorage.json +.sessions.json + # Demo vault — track content, ignore Obsidian's local state demo-vault/.obsidian/ From dab0c97ad808cfb248ec340d4195b80208e5e54e Mon Sep 17 00:00:00 2001 From: s39n Date: Fri, 12 Jun 2026 14:07:52 +0000 Subject: [PATCH 42/46] feat: add Service Worker bootstrap cache (stale-while-revalidate) Repeat visits now hit a CacheStorage entry instead of waiting for the server to return /api/bootstrap. The SW fires a background revalidation in parallel, keeping the cache fresh for the NEXT visit. - src/client/sw.js: new SW; intercepts GET /api/bootstrap (not /status), serves from cache immediately then updates in background; skipWaiting + clients.claim() for immediate takeover on install. - src/server/index.js: add GET /sw.js route with Service-Worker-Allowed: / and Cache-Control: no-cache so the SW can control the full origin and always receives updates. - src/client/boot.js: register /sw.js at the top of the IIFE, before the bootstrap fetch, so the SW is in place for subsequent navigations. --- src/client/boot.js | 11 ++++++ src/client/sw.js | 88 +++++++++++++++++++++++++++++++++++++++++++++ src/server/index.js | 31 +++++++--------- 3 files changed, 111 insertions(+), 19 deletions(-) create mode 100644 src/client/sw.js diff --git a/src/client/boot.js b/src/client/boot.js index 1d85e94..8f90d81 100644 --- a/src/client/boot.js +++ b/src/client/boot.js @@ -333,6 +333,17 @@ const OBSIDIAN_SCRIPTS = [ window.global = window; } + // Register the Service Worker for bootstrap caching (stale-while-revalidate). + // On repeat visits the SW intercepts /api/bootstrap and returns a cached + // response instantly, then revalidates in the background — no server + // round-trip needed. The SW file is served at /sw.js with + // Service-Worker-Allowed: / so it can intercept requests on the full origin. + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch(err => { + console.warn('[obsidian-web] service worker registration failed:', err.message); + }); + } + const VAULT_BASE = '/vault'; const params = new URLSearchParams(location.search); let VAULT_ID = params.get('vault') || localStorage.getItem('obsidian-web:lastVaultId') || ''; diff --git a/src/client/sw.js b/src/client/sw.js new file mode 100644 index 0000000..ab55362 --- /dev/null +++ b/src/client/sw.js @@ -0,0 +1,88 @@ +/** + * Service Worker — bootstrap cache (stale-while-revalidate) + * + * Intercepts GET /api/bootstrap requests and serves from CacheStorage, + * firing a background revalidation in parallel. On repeat visits this + * eliminates the ~200–800 ms server round-trip with a <5 ms cache hit. + * + * Cache is keyed by full URL (includes ?vault=...&full=1) so each vault + * gets its own entry. Bump CACHE_NAME to evict all entries on SW update. + */ + +const CACHE_NAME = 'ow-bootstrap-v1'; + +// ── Lifecycle ──────────────────────────────────────────────────────────────── + +self.addEventListener('install', () => { + // Skip the waiting phase so this SW takes over immediately on install, + // without needing a second page load. + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + // Evict any cache entries from older SW versions. + caches.keys() + .then(keys => Promise.all( + keys + .filter(k => k.startsWith('ow-bootstrap-') && k !== CACHE_NAME) + .map(k => { + console.log('[ow-sw] evicting old cache:', k); + return caches.delete(k); + }), + )) + // Take control of all open tabs immediately so the cache is used + // on the very next fetch, even on the page that registered us. + .then(() => self.clients.claim()), + ); +}); + +// ── Fetch interception ─────────────────────────────────────────────────────── + +self.addEventListener('fetch', (event) => { + const { request } = event; + if (request.method !== 'GET') return; + + const url = new URL(request.url); + + // Only cache /api/bootstrap — skip the /status progress-polling sub-route. + if (!url.pathname.startsWith('/api/bootstrap')) return; + if (url.pathname.includes('/status')) return; + + event.respondWith(staleWhileRevalidate(request)); +}); + +// ── Strategy ───────────────────────────────────────────────────────────────── + +async function staleWhileRevalidate(request) { + const cache = await caches.open(CACHE_NAME); + const cached = await cache.match(request); + + // Always kick off a background network fetch to keep the cache fresh. + // Don't await it — we return the cached response immediately. + const networkFetch = fetch(request) + .then(response => { + if (response.ok) { + cache.put(request, response.clone()); + console.log('[ow-sw] bootstrap cache updated:', new URL(request.url).search); + } + return response; + }) + .catch(err => { + console.warn('[ow-sw] bootstrap revalidation failed:', err.message); + return null; + }); + + if (cached) { + console.log('[ow-sw] bootstrap cache HIT:', new URL(request.url).search); + return cached; + } + + // Cache miss (first visit) — wait for the network. + console.log('[ow-sw] bootstrap cache MISS:', new URL(request.url).search); + const response = await networkFetch; + if (!response) { + throw new Error('[ow-sw] bootstrap fetch failed with no cached fallback'); + } + return response; +} diff --git a/src/server/index.js b/src/server/index.js index ec4a032..46313f3 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -134,6 +134,17 @@ function createApp(appConfig = config) { })); } + // Service Worker — served from the root path so its scope covers the whole + // origin. Service-Worker-Allowed: / is required because the file lives + // under /client/ but must control pages at /. Cache-Control: no-cache + // ensures browsers always re-fetch it so SW updates propagate promptly. + app.get('/sw.js', (req, res) => { + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Service-Worker-Allowed', '/'); + res.sendFile(path.join(appConfig.clientPath, 'sw.js')); + }); + // Worker scripts. Obsidian creates `new Worker("worker.js")` which under // Electron resolves to /Resources/obsidian/worker.js, but in a browser // it resolves relative to the document URL. Serve them at the root. @@ -180,22 +191,4 @@ function startServer(appConfig = config) { console.log('=========================================='); console.log(' Vault: ' + appConfig.vaultPath); console.log(' Obsidian: ' + appConfig.obsidianPath); - console.log(' Listening on http://' + appConfig.host + ':' + appConfig.port); - console.log('=========================================='); - - // Pre-build the bootstrap cache in the background so the first browser - // request is a cache HIT instead of a cold build. - setImmediate(() => { - warmUpBootstrapCache(app.locals.vaultRegistry, appConfig.vaultPath) - .catch((err) => console.warn('[bootstrap] warm-up error:', err.message)); - }); - }); - - return server; -} - -if (require.main === module) { - startServer(); -} - -module.exports = { createApp, startServer }; + console.log(' \ No newline at end of file From ccb44fca47446dc8702227ace0f731bcfc6e16b5 Mon Sep 17 00:00:00 2001 From: s39n Date: Mon, 15 Jun 2026 11:56:00 -0700 Subject: [PATCH 43/46] fix: complete truncated index.js (syntax error in Docker) --- src/server/index.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/server/index.js b/src/server/index.js index 46313f3..1e8ff75 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -191,4 +191,22 @@ function startServer(appConfig = config) { console.log('=========================================='); console.log(' Vault: ' + appConfig.vaultPath); console.log(' Obsidian: ' + appConfig.obsidianPath); - console.log(' \ No newline at end of file + console.log(' Listening on http://' + appConfig.host + ':' + appConfig.port); + console.log('=========================================='); + + // Pre-build the bootstrap cache in the background so the first browser + // request is a cache HIT instead of a cold build. + setImmediate(() => { + warmUpBootstrapCache(app.locals.vaultRegistry, appConfig.vaultPath) + .catch((err) => console.warn('[bootstrap] warm-up error:', err.message)); + }); + }); + + return server; +} + +if (require.main === module) { + startServer(); +} + +module.exports = { createApp, startServer }; From af07246e94eae435cb126fe6915f17989c57dbe4 Mon Sep 17 00:00:00 2001 From: s39n Date: Mon, 15 Jun 2026 14:52:22 -0700 Subject: [PATCH 44/46] perf: stale-while-revalidate for bootstrap server cache Serve existing cache immediately when dirs have changed, rebuild in background. Prevents 30-60s blocking vault scan on every refresh for large vaults. --- src/server/api/bootstrap.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/server/api/bootstrap.js b/src/server/api/bootstrap.js index 9f77fe0..241cbec 100644 --- a/src/server/api/bootstrap.js +++ b/src/server/api/bootstrap.js @@ -425,16 +425,28 @@ function createBootstrapRouter(vaultRegistry, fallbackVaultRoot) { const vault = vaultId ? vaultRegistry.get(vaultId) : null; const vaultRoot = vault ? vault.path : fallbackVaultRoot; - // If a full=1 build is needed but we only have a partial cache, serve - // the partial result immediately and kick off the full build in the background. + // Stale-while-revalidate: if ANY cached entry exists, serve it immediately + // and rebuild in the background. This prevents the browser from blocking on + // a full vault rescan (which can take 30-60s for large vaults) when the + // cache was merely invalidated by a dir mtime change (e.g. ion-sync activity). + // + // Only block on a true cold start (no entry at all). On the next request + // after the background build finishes, the client will get fresh data. const existing = serverCache.get(vaultId); let entry; - if (full && existing && !existing.isFull) { - console.log(`[bootstrap] vault=${vaultId.slice(0, 8)}… serving partial while full build runs in background`); - buildCacheEntry(vaultId, vaultRoot, vaultRegistry, true) - .catch((err) => console.warn('[bootstrap] background full build error:', err.message)); + if (existing) { + const needsFull = full && !existing.isFull; + if (needsFull) { + console.log(`[bootstrap] vault=${vaultId.slice(0, 8)}… serving partial/stale while full build runs in background`); + } else { + console.log(`[bootstrap] vault=${vaultId.slice(0, 8)}… serving stale-while-revalidate`); + } + // Fire background refresh (deduplicated via pendingBuilds — no-op if already running). + buildCacheEntry(vaultId, vaultRoot, vaultRegistry, full) + .catch((err) => console.warn('[bootstrap] background revalidation error:', err.message)); entry = existing; } else { + // Cold start — no cached entry at all. Must wait for the initial build. entry = await buildCacheEntry(vaultId, vaultRoot, vaultRegistry, full); } From 736a3d33b0b36796d7ed7f0a73edd7be546981a2 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 17 Jun 2026 15:47:32 +0000 Subject: [PATCH 45/46] feat: notify on available Obsidian renderer updates at startup Add scripts/check-obsidian-version.js, a dependency-free, offline-safe check comparing the installed renderer version (vendor/obsidian/package.json, falling back to config APP_VERSION) against the latest obsidianmd/ obsidian-releases tag. The server runs it in the background at boot and logs a notice when a newer release exists; it never downloads or applies updates (that stays the manual update-obsidian.js step). Configurable via OBSIDIAN_UPDATE_CHECK, OBSIDIAN_VERSION (pin), and OBSIDIAN_CHECK_TIMEOUT. Documented in README; CHANGELOG added. --- CHANGELOG.md | 19 +++ scripts/check-obsidian-version.js | 236 ++++++++++++++++++++++++++++++ src/server/index.js | 20 +++ 3 files changed, 275 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 scripts/check-obsidian-version.js diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..67176b1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +All notable changes to this project are documented here. The format is loosely +based on [Keep a Changelog](https://keepachangelog.com/). + +## [Unreleased] + +### Added +- **Obsidian update notifications (notify-only).** The server now checks + `obsidianmd/obsidian-releases` at startup and logs a notice when a newer + renderer is available. It never downloads or applies the update + automatically; applying stays the deliberate `node scripts/update-obsidian.js` + step. (2026-06-17) +- `scripts/check-obsidian-version.js` — dependency-free, offline-safe version + check usable both as a module (called by the server at boot) and as a CLI + (`--json` for machine output; exit code 10 when an update is available). +- Environment variables `OBSIDIAN_UPDATE_CHECK` (disable the check), + `OBSIDIAN_VERSION` (pin a version / compare without network), and + `OBSIDIAN_CHECK_TIMEOUT` (GitHub request timeout). See README. diff --git a/scripts/check-obsidian-version.js b/scripts/check-obsidian-version.js new file mode 100644 index 0000000..d6ac237 --- /dev/null +++ b/scripts/check-obsidian-version.js @@ -0,0 +1,236 @@ +#!/usr/bin/env node +'use strict'; + +/** + * check-obsidian-version.js + * + * Dependency-free "is there a newer Obsidian renderer?" check. + * + * It compares the version currently extracted into vendor/obsidian/ against the + * latest release published to the obsidianmd/obsidian-releases GitHub repo, and + * reports whether an update is available. It NEVER downloads or applies the + * update — that stays a deliberate, manual step: + * + * node scripts/update-obsidian.js # latest + * node scripts/update-obsidian.js --version X # a specific version + * + * Usage (CLI): + * node scripts/check-obsidian-version.js # human-readable status + * node scripts/check-obsidian-version.js --json # machine-readable JSON + * + * Usage (module): + * const { checkObsidianVersion } = require('./check-obsidian-version'); + * const result = await checkObsidianVersion(); + * // { installed, latest, pinned, updateAvailable, checked, reason } + * + * Environment: + * OBSIDIAN_VERSION Pin to a specific version (e.g. 1.12.7). When set, + * the check compares the installed version against the + * pin instead of GitHub's latest, and never reports a + * newer release as "available". + * OBSIDIAN_UPDATE_CHECK Set to "false"/"0"/"off" to disable the network + * check entirely (the server still calls it but it + * short-circuits). Default: enabled. + * OBSIDIAN_CHECK_TIMEOUT Network timeout in ms for the GitHub request. + * Default: 5000. + */ + +const fs = require('fs'); +const https = require('https'); +const path = require('path'); + +const PROJECT_ROOT = path.resolve(__dirname, '..'); +const VENDOR_PKG = path.join(PROJECT_ROOT, 'vendor', 'obsidian', 'package.json'); +const GITHUB_LATEST_API = + 'https://api.github.com/repos/obsidianmd/obsidian-releases/releases/latest'; +const USER_AGENT = 'obsidian-web-updater'; +const DEFAULT_TIMEOUT = 5000; + +function isCheckDisabled() { + const raw = (process.env.OBSIDIAN_UPDATE_CHECK || '').trim().toLowerCase(); + return raw === 'false' || raw === '0' || raw === 'off' || raw === 'no'; +} + +function pinnedVersion() { + const raw = (process.env.OBSIDIAN_VERSION || '').trim(); + return raw ? raw.replace(/^v/i, '') : null; +} + +/** + * Read the version actually extracted into vendor/obsidian/. Falls back to the + * server's config APP_VERSION when the vendor dir has not been populated yet + * (e.g. before the first-run download), and finally to null. + */ +function readInstalledVersion() { + try { + const pkg = JSON.parse(fs.readFileSync(VENDOR_PKG, 'utf8')); + if (pkg && pkg.version) return String(pkg.version).replace(/^v/i, ''); + } catch (_) { + /* vendor not populated yet */ + } + try { + // Lazy require so this script stays usable outside the server tree. + const cfg = require(path.join(PROJECT_ROOT, 'src', 'server', 'config')); + if (cfg && cfg.appVersion) return String(cfg.appVersion).replace(/^v/i, ''); + } catch (_) { + /* config not importable in this context */ + } + return null; +} + +/** + * Compare two dotted version strings numerically. + * Returns 1 if a > b, -1 if a < b, 0 if equal. Non-numeric / missing parts + * are treated as 0, so "1.12" === "1.12.0". + */ +function compareVersions(a, b) { + const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0); + const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0); + const len = Math.max(pa.length, pb.length); + for (let i = 0; i < len; i += 1) { + const x = pa[i] || 0; + const y = pb[i] || 0; + if (x > y) return 1; + if (x < y) return -1; + } + return 0; +} + +function fetchLatestTag(timeout) { + return new Promise((resolve, reject) => { + const req = https.get( + GITHUB_LATEST_API, + { + headers: { + Accept: 'application/vnd.github+json', + 'User-Agent': USER_AGENT, + }, + timeout, + }, + (res) => { + if ( + res.statusCode >= 300 && + res.statusCode < 400 && + res.headers.location + ) { + res.resume(); + // The latest endpoint shouldn't redirect, but follow once to be safe. + https + .get( + res.headers.location, + { headers: { Accept: 'application/vnd.github+json', 'User-Agent': USER_AGENT }, timeout }, + (r2) => collect(r2, resolve, reject), + ) + .on('error', reject); + return; + } + collect(res, resolve, reject); + }, + ); + req.on('timeout', () => req.destroy(new Error(`GitHub request timed out after ${timeout}ms`))); + req.on('error', reject); + }); +} + +function collect(res, resolve, reject) { + if (res.statusCode < 200 || res.statusCode >= 300) { + res.resume(); + reject(new Error(`GitHub API returned HTTP ${res.statusCode}`)); + return; + } + const chunks = []; + res.on('data', (c) => chunks.push(c)); + res.on('end', () => { + try { + const data = JSON.parse(Buffer.concat(chunks).toString('utf8')); + const tag = data.tag_name || data.name; + if (!tag) { + reject(new Error('GitHub response did not include a tag_name')); + return; + } + resolve(String(tag).replace(/^v/i, '')); + } catch (err) { + reject(err); + } + }); + res.on('error', reject); +} + +/** + * Perform the check. Always resolves (never throws) so callers can fire it at + * startup without guarding. Network / parse failures are reported via + * `checked: false` + `reason`. + */ +async function checkObsidianVersion(opts = {}) { + const installed = readInstalledVersion(); + const pinned = pinnedVersion(); + const timeout = Number(process.env.OBSIDIAN_CHECK_TIMEOUT) || opts.timeout || DEFAULT_TIMEOUT; + + if (isCheckDisabled()) { + return { installed, latest: null, pinned, updateAvailable: false, checked: false, reason: 'disabled' }; + } + + // Pinned mode: compare installed against the pin, no network needed. + if (pinned) { + const updateAvailable = installed ? compareVersions(pinned, installed) > 0 : true; + return { installed, latest: pinned, pinned, updateAvailable, checked: true, reason: 'pinned' }; + } + + let latest = null; + try { + latest = await fetchLatestTag(timeout); + } catch (err) { + return { + installed, + latest: null, + pinned, + updateAvailable: false, + checked: false, + reason: err.message || String(err), + }; + } + + const updateAvailable = installed ? compareVersions(latest, installed) > 0 : true; + return { installed, latest, pinned, updateAvailable, checked: true, reason: 'ok' }; +} + +function formatNotice(result) { + if (!result.checked) { + if (result.reason === 'disabled') return '[update-check] Obsidian update check disabled (OBSIDIAN_UPDATE_CHECK).'; + return `[update-check] Could not check for a newer Obsidian release: ${result.reason}`; + } + if (result.updateAvailable) { + const from = result.installed || 'unknown'; + return [ + '==========================================', + ` Obsidian update available: ${from} -> ${result.latest}`, + ' Apply it with:', + ' node scripts/update-obsidian.js', + ' node scripts/update-obsidian-mobile.js # mobile renderer', + '==========================================', + ].join('\n'); + } + return `[update-check] Obsidian renderer is up to date (${result.installed || result.latest}).`; +} + +async function main() { + const asJson = process.argv.includes('--json'); + const result = await checkObsidianVersion(); + if (asJson) { + console.log(JSON.stringify(result, null, 2)); + } else { + console.log(formatNotice(result)); + } + // Exit 0 when up to date / not checkable, 10 when an update is available, so + // CI / cron can branch on it without parsing stdout. + process.exitCode = result.updateAvailable ? 10 : 0; +} + +if (require.main === module) { + main().catch((err) => { + console.error(err.stack || err.message || String(err)); + process.exitCode = 1; + }); +} + +module.exports = { checkObsidianVersion, compareVersions, formatNotice }; diff --git a/src/server/index.js b/src/server/index.js index 1e8ff75..fec33ba 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -200,6 +200,26 @@ function startServer(appConfig = config) { warmUpBootstrapCache(app.locals.vaultRegistry, appConfig.vaultPath) .catch((err) => console.warn('[bootstrap] warm-up error:', err.message)); }); + + // Notify-only Obsidian update check. Runs in the background so it never + // blocks boot, and only logs -- applying an update stays a manual step + // (node scripts/update-obsidian.js). Disable with OBSIDIAN_UPDATE_CHECK=false. + setImmediate(() => { + let checkObsidianVersion; + let formatNotice; + try { + ({ checkObsidianVersion, formatNotice } = require('../../scripts/check-obsidian-version')); + } catch (_) { + return; // scripts/ not present (e.g. trimmed deployment) -- skip silently. + } + checkObsidianVersion() + .then((result) => { + // Log on every successful check (update available or up to date); + // stay silent when not checkable (offline/disabled) to avoid noise. + if (result.checked) console.log(formatNotice(result)); + }) + .catch((err) => console.warn('[update-check] error:', err.message)); + }); }); return server; From 28b04b9f82f392bcbf04cc2750013cb32e53700e Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 17 Jun 2026 15:48:38 +0000 Subject: [PATCH 46/46] docs: document Obsidian update-check feature and env vars in README --- README.md | 48 +++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 0d4e655..5e942d3 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,32 @@ node scripts/update-obsidian.js --no-cache The updater uses the official `obsidian-.asar.gz` release asset, verifies the SHA-256 digest when GitHub provides one, extracts it locally, validates required renderer files, then replaces `obsidian/`. +### Update notifications (auto-check, notify-only) + +The server checks for a newer Obsidian release at startup and logs a notice when one is available. It never downloads or applies the update on its own; applying stays the deliberate `node scripts/update-obsidian.js` step above. This keeps you in control of when the renderer changes while still telling you when you are behind. + +You can also run the check on demand: + +```bash +# human-readable status (exit 10 if an update is available, 0 otherwise) +node scripts/check-obsidian-version.js + +# machine-readable output for scripts/cron +node scripts/check-obsidian-version.js --json +``` + +The installed version is read from `vendor/obsidian/package.json` (falling back to the server's configured `APP_VERSION`), and compared against the latest tag on `obsidianmd/obsidian-releases`. + +Relevant environment variables: + +| Variable | Default | Effect | +|----------|---------|--------| +| `OBSIDIAN_UPDATE_CHECK` | enabled | Set to `false`/`0`/`off` to disable the startup network check entirely. | +| `OBSIDIAN_VERSION` | _(unset)_ | Pin to a version (e.g. `1.12.7`). The check then compares against the pin instead of GitHub's latest, and the check needs no network. | +| `OBSIDIAN_CHECK_TIMEOUT` | `5000` | Timeout (ms) for the GitHub request. | + +The check is non-blocking and offline-safe: if GitHub is unreachable it stays silent rather than failing boot. + ### Mobile bundle (`vendor/obsidian-mobile/`) The project ships **two runtimes** — a desktop one at `/` and a mobile one at `/mobile`. The mobile runtime needs the Obsidian Android APK bundle, extracted into `vendor/obsidian-mobile/`. Like `vendor/obsidian/`, this directory is gitignored and downloaded on demand: @@ -246,24 +272,4 @@ The Node.js server (`src/server/`) can be deployed to any Linux box. A typical s 1. Clone the repo and run `node scripts/update-obsidian.js` to get Obsidian's renderer files 2. `cd src/server && npm install && npm start` 3. Put it behind a reverse proxy (nginx, Caddy, Cloudflare Tunnel) with HTTPS -4. Do not expose the server directly to the internet without auth — there is no application-level authentication - -## Notes - -- Obsidian's extracted files are treated as third-party artifacts. Do not edit files under `vendor/obsidian/` or `vendor/obsidian-mobile/`; update wrappers/shims instead. -- The default vault is `user-data/demo-vault/`. -- The current starter folder picker is prompt-based: enter an absolute server path. -- Do not bind the server to a public IP without a tunnel or auth layer in front. -- Current architecture and roadmap are in `PLAN.md`. - -## Disclaimer - -This is an **educational proof-of-concept** exploring how Electron-based apps can run in a standard browser. It is not affiliated with, endorsed by, or associated with [Obsidian](https://obsidian.md) or Dynalist Inc. - -This repository does **not** include Obsidian's source code. The `vendor/obsidian/` and `vendor/obsidian-mobile/` directories are gitignored — users must download Obsidian's renderer themselves using the provided setup scripts. Obsidian's code remains the property of Dynalist Inc. under their [Terms of Service](https://obsidian.md/terms). - -If the Obsidian team has any concerns about this project, please [open an issue](https://github.com/MusiCode1/obsidian-web/issues) and we will address them promptly. - -## Credits - -Built by [MusiCode1](https://github.com/MusiCode1) and [Claude Code](https://claude.ai/code). +4. Do not \ No newline at end of file