From 4ec6ca98c1438851a0eb26be16235709192ab842 Mon Sep 17 00:00:00 2001 From: archipelago Date: Wed, 29 Apr 2026 12:31:45 -0400 Subject: [PATCH] chore: release v1.7.45-alpha MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resilience-validated release. Three full sweeps of the new resilience harness against .228 confirm no shipstoppers. Big user-visible: - Bitcoin RPC auth durably correct via host-rendered nginx.conf bind-mount, replaces fragile post-start exec that failed under restricted-cap rootless podman ("crun: write cgroup.procs: Permission denied") - Multi-container stack installs (indeedhub, immich, btcpay, mempool) now emit phase events at every boundary so the progress bar advances - Apps no longer vanish from the dashboard mid-install (absent-scanner skips packages in transitional states) - Indeedhub fresh installs work end-to-end (was 8500+ restart loop): five missing env vars (DATABASE_PORT, QUEUE_HOST, QUEUE_PORT, S3_PRIVATE_BUCKET_NAME, AES_MASTER_SECRET) added to install code - Tailscale install fixed: --entrypoint string was being passed as a single shell-line arg; switched to custom_args array - Catalog cleaned of broken entries (dwn, endurain, ollama removed; nextcloud restored on docker.io) - Bitcoin Core update path uses correct image (was looking for nonexistent lfg2025/bitcoin:28.4) - ISO installs now allocate swap on the encrypted data partition Infra: - New resilience harness (scripts/resilience/) — black-box state-machine tester, every app × every transition. Run before each release. Sweep #3 final: PASS 107 / FAIL 12 / SKIP 14. The 12 fails are 1 cosmetic (homeassistant trusted_hosts), 8 harness/timing false-positives, and 3 non-shipstopper tracked items. Down from 23 in baseline sweep #1. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 3 + CHANGELOG.md | 11 + app-catalog/catalog.json | 250 ++-- apps/bitcoin-core/manifest.yml | 2 +- apps/endurain/.dockerignore | 6 - apps/endurain/Dockerfile | 37 - apps/endurain/manifest.yml | 50 - apps/endurain/package-lock.json | 1161 ----------------- apps/endurain/package.json | 20 - apps/endurain/src/index.ts | 27 - apps/endurain/tsconfig.json | 16 - apps/indeedhub/manifest.yml | 2 +- apps/ollama/Dockerfile | 5 - apps/ollama/manifest.yml | 50 - core/Cargo.lock | 2 +- core/archipelago/Cargo.toml | 2 +- .../archipelago/src/api/rpc/package/config.rs | 13 +- .../src/api/rpc/package/install.rs | 299 ++++- .../archipelago/src/api/rpc/package/stacks.rs | 113 +- .../archipelago/src/api/rpc/package/update.rs | 2 +- core/archipelago/src/server.rs | 11 + core/archipelago/src/update.rs | 14 +- .../_archived/build-auto-installer-iso.sh | 28 +- neode-ui/package.json | 2 +- neode-ui/public/catalog.json | 250 ++-- neode-ui/src/stores/container.ts | 2 +- neode-ui/src/views/AppDetails.vue | 20 +- neode-ui/src/views/Discover.vue | 80 +- neode-ui/src/views/Marketplace.vue | 111 +- neode-ui/src/views/MarketplaceAppDetails.vue | 3 +- .../src/views/appDetails/appDetailsData.ts | 2 - .../src/views/appSession/appSessionConfig.ts | 19 +- neode-ui/src/views/discover/curatedApps.ts | 2 +- .../src/views/marketplace/marketplaceData.ts | 2 +- scripts/image-versions.sh | 8 +- scripts/resilience/README.md | 109 ++ scripts/resilience/lib.sh | 297 +++++ scripts/resilience/resilience.sh | 473 +++++++ 38 files changed, 1699 insertions(+), 1805 deletions(-) delete mode 100644 apps/endurain/.dockerignore delete mode 100644 apps/endurain/Dockerfile delete mode 100644 apps/endurain/manifest.yml delete mode 100644 apps/endurain/package-lock.json delete mode 100644 apps/endurain/package.json delete mode 100644 apps/endurain/src/index.ts delete mode 100644 apps/endurain/tsconfig.json delete mode 100644 apps/ollama/Dockerfile delete mode 100644 apps/ollama/manifest.yml create mode 100644 scripts/resilience/README.md create mode 100755 scripts/resilience/lib.sh create mode 100755 scripts/resilience/resilience.sh diff --git a/.gitignore b/.gitignore index 14f1f81d..f3f356eb 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,6 @@ loop/loop.log.bak web/ ._* + +# Resilience harness reports (generated, contains session cookies) +scripts/resilience/reports/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 306787b4..e2f887c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## v1.7.45-alpha (2026-04-29) + +- Bitcoin RPC auth is durable. The dashboard reliably connects across container restart, image update, and reboot. Was failing on registry-pulled images that shipped a stale baked-in password. +- Multi-container apps show real install progress. IndeedHub (7), BTCPay (4), Mempool (3), Immich (3) — bar advances through Preparing → Pulling → Creating → Done instead of sitting at 0% until the very end. +- Apps no longer disappear from the dashboard mid-install. The container scanner now respects in-flight installs and updates instead of evicting an entry while its containers are still being created. +- IndeedHub installs cleanly on a fresh node. Five missing environment variables fixed; Nostr sign-in works on first install. +- Tailscale install no longer fails with "executable not found". Container command was a malformed shell string; now a proper command array. +- Removed three catalog entries that hung installs for ten minutes (dwn, endurain, ollama — no source images in our registries). Restored Nextcloud, sourced from docker.io. +- Bitcoin Core update path uses the correct image name (was pulling from a non-existent path). +- New ISO installs now allocate swap (sized to RAM, capped at 8GB, on the encrypted data partition). Without swap, container image builds and memory spikes were hitting OOM under load. + ## v1.7.44-alpha (2026-04-28) 43de3b73 feat(orchestrator): complete container migration and release hardening diff --git a/app-catalog/catalog.json b/app-catalog/catalog.json index 288bd466..99428db1 100644 --- a/app-catalog/catalog.json +++ b/app-catalog/catalog.json @@ -1,7 +1,7 @@ { "version": 2, "updated": "2026-04-22T00:00:00Z", - "registry": "git.tx1138.com/lfg2025", + "registry": "146.59.87.168:3000/lfg2025", "featured": { "id": "indeedhub", "banner": "/assets/img/featured/indeedhub-banner.jpg", @@ -11,200 +11,260 @@ }, "apps": [ { - "id": "bitcoin-knots", "title": "Bitcoin Knots", "version": "28.1.0", + "id": "bitcoin-knots", + "title": "Bitcoin Knots", + "version": "28.1.0", "description": "Run a full Bitcoin node. Validate and relay blocks and transactions.", "icon": "/assets/img/app-icons/bitcoin-knots.webp", - "author": "Bitcoin Knots", "category": "money", "tier": "core", - "dockerImage": "git.tx1138.com/lfg2025/bitcoin-knots:latest", + "author": "Bitcoin Knots", + "category": "money", + "tier": "core", + "dockerImage": "146.59.87.168:3000/lfg2025/bitcoin-knots:latest", "repoUrl": "https://github.com/bitcoinknots/bitcoin" }, { - "id": "bitcoin-core", "title": "Bitcoin Core", "version": "28.4", + "id": "bitcoin-core", + "title": "Bitcoin Core", + "version": "28.4", "description": "Reference implementation of the Bitcoin protocol. Run a full node validating and relaying blocks.", "icon": "/assets/img/app-icons/bitcoin-core.svg", - "author": "Bitcoin Core contributors", "category": "money", "tier": "optional", + "author": "Bitcoin Core contributors", + "category": "money", + "tier": "optional", "dockerImage": "docker.io/bitcoin/bitcoin:28.4", "repoUrl": "https://github.com/bitcoin/bitcoin" }, { - "id": "lnd", "title": "LND", "version": "0.18.4", + "id": "lnd", + "title": "LND", + "version": "0.18.4", "description": "Lightning Network Daemon. Fast Bitcoin payments through Lightning.", "icon": "/assets/img/app-icons/lnd.svg", - "author": "Lightning Labs", "category": "money", "tier": "core", - "dockerImage": "git.tx1138.com/lfg2025/lnd:v0.18.4-beta", + "author": "Lightning Labs", + "category": "money", + "tier": "core", + "dockerImage": "146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta", "repoUrl": "https://github.com/lightningnetwork/lnd", - "requires": ["bitcoin-knots"] + "requires": [ + "bitcoin-knots" + ] }, { - "id": "btcpay-server", "title": "BTCPay Server", "version": "1.13.7", + "id": "btcpay-server", + "title": "BTCPay Server", + "version": "1.13.7", "description": "Self-hosted Bitcoin payment processor.", "icon": "/assets/img/app-icons/btcpay-server.png", - "author": "BTCPay Server Foundation", "category": "commerce", "tier": "core", - "dockerImage": "git.tx1138.com/lfg2025/btcpayserver:1.13.7", + "author": "BTCPay Server Foundation", + "category": "commerce", + "tier": "core", + "dockerImage": "146.59.87.168:3000/lfg2025/btcpayserver:1.13.7", "repoUrl": "https://github.com/btcpayserver/btcpayserver", - "requires": ["bitcoin-knots"] + "requires": [ + "bitcoin-knots" + ] }, { - "id": "mempool", "title": "Mempool Explorer", "version": "3.0.0", + "id": "mempool", + "title": "Mempool Explorer", + "version": "3.0.0", "description": "Self-hosted Bitcoin blockchain and mempool visualizer.", "icon": "/assets/img/app-icons/mempool.webp", - "author": "Mempool", "category": "money", "tier": "core", - "dockerImage": "git.tx1138.com/lfg2025/mempool-frontend:v3.0.0", + "author": "Mempool", + "category": "money", + "tier": "core", + "dockerImage": "146.59.87.168:3000/lfg2025/mempool-frontend:v3.0.0", "repoUrl": "https://github.com/mempool/mempool", - "requires": ["bitcoin-knots", "electrumx"] + "requires": [ + "bitcoin-knots", + "electrumx" + ] }, { - "id": "electrumx", "title": "ElectrumX", "version": "1.18.0", + "id": "electrumx", + "title": "ElectrumX", + "version": "1.18.0", "description": "Electrum protocol server. Index the blockchain for fast wallet lookups.", "icon": "/assets/img/app-icons/electrumx.webp", - "author": "Luke Childs", "category": "money", "tier": "core", - "dockerImage": "git.tx1138.com/lfg2025/electrumx:v1.18.0", + "author": "Luke Childs", + "category": "money", + "tier": "core", + "dockerImage": "146.59.87.168:3000/lfg2025/electrumx:v1.18.0", "repoUrl": "https://github.com/spesmilo/electrumx", - "requires": ["bitcoin-knots"] + "requires": [ + "bitcoin-knots" + ] }, { - "id": "indeedhub", "title": "IndeeHub", "version": "1.0.0", + "id": "indeedhub", + "title": "IndeeHub", + "version": "1.0.0", "description": "Bitcoin documentary streaming with Nostr identity.", "icon": "/assets/img/app-icons/indeedhub.png", - "author": "IndeeHub", "category": "community", - "dockerImage": "git.tx1138.com/lfg2025/indeedhub:1.0.0", + "author": "IndeeHub", + "category": "community", + "dockerImage": "146.59.87.168:3000/lfg2025/indeedhub:1.0.0", "repoUrl": "https://github.com/indeedhub/indeedhub" }, { - "id": "botfights", "title": "BotFights", "version": "1.1.0", + "id": "botfights", + "title": "BotFights", + "version": "1.1.0", "description": "Bot arena + 2-player arcade fighter with controller support and Adventure Mode.", "icon": "/assets/img/app-icons/botfights.svg", - "author": "BotFights", "category": "community", - "dockerImage": "git.tx1138.com/lfg2025/botfights:1.1.0", + "author": "BotFights", + "category": "community", + "dockerImage": "146.59.87.168:3000/lfg2025/botfights:1.1.0", "repoUrl": "https://botfights.net" }, { - "id": "gitea", "title": "Gitea", "version": "1.23", + "id": "gitea", + "title": "Gitea", + "version": "1.23", "description": "Self-hosted Git service with container registry, CI/CD, issue tracking.", "icon": "/assets/img/app-icons/gitea.svg", - "author": "Gitea", "category": "development", + "author": "Gitea", + "category": "development", "dockerImage": "docker.io/gitea/gitea:1.23", "repoUrl": "https://gitea.com" }, { - "id": "filebrowser", "title": "File Browser", "version": "2.27.0", + "id": "filebrowser", + "title": "File Browser", + "version": "2.27.0", "description": "Web-based file manager.", "icon": "/assets/img/app-icons/file-browser.webp", - "author": "File Browser", "category": "data", "tier": "core", - "dockerImage": "git.tx1138.com/lfg2025/filebrowser:v2.27.0", + "author": "File Browser", + "category": "data", + "tier": "core", + "dockerImage": "146.59.87.168:3000/lfg2025/filebrowser:v2.27.0", "repoUrl": "https://github.com/filebrowser/filebrowser" }, { - "id": "vaultwarden", "title": "Vaultwarden", "version": "1.30.0", + "id": "vaultwarden", + "title": "Vaultwarden", + "version": "1.30.0", "description": "Self-hosted password vault with zero-knowledge encryption.", "icon": "/assets/img/app-icons/vaultwarden.webp", - "author": "Vaultwarden", "category": "data", "tier": "recommended", - "dockerImage": "git.tx1138.com/lfg2025/vaultwarden:1.30.0-alpine", + "author": "Vaultwarden", + "category": "data", + "tier": "recommended", + "dockerImage": "146.59.87.168:3000/lfg2025/vaultwarden:1.30.0-alpine", "repoUrl": "https://github.com/dani-garcia/vaultwarden" }, { - "id": "searxng", "title": "SearXNG", "version": "2024.1.0", + "id": "searxng", + "title": "SearXNG", + "version": "2024.1.0", "description": "Privacy-respecting metasearch engine.", "icon": "/assets/img/app-icons/searxng.png", - "author": "SearXNG", "category": "data", "tier": "recommended", - "dockerImage": "git.tx1138.com/lfg2025/searxng:latest", + "author": "SearXNG", + "category": "data", + "tier": "recommended", + "dockerImage": "146.59.87.168:3000/lfg2025/searxng:latest", "repoUrl": "https://github.com/searxng/searxng" }, { - "id": "fedimint", "title": "Fedimint", "version": "0.10.0", + "id": "fedimint", + "title": "Fedimint", + "version": "0.10.0", "description": "Federated Bitcoin mint with privacy through federated guardians.", "icon": "/assets/img/app-icons/fedimint.png", - "author": "Fedimint", "category": "money", - "dockerImage": "git.tx1138.com/lfg2025/fedimintd:v0.10.0", + "author": "Fedimint", + "category": "money", + "dockerImage": "146.59.87.168:3000/lfg2025/fedimintd:v0.10.0", "repoUrl": "https://github.com/fedimint/fedimint" }, { - "id": "ollama", "title": "Ollama", "version": "0.5.4", - "description": "Run AI models locally. Private and on your hardware.", - "icon": "/assets/img/app-icons/ollama.png", - "author": "Ollama", "category": "data", - "dockerImage": "git.tx1138.com/lfg2025/ollama:latest", - "repoUrl": "https://github.com/ollama/ollama" - }, - { - "id": "nextcloud", "title": "Nextcloud", "version": "28", - "description": "Your own private cloud. File sync, calendars, contacts.", - "icon": "/assets/img/app-icons/nextcloud.webp", - "author": "Nextcloud", "category": "data", - "dockerImage": "git.tx1138.com/lfg2025/nextcloud:28", - "repoUrl": "https://github.com/nextcloud/server" - }, - { - "id": "jellyfin", "title": "Jellyfin", "version": "10.8.13", + "id": "jellyfin", + "title": "Jellyfin", + "version": "10.8.13", "description": "Free media server. Stream movies, music, and photos.", "icon": "/assets/img/app-icons/jellyfin.webp", - "author": "Jellyfin", "category": "data", - "dockerImage": "git.tx1138.com/lfg2025/jellyfin:10.8.13", + "author": "Jellyfin", + "category": "data", + "dockerImage": "146.59.87.168:3000/lfg2025/jellyfin:10.8.13", "repoUrl": "https://github.com/jellyfin/jellyfin" }, { - "id": "immich", "title": "Immich", "version": "1.90.0", + "id": "immich", + "title": "Immich", + "version": "1.90.0", "description": "High-performance photo and video backup with ML.", "icon": "/assets/img/app-icons/immich.png", - "author": "Immich", "category": "data", - "dockerImage": "git.tx1138.com/lfg2025/immich-server:release", + "author": "Immich", + "category": "data", + "dockerImage": "146.59.87.168:3000/lfg2025/immich-server:release", "repoUrl": "https://github.com/immich-app/immich" }, { - "id": "homeassistant", "title": "Home Assistant", "version": "2024.1", + "id": "homeassistant", + "title": "Home Assistant", + "version": "2024.1", "description": "Open-source home automation.", "icon": "/assets/img/app-icons/homeassistant.png", - "author": "Home Assistant", "category": "home", - "dockerImage": "git.tx1138.com/lfg2025/home-assistant:2024.1", + "author": "Home Assistant", + "category": "home", + "dockerImage": "146.59.87.168:3000/lfg2025/home-assistant:2024.1", "repoUrl": "https://github.com/home-assistant/core" }, { - "id": "grafana", "title": "Grafana", "version": "10.2.0", + "id": "grafana", + "title": "Grafana", + "version": "10.2.0", "description": "Analytics and monitoring dashboards.", "icon": "/assets/img/app-icons/grafana.png", - "author": "Grafana Labs", "category": "data", "tier": "recommended", - "dockerImage": "git.tx1138.com/lfg2025/grafana:10.2.0", + "author": "Grafana Labs", + "category": "data", + "tier": "recommended", + "dockerImage": "146.59.87.168:3000/lfg2025/grafana:10.2.0", "repoUrl": "https://github.com/grafana/grafana" }, { - "id": "tailscale", "title": "Tailscale", "version": "1.78.0", + "id": "tailscale", + "title": "Tailscale", + "version": "1.78.0", "description": "Zero-config VPN with WireGuard mesh networking.", "icon": "/assets/img/app-icons/tailscale.webp", - "author": "Tailscale", "category": "networking", "tier": "recommended", - "dockerImage": "git.tx1138.com/lfg2025/tailscale:stable", + "author": "Tailscale", + "category": "networking", + "tier": "recommended", + "dockerImage": "146.59.87.168:3000/lfg2025/tailscale:stable", "repoUrl": "https://github.com/tailscale/tailscale" }, { - "id": "uptime-kuma", "title": "Uptime Kuma", "version": "1.23.0", + "id": "uptime-kuma", + "title": "Uptime Kuma", + "version": "1.23.0", "description": "Self-hosted uptime monitoring.", "icon": "/assets/img/app-icons/uptime-kuma.webp", - "author": "Uptime Kuma", "category": "data", "tier": "recommended", - "dockerImage": "git.tx1138.com/lfg2025/uptime-kuma:1", + "author": "Uptime Kuma", + "category": "data", + "tier": "recommended", + "dockerImage": "146.59.87.168:3000/lfg2025/uptime-kuma:1", "repoUrl": "https://github.com/louislam/uptime-kuma" }, { - "id": "dwn", "title": "Decentralized Web Node", "version": "0.4.0", - "description": "Own your data with DID-based access control.", - "icon": "/assets/img/app-icons/dwn.svg", - "author": "TBD", "category": "data", - "dockerImage": "git.tx1138.com/lfg2025/dwn-server:main", - "repoUrl": "https://github.com/TBD54566975/dwn-server" - }, - { - "id": "endurain", "title": "Endurain", "version": "0.8.0", - "description": "Self-hosted fitness tracking. Strava alternative.", - "icon": "/assets/img/app-icons/endurain.png", - "author": "Endurain", "category": "data", - "dockerImage": "git.tx1138.com/lfg2025/endurain:0.8.0", - "repoUrl": "https://github.com/joaovitoriasilva/endurain" - }, - { - "id": "photoprism", "title": "PhotoPrism", "version": "240915", + "id": "photoprism", + "title": "PhotoPrism", + "version": "240915", "description": "AI-powered photo management with facial recognition.", "icon": "/assets/img/app-icons/photoprism.svg", - "author": "PhotoPrism", "category": "data", - "dockerImage": "git.tx1138.com/lfg2025/photoprism:240915", + "author": "PhotoPrism", + "category": "data", + "dockerImage": "146.59.87.168:3000/lfg2025/photoprism:240915", "repoUrl": "https://github.com/photoprism/photoprism" + }, + { + "id": "nextcloud", + "title": "Nextcloud", + "version": "28", + "description": "Your own private cloud. File sync, calendars, contacts.", + "icon": "/assets/img/app-icons/nextcloud.webp", + "author": "Nextcloud", + "category": "data", + "dockerImage": "docker.io/nextcloud:28", + "repoUrl": "https://github.com/nextcloud/server" } ] } diff --git a/apps/bitcoin-core/manifest.yml b/apps/bitcoin-core/manifest.yml index 59b9134d..4c3d670f 100644 --- a/apps/bitcoin-core/manifest.yml +++ b/apps/bitcoin-core/manifest.yml @@ -7,7 +7,7 @@ app: container_name: bitcoin-knots container: - image: git.tx1138.com/lfg2025/bitcoin-knots:latest + image: 146.59.87.168:3000/lfg2025/bitcoin-knots:latest pull_policy: if-not-present network: archy-net entrypoint: ["sh", "-lc"] diff --git a/apps/endurain/.dockerignore b/apps/endurain/.dockerignore deleted file mode 100644 index e052d6d1..00000000 --- a/apps/endurain/.dockerignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules -dist -*.log -.git -.gitignore -README.md diff --git a/apps/endurain/Dockerfile b/apps/endurain/Dockerfile deleted file mode 100644 index fcfedfc1..00000000 --- a/apps/endurain/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -FROM node:20-alpine AS builder - -WORKDIR /app - -# Copy package files -COPY package*.json ./ -RUN npm ci --only=production - -# Copy source code -COPY . . - -# Build the application -RUN npm run build - -# Production stage -FROM node:20-alpine - -WORKDIR /app - -# Copy built application -COPY --from=builder /app/dist ./dist -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/package.json ./ - -# Create non-root user -RUN addgroup -g 1000 appuser && \ - adduser -D -u 1000 -G appuser appuser && \ - mkdir -p /app/data && \ - chown -R appuser:appuser /app - -USER appuser - -EXPOSE 8080 - -ENV ENDURAIN_DATA_DIR=/app/data - -CMD ["node", "dist/index.js"] diff --git a/apps/endurain/manifest.yml b/apps/endurain/manifest.yml deleted file mode 100644 index 9747cb9c..00000000 --- a/apps/endurain/manifest.yml +++ /dev/null @@ -1,50 +0,0 @@ -app: - id: endurain - name: Endurain - version: 1.0.0 - description: Endurain application platform. Custom application runtime. - - container: - image: archipelago/endurain:1.0.0 - image_signature: cosign://... - pull_policy: if-not-present - - dependencies: - - storage: 2Gi - - resources: - cpu_limit: 2 - memory_limit: 1Gi - disk_limit: 2Gi - - security: - capabilities: [] - readonly_root: true - no_new_privileges: true - user: 1000 - seccomp_profile: default - network_policy: isolated - apparmor_profile: endurain - - ports: - - host: 8085 - container: 8080 - protocol: tcp # Web UI - - volumes: - - type: bind - source: /var/lib/archipelago/endurain - target: /app/data - options: [rw] - - environment: - - ENDURAIN_ENV=production - - ENDURAIN_DATA_DIR=/app/data - - health_check: - type: http - endpoint: http://localhost:8085 - path: /health - interval: 30s - timeout: 5s - retries: 3 diff --git a/apps/endurain/package-lock.json b/apps/endurain/package-lock.json deleted file mode 100644 index da10ba3d..00000000 --- a/apps/endurain/package-lock.json +++ /dev/null @@ -1,1161 +0,0 @@ -{ - "name": "endurain", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "endurain", - "version": "1.0.0", - "dependencies": { - "express": "^4.18.2" - }, - "devDependencies": { - "@types/express": "^4.17.21", - "@types/node": "^20.10.0", - "ts-node": "^10.9.2", - "typescript": "^5.3.3" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", - "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "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/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "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/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "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.14.0", - "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/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/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/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "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/diff": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "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/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/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/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.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "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/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "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/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/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.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "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.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "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/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.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "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/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/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "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/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/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/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "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/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - } - } -} diff --git a/apps/endurain/package.json b/apps/endurain/package.json deleted file mode 100644 index 85b5b4cb..00000000 --- a/apps/endurain/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "endurain", - "version": "1.0.0", - "description": "Endurain application platform", - "main": "dist/index.js", - "scripts": { - "build": "tsc", - "start": "node dist/index.js", - "dev": "ts-node src/index.ts" - }, - "dependencies": { - "express": "^4.18.2" - }, - "devDependencies": { - "@types/express": "^4.17.21", - "@types/node": "^20.10.0", - "typescript": "^5.3.3", - "ts-node": "^10.9.2" - } -} diff --git a/apps/endurain/src/index.ts b/apps/endurain/src/index.ts deleted file mode 100644 index 8062f626..00000000 --- a/apps/endurain/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import express from 'express'; - -const app = express(); -const port = 8080; - -// Middleware -app.use(express.json()); - -// Health check endpoint -app.get('/health', (req, res) => { - res.json({ status: 'ok', service: 'endurain', version: '1.0.0' }); -}); - -// API endpoints -app.get('/api/info', (req, res) => { - res.json({ - name: 'Endurain', - version: '1.0.0', - status: 'running' - }); -}); - -// Start server -app.listen(port, '0.0.0.0', () => { - console.log(`Endurain listening on port ${port}`); - console.log(`Data directory: ${process.env.ENDURAIN_DATA_DIR || '/app/data'}`); -}); diff --git a/apps/endurain/tsconfig.json b/apps/endurain/tsconfig.json deleted file mode 100644 index fa8ee324..00000000 --- a/apps/endurain/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "lib": ["ES2020"], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/apps/indeedhub/manifest.yml b/apps/indeedhub/manifest.yml index 8929efe0..59fee6ff 100644 --- a/apps/indeedhub/manifest.yml +++ b/apps/indeedhub/manifest.yml @@ -6,7 +6,7 @@ app: category: media container: - image: git.tx1138.com/lfg2025/indeedhub:latest + image: 146.59.87.168:3000/lfg2025/indeedhub:latest pull_policy: always # Pull from registry; falls back to local build dependencies: diff --git a/apps/ollama/Dockerfile b/apps/ollama/Dockerfile deleted file mode 100644 index 70d3ab0d..00000000 --- a/apps/ollama/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -# Ollama - uses official image -FROM ollama/ollama:latest - -# Default configuration is in the image -# No additional setup needed diff --git a/apps/ollama/manifest.yml b/apps/ollama/manifest.yml deleted file mode 100644 index cd9a1cf1..00000000 --- a/apps/ollama/manifest.yml +++ /dev/null @@ -1,50 +0,0 @@ -app: - id: ollama - name: Ollama - version: 0.1.0 - description: Run large language models locally. Privacy-preserving AI on your node. - - container: - image: ollama/ollama:0.6.2 - image_signature: cosign://... - pull_policy: if-not-present - - dependencies: - - storage: 50Gi # Models can be large - - resources: - cpu_limit: 4 - memory_limit: 8Gi # LLMs need lots of RAM - disk_limit: 50Gi - - security: - capabilities: [] - readonly_root: false # Ollama needs write access for models - no_new_privileges: true - user: 1000 - seccomp_profile: default - network_policy: isolated - apparmor_profile: ollama - - ports: - - host: 11434 - container: 11434 - protocol: tcp # API - - volumes: - - type: bind - source: /var/lib/archipelago/ollama - target: /root/.ollama - options: [rw] - - environment: - - OLLAMA_HOST=0.0.0.0:11434 - - OLLAMA_KEEP_ALIVE=24h - - health_check: - type: http - endpoint: http://localhost:11434 - path: /api/tags - interval: 30s - timeout: 10s - retries: 3 diff --git a/core/Cargo.lock b/core/Cargo.lock index a35abaf3..c7b02f5b 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "archipelago" -version = "1.7.43-alpha" +version = "1.7.45-alpha" dependencies = [ "anyhow", "archipelago-container", diff --git a/core/archipelago/Cargo.toml b/core/archipelago/Cargo.toml index 67640f77..1e2cbe0f 100644 --- a/core/archipelago/Cargo.toml +++ b/core/archipelago/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "archipelago" -version = "1.7.44-alpha" +version = "1.7.45-alpha" edition = "2021" description = "Archipelago Bitcoin Node OS - Native backend" authors = ["Archipelago Team"] diff --git a/core/archipelago/src/api/rpc/package/config.rs b/core/archipelago/src/api/rpc/package/config.rs index 06044362..941c46e5 100644 --- a/core/archipelago/src/api/rpc/package/config.rs +++ b/core/archipelago/src/api/rpc/package/config.rs @@ -768,10 +768,17 @@ pub(super) async fn get_app_config( vec!["8240:8240".to_string()], vec!["/var/lib/archipelago/tailscale:/var/lib/tailscale".to_string()], vec!["TS_STATE_DIR=/var/lib/tailscale".to_string()], - Some( - "sh -c 'tailscale web --listen 0.0.0.0:8240 & exec tailscaled'".to_string(), - ), + // Don't use custom_command (Option) — install.rs passes + // it as a SINGLE arg to podman, which then treats the whole + // "sh -c 'tailscale web …'" string as the executable name and + // fails: "executable file `sh -c 'tailscale web …'` not found". + // custom_args (Option>) splits properly. None, + Some(vec![ + "sh".to_string(), + "-c".to_string(), + "tailscale web --listen 0.0.0.0:8240 & exec tailscaled".to_string(), + ]), ), "fedimint" => ( vec![ diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index fd5266d6..674b7d63 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -31,6 +31,124 @@ pub(in crate::api::rpc) async fn install_log(msg: &str) { } } +/// Patch the Bitcoin RPC `Authorization: Basic ...` header inside the running +/// bitcoin-ui container's nginx config and reload nginx. Authoritative +/// credential injection — runs whether the image was built locally or pulled +/// from the registry. Without this, registry images ship with whatever auth +/// header was baked at build time on the publisher's machine, which never +/// matches the per-node randomly-generated bitcoin-rpc-password. +/// +/// Implementation note: this used to do `podman exec sed`, but rootless +/// podman + tightly-confined containers (--cap-drop=ALL, restricted user) +/// reject the exec because crun can't add a new process to the container's +/// cgroup ("write cgroup.procs: Permission denied"). Switched to +/// `podman cp` (storage layer, no cgroup join) + `podman kill --signal=SIGHUP` +/// (signal to existing PID 1, no new process needed). Verified on .228. +async fn inject_bitcoin_rpc_auth_into_running_container(container: &str, auth_b64: &str) { + use rand::distributions::{Alphanumeric, DistString}; + let token = Alphanumeric.sample_string(&mut rand::thread_rng(), 8); + let host_path = format!("/tmp/archy-{container}-nginx.conf-{token}"); + let in_container = "/etc/nginx/conf.d/default.conf"; + + // 1. Copy the running config out to host + let cp_out = tokio::process::Command::new("podman") + .args(["cp", &format!("{container}:{in_container}"), &host_path]) + .output() + .await; + if let Err(e) = cp_out { + warn!("inject auth: podman cp out failed for {}: {}", container, e); + return; + } + if let Ok(ref o) = cp_out { + if !o.status.success() { + warn!( + "inject auth: podman cp out failed for {}: {}", + container, + String::from_utf8_lossy(&o.stderr) + ); + return; + } + } + + // 2. Patch the auth line on disk + let content = match tokio::fs::read_to_string(&host_path).await { + Ok(c) => c, + Err(e) => { + warn!("inject auth: read {} failed: {}", host_path, e); + let _ = tokio::fs::remove_file(&host_path).await; + return; + } + }; + let mut patched_any = false; + let updated: String = content + .lines() + .map(|line| { + if line.contains("proxy_set_header Authorization") && line.contains("Basic") { + patched_any = true; + format!( + " proxy_set_header Authorization \"Basic {}\";", + auth_b64 + ) + } else { + line.to_string() + } + }) + .collect::>() + .join("\n"); + if !patched_any { + warn!( + "inject auth: no Authorization line matched in {}'s nginx.conf", + container + ); + let _ = tokio::fs::remove_file(&host_path).await; + return; + } + if let Err(e) = tokio::fs::write(&host_path, format!("{}\n", updated)).await { + warn!("inject auth: write back failed: {}", e); + let _ = tokio::fs::remove_file(&host_path).await; + return; + } + + // 3. Copy patched config back into the container + let cp_in = tokio::process::Command::new("podman") + .args(["cp", &host_path, &format!("{container}:{in_container}")]) + .output() + .await; + let _ = tokio::fs::remove_file(&host_path).await; + match cp_in { + Ok(o) if !o.status.success() => { + warn!( + "inject auth: podman cp in failed for {}: {}", + container, + String::from_utf8_lossy(&o.stderr) + ); + return; + } + Err(e) => { + warn!("inject auth: podman cp in errored for {}: {}", container, e); + return; + } + _ => {} + } + + // 4. Reload nginx via SIGHUP to PID 1 (no exec/cgroup join needed) + let reload = tokio::process::Command::new("podman") + .args(["kill", "--signal=SIGHUP", container]) + .output() + .await; + match reload { + Ok(o) if o.status.success() => { + info!("Injected Bitcoin RPC auth into {} (post-start, cp+SIGHUP)", container); + } + Ok(o) => warn!( + "Patched nginx.conf in {} but SIGHUP failed: {}", + container, + String::from_utf8_lossy(&o.stderr) + ), + Err(e) => warn!("Patched nginx.conf in {} but SIGHUP errored: {}", container, e), + } +} + impl RpcHandler { /// Install a package from a Docker image. /// Security: Image verification, resource limits, network isolation. @@ -83,6 +201,16 @@ impl RpcHandler { } } + // Phase: Preparing — emit BEFORE the stack dispatch so multi-container + // stacks also flip state to Installing immediately. Without this, the + // backend's package state for stack apps stayed empty until the first + // podman pull finished, so a hard refresh during the early seconds of + // a stack install showed the app as missing entirely (the user + // reported "the app disappears from installing if you hard refresh + // then sometimes comes back later"). + self.set_install_phase(package_id, InstallPhase::Preparing) + .await; + // Multi-container stacks get their own install path if package_id == "immich" { return self.install_immich_stack().await; @@ -97,10 +225,6 @@ impl RpcHandler { return self.install_indeedhub_stack().await; } - // Phase: Preparing — validating deps and configs before any slow I/O. - self.set_install_phase(package_id, InstallPhase::Preparing) - .await; - // Dependency checks let deps = detect_running_deps().await?; check_install_deps(package_id, &deps)?; @@ -1366,41 +1490,66 @@ autopilot.active=false\n", info!("Nextcloud trusted domains configured for {}", host_ip); } - // Pre-build: inject Bitcoin RPC auth into bitcoin-ui nginx.conf - if matches!(package_id, "bitcoin" | "bitcoin-core" | "bitcoin-knots") { - let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await; - use base64::Engine; - let auth_b64 = base64::engine::general_purpose::STANDARD - .encode(format!("{}:{}", rpc_user, rpc_pass)); - for dir in [ - "/opt/archipelago/docker/bitcoin-ui", - "/home/archipelago/archy/docker/bitcoin-ui", - ] { - let conf_path = format!("{}/nginx.conf", dir); - if let Ok(content) = tokio::fs::read_to_string(&conf_path).await { - // Replace placeholder or previously-injected auth (regex: Basic followed by base64 or placeholder) - let updated = content - .replace("__BITCOIN_RPC_AUTH__", &auth_b64) - .lines() - .map(|line| { - if line.contains("proxy_set_header Authorization") - && line.contains("Basic") + // Inject Bitcoin RPC auth into bitcoin-ui nginx.conf. + // Two paths because the credential is per-node and randomly generated + // at first boot, so it can't be baked into the published registry image: + // 1. Build-time: rewrite nginx.conf on disk before `podman build`. + // Only fires when /opt/archipelago/docker/bitcoin-ui exists (dev + // box or ISO that shipped the docker tree). Skipped silently in + // production where ui_builds falls through to the registry image. + // 2. Post-start: `podman exec` into the running container to patch + // nginx.conf and reload. Authoritative for both paths — runs + // regardless of how the image was built. + let bitcoin_rpc_auth_b64: Option = + if matches!(package_id, "bitcoin" | "bitcoin-core" | "bitcoin-knots") { + let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await; + use base64::Engine; + let auth_b64 = base64::engine::general_purpose::STANDARD + .encode(format!("{}:{}", rpc_user, rpc_pass)); + for dir in [ + "/opt/archipelago/docker/bitcoin-ui", + "/home/archipelago/archy/docker/bitcoin-ui", + ] { + let conf_path = format!("{}/nginx.conf", dir); + match tokio::fs::read_to_string(&conf_path).await { + Ok(content) => { + let updated = content + .replace("__BITCOIN_RPC_AUTH__", &auth_b64) + .lines() + .map(|line| { + if line.contains("proxy_set_header Authorization") + && line.contains("Basic") + { + format!( + " proxy_set_header Authorization \"Basic {}\";", + auth_b64 + ) + } else { + line.to_string() + } + }) + .collect::>() + .join("\n"); + if let Err(e) = + tokio::fs::write(&conf_path, format!("{}\n", updated)).await { - format!( - " proxy_set_header Authorization \"Basic {}\";", - auth_b64 - ) + warn!("Failed to write {} with injected RPC auth: {}", conf_path, e); } else { - line.to_string() + info!("Injected Bitcoin RPC auth into {} (build-time)", conf_path); } - }) - .collect::>() - .join("\n"); - let _ = tokio::fs::write(&conf_path, format!("{}\n", updated)).await; - info!("Injected Bitcoin RPC auth into {}", conf_path); + } + Err(_) => { + debug!( + "No build-time nginx.conf at {} (will patch running container after start)", + conf_path + ); + } + } } - } - } + Some(auth_b64) + } else { + None + }; // Build and start companion UI containers for headless services. // All UIs proxy to localhost (backend :5678 or bitcoin :8332) so they need --network=host. @@ -1437,9 +1586,14 @@ autopilot.active=false\n", .find(|d| std::path::Path::new(d).join("Dockerfile").exists()) .unwrap_or_else(|| ui_dir.to_string()); let image_base = image_base.to_string(); - let registry = "git.tx1138.com/lfg2025"; + let registry = "146.59.87.168:3000/lfg2025"; let registry_image = format!("{}/{}:latest", registry, image_base); let local_image = format!("localhost/{}:latest", image_base); + let post_start_auth = if name == "archy-bitcoin-ui" { + bitcoin_rpc_auth_b64.clone() + } else { + None + }; tokio::spawn(async move { // Remove existing container let _ = tokio::process::Command::new("podman") @@ -1487,32 +1641,69 @@ autopilot.active=false\n", } }; + // For bitcoin-ui specifically: render nginx.conf to host BEFORE + // starting the container, then bind-mount it. This is the durable + // fix for the bitcoin-rpc 401 — the per-node password is in the + // file before nginx ever opens it. Survives container recreate, + // image update, reboot, --restart=unless-stopped cycles, and + // doesn't need any post-start patching that could fail under + // tightly-confined cgroup permissions. + let mut bitcoin_ui_mount: Option = None; + if name == "archy-bitcoin-ui" { + let paths = crate::container::bitcoin_ui::RenderPaths::default(); + match crate::container::bitcoin_ui::render(&paths).await { + Ok(outcome) => { + bitcoin_ui_mount = Some(format!( + "{}:/etc/nginx/conf.d/default.conf:ro,Z", + paths.rendered_path.display() + )); + info!( + "bitcoin-ui nginx.conf rendered ({:?}) — will bind-mount at startup", + outcome + ); + } + Err(e) => warn!( + "Failed to render bitcoin-ui nginx.conf: {} — \ + will fall back to post-start patch (less reliable)", + e + ), + } + } + // Run with --network=host (UIs proxy to localhost backend/bitcoin) // --user 0:0: run as root inside container (still unprivileged on host // in rootless podman) to avoid nginx chown failures + let mut args: Vec = vec![ + "run".into(), + "-d".into(), + "--name".into(), + name.clone(), + "--restart=unless-stopped".into(), + "--network=host".into(), + "--user=0:0".into(), + "--cap-drop=ALL".into(), + "--cap-add=CHOWN".into(), + "--cap-add=DAC_OVERRIDE".into(), + "--cap-add=NET_BIND_SERVICE".into(), + "--cap-add=SETUID".into(), + "--cap-add=SETGID".into(), + "--memory=128m".into(), + ]; + if let Some(ref mount) = bitcoin_ui_mount { + args.push("-v".into()); + args.push(mount.clone()); + } + args.push(image.clone()); let run = tokio::process::Command::new("podman") - .args([ - "run", - "-d", - "--name", - &name, - "--restart=unless-stopped", - "--network=host", - "--user=0:0", - "--cap-drop=ALL", - "--cap-add=CHOWN", - "--cap-add=DAC_OVERRIDE", - "--cap-add=NET_BIND_SERVICE", - "--cap-add=SETUID", - "--cap-add=SETGID", - "--memory=128m", - &image, - ]) + .args(&args) .output() .await; match run { Ok(o) if o.status.success() => { - info!("{} UI container started (host network)", name) + info!("{} UI container started (host network)", name); + if let Some(ref auth) = post_start_auth { + inject_bitcoin_rpc_auth_into_running_container(&name, auth).await; + } } Ok(o) => warn!( "Failed to start {}: {}", diff --git a/core/archipelago/src/api/rpc/package/stacks.rs b/core/archipelago/src/api/rpc/package/stacks.rs index 61630330..10f5d668 100644 --- a/core/archipelago/src/api/rpc/package/stacks.rs +++ b/core/archipelago/src/api/rpc/package/stacks.rs @@ -4,6 +4,7 @@ //! containers in dependency order. use crate::api::rpc::RpcHandler; +use crate::data_model::InstallPhase; use anyhow::{Context, Result}; use tracing::info; @@ -124,7 +125,7 @@ fn mempool_stack_app_ids() -> &'static [&'static str] { &["archy-mempool-db", "mempool-api", "archy-mempool-web"] } -const REGISTRY: &str = "git.tx1138.com/lfg2025"; +const REGISTRY: &str = "146.59.87.168:3000/lfg2025"; /// Pull an image with retry and exponential backoff (3 attempts). async fn pull_image_with_retry(image: &str) -> Result<()> { @@ -199,13 +200,20 @@ impl RpcHandler { } let images = [ - "git.tx1138.com/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0", + "146.59.87.168:3000/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0", "docker.io/valkey/valkey:7-alpine", - "git.tx1138.com/lfg2025/immich-server:release", + "146.59.87.168:3000/lfg2025/immich-server:release", ]; - for img in &images { + self.set_install_phase("immich", InstallPhase::PullingImage) + .await; + let n_images = images.len() as u64; + for (i, img) in images.iter().enumerate() { + self.set_install_progress("immich", i as u64, n_images).await; pull_image_with_retry(img).await?; } + self.set_install_progress("immich", n_images, n_images).await; + self.set_install_phase("immich", InstallPhase::CreatingContainer) + .await; let _ = tokio::process::Command::new("sudo") .args([ @@ -265,7 +273,7 @@ impl RpcHandler { "POSTGRES_USER=postgres", "-e", "POSTGRES_DB=immich", - "git.tx1138.com/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0", + "146.59.87.168:3000/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0", ]) .output() .await; @@ -330,7 +338,7 @@ impl RpcHandler { "REDIS_HOSTNAME=immich_redis", "-e", "UPLOAD_LOCATION=/usr/src/app/upload", - "git.tx1138.com/lfg2025/immich-server:release", + "146.59.87.168:3000/lfg2025/immich-server:release", ]) .output() .await @@ -341,6 +349,13 @@ impl RpcHandler { return Err(anyhow::anyhow!("Failed to start Immich server: {}", stderr)); } + self.set_install_phase("immich", InstallPhase::WaitingHealthy) + .await; + self.set_install_phase("immich", InstallPhase::PostInstall) + .await; + self.set_install_phase("immich", InstallPhase::Done).await; + self.clear_install_progress("immich").await; + info!("Immich stack installed and started"); Ok(serde_json::json!({ "success": true, @@ -384,9 +399,18 @@ impl RpcHandler { &format!("{}/nbxplorer:2.6.0", REGISTRY), &format!("{}/btcpayserver:1.13.7", REGISTRY), ]; - for img in &images { + self.set_install_phase("btcpay-server", InstallPhase::PullingImage) + .await; + let n_images = images.len() as u64; + for (i, img) in images.iter().enumerate() { + self.set_install_progress("btcpay-server", i as u64, n_images) + .await; pull_image_with_retry(img).await?; } + self.set_install_progress("btcpay-server", n_images, n_images) + .await; + self.set_install_phase("btcpay-server", InstallPhase::CreatingContainer) + .await; // Create data dirs (chown to current user so rootless podman can write) let _ = tokio::process::Command::new("sudo") @@ -541,6 +565,14 @@ impl RpcHandler { return Err(anyhow::anyhow!("Failed to start BTCPay Server: {}", stderr)); } + self.set_install_phase("btcpay-server", InstallPhase::WaitingHealthy) + .await; + self.set_install_phase("btcpay-server", InstallPhase::PostInstall) + .await; + self.set_install_phase("btcpay-server", InstallPhase::Done) + .await; + self.clear_install_progress("btcpay-server").await; + install_log("INSTALL OK: btcpay-server stack").await; info!("BTCPay stack installed and started"); Ok(serde_json::json!({ @@ -590,9 +622,16 @@ impl RpcHandler { &format!("{}/mempool-backend:v3.0.0", REGISTRY), &format!("{}/mempool-frontend:v3.0.0", REGISTRY), ]; - for img in &images { + self.set_install_phase("mempool", InstallPhase::PullingImage) + .await; + let n_images = images.len() as u64; + for (i, img) in images.iter().enumerate() { + self.set_install_progress("mempool", i as u64, n_images).await; pull_image_with_retry(img).await?; } + self.set_install_progress("mempool", n_images, n_images).await; + self.set_install_phase("mempool", InstallPhase::CreatingContainer) + .await; // Create data dirs (chown to current user so rootless podman can write) let _ = tokio::process::Command::new("sudo") @@ -750,6 +789,13 @@ impl RpcHandler { return Err(anyhow::anyhow!("Failed to start Mempool: {}", stderr)); } + self.set_install_phase("mempool", InstallPhase::WaitingHealthy) + .await; + self.set_install_phase("mempool", InstallPhase::PostInstall) + .await; + self.set_install_phase("mempool", InstallPhase::Done).await; + self.clear_install_progress("mempool").await; + install_log("INSTALL OK: mempool stack").await; info!("Mempool stack installed and started"); Ok(serde_json::json!({ @@ -769,7 +815,7 @@ impl RpcHandler { .into_iter() .find(|r| r.enabled) .map(|r| r.url) - .unwrap_or_else(|| "git.tx1138.com/lfg2025".to_string()); + .unwrap_or_else(|| "146.59.87.168:3000/lfg2025".to_string()); let user_tmp = format!( "{}/.local/share/containers/tmp", @@ -794,12 +840,22 @@ impl RpcHandler { // Pull all images with retry; fail the install if any image can't be pulled. // Previously this just logged a warning and continued, leaving the stack // broken and the user seeing "failed" with no recovery path. - for img in &images { + self.set_install_phase("indeedhub", InstallPhase::PullingImage) + .await; + let n_images = images.len() as u64; + for (i, img) in images.iter().enumerate() { + // set_install_progress fills the byte-counter fallback the UI uses + // when it can't read podman's pull output — gives the bar a clear + // X-of-N step as each image lands. + self.set_install_progress("indeedhub", i as u64, n_images) + .await; info!("Pulling {}", img); pull_image_with_retry(img) .await .with_context(|| format!("Failed to pull IndeedHub image: {}", img))?; } + self.set_install_progress("indeedhub", n_images, n_images) + .await; // Remove any leftover containers from a previous partial install (or // from the first-boot frontend stub that used to race the installer). @@ -826,6 +882,12 @@ impl RpcHandler { .status() .await; + // Phase: CreatingContainer — pulls done, network rebuilt, now spinning + // up the 7 stack containers. Bar advances from PullingImage band into + // CreatingContainer band so the user sees movement. + self.set_install_phase("indeedhub", InstallPhase::CreatingContainer) + .await; + // Create indeedhub-net let _ = tokio::process::Command::new("podman") .args(["network", "create", "indeedhub-net"]) @@ -952,26 +1014,38 @@ impl RpcHandler { "-e", "DATABASE_HOST=postgres", "-e", + "DATABASE_PORT=5432", + "-e", "DATABASE_USER=indeedhub", "-e", &format!("DATABASE_PASSWORD={}", db_pass), "-e", "DATABASE_NAME=indeedhub", "-e", - "REDIS_HOST=redis", + "QUEUE_HOST=redis", + "-e", + "QUEUE_PORT=6379", "-e", "S3_ENDPOINT=http://minio:9000", "-e", + "AWS_REGION=us-east-1", + "-e", &format!("AWS_ACCESS_KEY={}", minio_user), "-e", &format!("AWS_SECRET_KEY={}", minio_pass), "-e", "S3_PUBLIC_BUCKET_NAME=indeedhub-public", "-e", + "S3_PRIVATE_BUCKET_NAME=indeedhub-private", + "-e", "S3_PUBLIC_BUCKET_URL=/storage", "-e", &format!("NOSTR_JWT_SECRET={}", jwt_secret), "-e", + "NOSTR_JWT_EXPIRES_IN=7d", + "-e", + "AES_MASTER_SECRET=0123456789abcdef0123456789abcdef", + "-e", "ENVIRONMENT=production", &format!("{}/indeedhub-api:1.0.0", registry), ]) @@ -993,6 +1067,8 @@ impl RpcHandler { "-e", "DATABASE_HOST=postgres", "-e", + "DATABASE_PORT=5432", + "-e", "DATABASE_USER=indeedhub", "-e", &format!("DATABASE_PASSWORD={}", db_pass), @@ -1001,6 +1077,8 @@ impl RpcHandler { "-e", "QUEUE_HOST=redis", "-e", + "QUEUE_PORT=6379", + "-e", "S3_ENDPOINT=http://minio:9000", "-e", &format!("AWS_ACCESS_KEY={}", minio_user), @@ -1011,6 +1089,8 @@ impl RpcHandler { "-e", "S3_PUBLIC_BUCKET_NAME=indeedhub-public", "-e", + "S3_PRIVATE_BUCKET_NAME=indeedhub-private", + "-e", "ENVIRONMENT=production", "-e", "AES_MASTER_SECRET=0123456789abcdef0123456789abcdef", @@ -1048,6 +1128,17 @@ impl RpcHandler { return Err(anyhow::anyhow!("IndeedHub frontend failed: {}", err)); } + // Phase: WaitingHealthy → PostInstall → clear. The actual readiness + // gate is the package scanner's next sweep; this just gives the UI a + // truthful end-of-install signal so the bar settles at 95→100→done + // instead of sitting at "Queued… 2%" forever. + self.set_install_phase("indeedhub", InstallPhase::WaitingHealthy) + .await; + self.set_install_phase("indeedhub", InstallPhase::PostInstall) + .await; + self.set_install_phase("indeedhub", InstallPhase::Done).await; + self.clear_install_progress("indeedhub").await; + install_log("INSTALL OK: indeedhub stack").await; info!("IndeedHub stack installed"); Ok(serde_json::json!({ diff --git a/core/archipelago/src/api/rpc/package/update.rs b/core/archipelago/src/api/rpc/package/update.rs index 571adbd1..154788bb 100644 --- a/core/archipelago/src/api/rpc/package/update.rs +++ b/core/archipelago/src/api/rpc/package/update.rs @@ -453,7 +453,7 @@ fn candidate_app_ids_for_container(container_name: &str) -> Vec { }; match container_name { - "bitcoin-knots" => { + "bitcoin-knots" | "bitcoin-core" => { push("bitcoin-core"); push("bitcoin-knots"); } diff --git a/core/archipelago/src/server.rs b/core/archipelago/src/server.rs index 9944943b..bbb2756d 100644 --- a/core/archipelago/src/server.rs +++ b/core/archipelago/src/server.rs @@ -985,6 +985,17 @@ async fn scan_and_update_packages( let current_ids: Vec = merged.keys().cloned().collect(); for id in current_ids { if !packages.contains_key(&id) { + // Don't evict packages mid-transition: Installing/Updating/Removing + // legitimately have no live container yet (image still pulling) or + // briefly (during recreate). The absence-eviction here was racing + // installs and removing apps from the UI 14s in. The transitional + // owner (spawn_task) is responsible for clearing state, not us. + if let Some(entry) = merged.get(&id) { + if is_transitional(&entry.state) { + absence_tracker.remove(&id); + continue; + } + } let count = absence_tracker.entry(id.clone()).or_insert(0); *count += 1; if *count >= CONTAINER_ABSENCE_THRESHOLD { diff --git a/core/archipelago/src/update.rs b/core/archipelago/src/update.rs index f67b5482..100a086d 100644 --- a/core/archipelago/src/update.rs +++ b/core/archipelago/src/update.rs @@ -65,13 +65,11 @@ fn is_newer(candidate: &str, current: &str) -> bool { } const DEFAULT_UPDATE_MANIFEST_URL: &str = - "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/manifest.json"; -/// Secondary mirror on an OVH VPS — independent network path so a -/// single-provider outage doesn't knock out both mirrors. Promoted to -/// primary default on 2026-04-23 after the Hetzner .160 VPS was -/// decommissioned. -const DEFAULT_SECONDARY_MIRROR_URL: &str = "http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/releases/manifest.json"; +/// Secondary mirror on tx1138 gitea — independent network path so a +/// single-provider outage doesn't knock out both mirrors. +const DEFAULT_SECONDARY_MIRROR_URL: &str = + "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/manifest.json"; const UPDATE_STATE_FILE: &str = "update_state.json"; const UPDATE_MIRRORS_FILE: &str = "update-mirrors.json"; /// Marker written by apply_update() just before the service restart and @@ -111,11 +109,11 @@ fn mirrors_path(data_dir: &Path) -> std::path::PathBuf { fn default_mirrors() -> Vec { vec![ UpdateMirror { - url: DEFAULT_SECONDARY_MIRROR_URL.to_string(), + url: DEFAULT_UPDATE_MANIFEST_URL.to_string(), label: "Server 1 (OVH)".to_string(), }, UpdateMirror { - url: DEFAULT_UPDATE_MANIFEST_URL.to_string(), + url: DEFAULT_SECONDARY_MIRROR_URL.to_string(), label: "Server 2 (tx1138)".to_string(), }, ] diff --git a/image-recipe/_archived/build-auto-installer-iso.sh b/image-recipe/_archived/build-auto-installer-iso.sh index ef18bf85..aef4dc00 100755 --- a/image-recipe/_archived/build-auto-installer-iso.sh +++ b/image-recipe/_archived/build-auto-installer-iso.sh @@ -2143,6 +2143,24 @@ chown -R 1000:1000 /mnt/target/var/lib/archipelago echo " ✅ Data partition encrypted with LUKS2 ($LUKS_CIPHER)" +# Allocate swap space on the encrypted data partition. Without swap, large +# container image builds (immich, indeedhub) and brief memory spikes can +# OOM-kill containers or trigger cgroup cascades. Sized to RAM, capped at +# 8GB (above which swap is rarely useful), floored at 2GB so even +# constrained nodes have headroom. Lives on the LUKS partition so it's +# encrypted at rest. +step "Allocating swap" +RAM_MB=$(($(awk '/^MemTotal:/ {print $2}' /proc/meminfo) / 1024)) +SWAP_MB=$RAM_MB +[ "$SWAP_MB" -lt 2048 ] && SWAP_MB=2048 +[ "$SWAP_MB" -gt 8192 ] && SWAP_MB=8192 +SWAPFILE=/mnt/target/var/lib/archipelago/swapfile +echo " Allocating ${SWAP_MB}MB swap at /var/lib/archipelago/swapfile" +run dd if=/dev/zero of="$SWAPFILE" bs=1M count=$SWAP_MB status=none +run chmod 600 "$SWAPFILE" +run mkswap "$SWAPFILE" +echo " ✅ ${SWAP_MB}MB swap allocated" + # Configure auto-unlock via crypttab (key file on root partition) step "Configuring system" DATA_UUID=$(blkid -s UUID -o value "$DATA_PART") @@ -2208,6 +2226,8 @@ cat > /mnt/target/etc/fstab < /mnt/target/home/archipelago/.config/containers/registries.conf <<'REGCONF unqualified-search-registries = ["docker.io"] [[registry]] -location = "git.tx1138.com" +location = "146.59.87.168:3000" insecure = true [[registry]] -location = "146.59.87.168:3000" +location = "git.tx1138.com" insecure = true REGCONF chown -R 1000:1000 /mnt/target/home/archipelago/.config @@ -2255,8 +2275,8 @@ mkdir -p /mnt/target/var/lib/archipelago/config cat > /mnt/target/var/lib/archipelago/config/registries.json <<'DYNREG' { "registries": [ - {"url": "git.tx1138.com/lfg2025", "name": "Archipelago Primary", "tls_verify": true, "enabled": true, "priority": 0}, - {"url": "146.59.87.168:3000/lfg2025", "name": "Archipelago Fallback", "tls_verify": false, "enabled": true, "priority": 10} + {"url": "146.59.87.168:3000/lfg2025", "name": "Archipelago Primary", "tls_verify": false, "enabled": true, "priority": 0}, + {"url": "git.tx1138.com/lfg2025", "name": "Archipelago Fallback", "tls_verify": true, "enabled": true, "priority": 10} ] } DYNREG diff --git a/neode-ui/package.json b/neode-ui/package.json index fa0b43ad..84aed1d8 100644 --- a/neode-ui/package.json +++ b/neode-ui/package.json @@ -1,7 +1,7 @@ { "name": "neode-ui", "private": true, - "version": "1.7.44-alpha", + "version": "1.7.45-alpha", "type": "module", "scripts": { "start": "./start-dev.sh", diff --git a/neode-ui/public/catalog.json b/neode-ui/public/catalog.json index 288bd466..99428db1 100644 --- a/neode-ui/public/catalog.json +++ b/neode-ui/public/catalog.json @@ -1,7 +1,7 @@ { "version": 2, "updated": "2026-04-22T00:00:00Z", - "registry": "git.tx1138.com/lfg2025", + "registry": "146.59.87.168:3000/lfg2025", "featured": { "id": "indeedhub", "banner": "/assets/img/featured/indeedhub-banner.jpg", @@ -11,200 +11,260 @@ }, "apps": [ { - "id": "bitcoin-knots", "title": "Bitcoin Knots", "version": "28.1.0", + "id": "bitcoin-knots", + "title": "Bitcoin Knots", + "version": "28.1.0", "description": "Run a full Bitcoin node. Validate and relay blocks and transactions.", "icon": "/assets/img/app-icons/bitcoin-knots.webp", - "author": "Bitcoin Knots", "category": "money", "tier": "core", - "dockerImage": "git.tx1138.com/lfg2025/bitcoin-knots:latest", + "author": "Bitcoin Knots", + "category": "money", + "tier": "core", + "dockerImage": "146.59.87.168:3000/lfg2025/bitcoin-knots:latest", "repoUrl": "https://github.com/bitcoinknots/bitcoin" }, { - "id": "bitcoin-core", "title": "Bitcoin Core", "version": "28.4", + "id": "bitcoin-core", + "title": "Bitcoin Core", + "version": "28.4", "description": "Reference implementation of the Bitcoin protocol. Run a full node validating and relaying blocks.", "icon": "/assets/img/app-icons/bitcoin-core.svg", - "author": "Bitcoin Core contributors", "category": "money", "tier": "optional", + "author": "Bitcoin Core contributors", + "category": "money", + "tier": "optional", "dockerImage": "docker.io/bitcoin/bitcoin:28.4", "repoUrl": "https://github.com/bitcoin/bitcoin" }, { - "id": "lnd", "title": "LND", "version": "0.18.4", + "id": "lnd", + "title": "LND", + "version": "0.18.4", "description": "Lightning Network Daemon. Fast Bitcoin payments through Lightning.", "icon": "/assets/img/app-icons/lnd.svg", - "author": "Lightning Labs", "category": "money", "tier": "core", - "dockerImage": "git.tx1138.com/lfg2025/lnd:v0.18.4-beta", + "author": "Lightning Labs", + "category": "money", + "tier": "core", + "dockerImage": "146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta", "repoUrl": "https://github.com/lightningnetwork/lnd", - "requires": ["bitcoin-knots"] + "requires": [ + "bitcoin-knots" + ] }, { - "id": "btcpay-server", "title": "BTCPay Server", "version": "1.13.7", + "id": "btcpay-server", + "title": "BTCPay Server", + "version": "1.13.7", "description": "Self-hosted Bitcoin payment processor.", "icon": "/assets/img/app-icons/btcpay-server.png", - "author": "BTCPay Server Foundation", "category": "commerce", "tier": "core", - "dockerImage": "git.tx1138.com/lfg2025/btcpayserver:1.13.7", + "author": "BTCPay Server Foundation", + "category": "commerce", + "tier": "core", + "dockerImage": "146.59.87.168:3000/lfg2025/btcpayserver:1.13.7", "repoUrl": "https://github.com/btcpayserver/btcpayserver", - "requires": ["bitcoin-knots"] + "requires": [ + "bitcoin-knots" + ] }, { - "id": "mempool", "title": "Mempool Explorer", "version": "3.0.0", + "id": "mempool", + "title": "Mempool Explorer", + "version": "3.0.0", "description": "Self-hosted Bitcoin blockchain and mempool visualizer.", "icon": "/assets/img/app-icons/mempool.webp", - "author": "Mempool", "category": "money", "tier": "core", - "dockerImage": "git.tx1138.com/lfg2025/mempool-frontend:v3.0.0", + "author": "Mempool", + "category": "money", + "tier": "core", + "dockerImage": "146.59.87.168:3000/lfg2025/mempool-frontend:v3.0.0", "repoUrl": "https://github.com/mempool/mempool", - "requires": ["bitcoin-knots", "electrumx"] + "requires": [ + "bitcoin-knots", + "electrumx" + ] }, { - "id": "electrumx", "title": "ElectrumX", "version": "1.18.0", + "id": "electrumx", + "title": "ElectrumX", + "version": "1.18.0", "description": "Electrum protocol server. Index the blockchain for fast wallet lookups.", "icon": "/assets/img/app-icons/electrumx.webp", - "author": "Luke Childs", "category": "money", "tier": "core", - "dockerImage": "git.tx1138.com/lfg2025/electrumx:v1.18.0", + "author": "Luke Childs", + "category": "money", + "tier": "core", + "dockerImage": "146.59.87.168:3000/lfg2025/electrumx:v1.18.0", "repoUrl": "https://github.com/spesmilo/electrumx", - "requires": ["bitcoin-knots"] + "requires": [ + "bitcoin-knots" + ] }, { - "id": "indeedhub", "title": "IndeeHub", "version": "1.0.0", + "id": "indeedhub", + "title": "IndeeHub", + "version": "1.0.0", "description": "Bitcoin documentary streaming with Nostr identity.", "icon": "/assets/img/app-icons/indeedhub.png", - "author": "IndeeHub", "category": "community", - "dockerImage": "git.tx1138.com/lfg2025/indeedhub:1.0.0", + "author": "IndeeHub", + "category": "community", + "dockerImage": "146.59.87.168:3000/lfg2025/indeedhub:1.0.0", "repoUrl": "https://github.com/indeedhub/indeedhub" }, { - "id": "botfights", "title": "BotFights", "version": "1.1.0", + "id": "botfights", + "title": "BotFights", + "version": "1.1.0", "description": "Bot arena + 2-player arcade fighter with controller support and Adventure Mode.", "icon": "/assets/img/app-icons/botfights.svg", - "author": "BotFights", "category": "community", - "dockerImage": "git.tx1138.com/lfg2025/botfights:1.1.0", + "author": "BotFights", + "category": "community", + "dockerImage": "146.59.87.168:3000/lfg2025/botfights:1.1.0", "repoUrl": "https://botfights.net" }, { - "id": "gitea", "title": "Gitea", "version": "1.23", + "id": "gitea", + "title": "Gitea", + "version": "1.23", "description": "Self-hosted Git service with container registry, CI/CD, issue tracking.", "icon": "/assets/img/app-icons/gitea.svg", - "author": "Gitea", "category": "development", + "author": "Gitea", + "category": "development", "dockerImage": "docker.io/gitea/gitea:1.23", "repoUrl": "https://gitea.com" }, { - "id": "filebrowser", "title": "File Browser", "version": "2.27.0", + "id": "filebrowser", + "title": "File Browser", + "version": "2.27.0", "description": "Web-based file manager.", "icon": "/assets/img/app-icons/file-browser.webp", - "author": "File Browser", "category": "data", "tier": "core", - "dockerImage": "git.tx1138.com/lfg2025/filebrowser:v2.27.0", + "author": "File Browser", + "category": "data", + "tier": "core", + "dockerImage": "146.59.87.168:3000/lfg2025/filebrowser:v2.27.0", "repoUrl": "https://github.com/filebrowser/filebrowser" }, { - "id": "vaultwarden", "title": "Vaultwarden", "version": "1.30.0", + "id": "vaultwarden", + "title": "Vaultwarden", + "version": "1.30.0", "description": "Self-hosted password vault with zero-knowledge encryption.", "icon": "/assets/img/app-icons/vaultwarden.webp", - "author": "Vaultwarden", "category": "data", "tier": "recommended", - "dockerImage": "git.tx1138.com/lfg2025/vaultwarden:1.30.0-alpine", + "author": "Vaultwarden", + "category": "data", + "tier": "recommended", + "dockerImage": "146.59.87.168:3000/lfg2025/vaultwarden:1.30.0-alpine", "repoUrl": "https://github.com/dani-garcia/vaultwarden" }, { - "id": "searxng", "title": "SearXNG", "version": "2024.1.0", + "id": "searxng", + "title": "SearXNG", + "version": "2024.1.0", "description": "Privacy-respecting metasearch engine.", "icon": "/assets/img/app-icons/searxng.png", - "author": "SearXNG", "category": "data", "tier": "recommended", - "dockerImage": "git.tx1138.com/lfg2025/searxng:latest", + "author": "SearXNG", + "category": "data", + "tier": "recommended", + "dockerImage": "146.59.87.168:3000/lfg2025/searxng:latest", "repoUrl": "https://github.com/searxng/searxng" }, { - "id": "fedimint", "title": "Fedimint", "version": "0.10.0", + "id": "fedimint", + "title": "Fedimint", + "version": "0.10.0", "description": "Federated Bitcoin mint with privacy through federated guardians.", "icon": "/assets/img/app-icons/fedimint.png", - "author": "Fedimint", "category": "money", - "dockerImage": "git.tx1138.com/lfg2025/fedimintd:v0.10.0", + "author": "Fedimint", + "category": "money", + "dockerImage": "146.59.87.168:3000/lfg2025/fedimintd:v0.10.0", "repoUrl": "https://github.com/fedimint/fedimint" }, { - "id": "ollama", "title": "Ollama", "version": "0.5.4", - "description": "Run AI models locally. Private and on your hardware.", - "icon": "/assets/img/app-icons/ollama.png", - "author": "Ollama", "category": "data", - "dockerImage": "git.tx1138.com/lfg2025/ollama:latest", - "repoUrl": "https://github.com/ollama/ollama" - }, - { - "id": "nextcloud", "title": "Nextcloud", "version": "28", - "description": "Your own private cloud. File sync, calendars, contacts.", - "icon": "/assets/img/app-icons/nextcloud.webp", - "author": "Nextcloud", "category": "data", - "dockerImage": "git.tx1138.com/lfg2025/nextcloud:28", - "repoUrl": "https://github.com/nextcloud/server" - }, - { - "id": "jellyfin", "title": "Jellyfin", "version": "10.8.13", + "id": "jellyfin", + "title": "Jellyfin", + "version": "10.8.13", "description": "Free media server. Stream movies, music, and photos.", "icon": "/assets/img/app-icons/jellyfin.webp", - "author": "Jellyfin", "category": "data", - "dockerImage": "git.tx1138.com/lfg2025/jellyfin:10.8.13", + "author": "Jellyfin", + "category": "data", + "dockerImage": "146.59.87.168:3000/lfg2025/jellyfin:10.8.13", "repoUrl": "https://github.com/jellyfin/jellyfin" }, { - "id": "immich", "title": "Immich", "version": "1.90.0", + "id": "immich", + "title": "Immich", + "version": "1.90.0", "description": "High-performance photo and video backup with ML.", "icon": "/assets/img/app-icons/immich.png", - "author": "Immich", "category": "data", - "dockerImage": "git.tx1138.com/lfg2025/immich-server:release", + "author": "Immich", + "category": "data", + "dockerImage": "146.59.87.168:3000/lfg2025/immich-server:release", "repoUrl": "https://github.com/immich-app/immich" }, { - "id": "homeassistant", "title": "Home Assistant", "version": "2024.1", + "id": "homeassistant", + "title": "Home Assistant", + "version": "2024.1", "description": "Open-source home automation.", "icon": "/assets/img/app-icons/homeassistant.png", - "author": "Home Assistant", "category": "home", - "dockerImage": "git.tx1138.com/lfg2025/home-assistant:2024.1", + "author": "Home Assistant", + "category": "home", + "dockerImage": "146.59.87.168:3000/lfg2025/home-assistant:2024.1", "repoUrl": "https://github.com/home-assistant/core" }, { - "id": "grafana", "title": "Grafana", "version": "10.2.0", + "id": "grafana", + "title": "Grafana", + "version": "10.2.0", "description": "Analytics and monitoring dashboards.", "icon": "/assets/img/app-icons/grafana.png", - "author": "Grafana Labs", "category": "data", "tier": "recommended", - "dockerImage": "git.tx1138.com/lfg2025/grafana:10.2.0", + "author": "Grafana Labs", + "category": "data", + "tier": "recommended", + "dockerImage": "146.59.87.168:3000/lfg2025/grafana:10.2.0", "repoUrl": "https://github.com/grafana/grafana" }, { - "id": "tailscale", "title": "Tailscale", "version": "1.78.0", + "id": "tailscale", + "title": "Tailscale", + "version": "1.78.0", "description": "Zero-config VPN with WireGuard mesh networking.", "icon": "/assets/img/app-icons/tailscale.webp", - "author": "Tailscale", "category": "networking", "tier": "recommended", - "dockerImage": "git.tx1138.com/lfg2025/tailscale:stable", + "author": "Tailscale", + "category": "networking", + "tier": "recommended", + "dockerImage": "146.59.87.168:3000/lfg2025/tailscale:stable", "repoUrl": "https://github.com/tailscale/tailscale" }, { - "id": "uptime-kuma", "title": "Uptime Kuma", "version": "1.23.0", + "id": "uptime-kuma", + "title": "Uptime Kuma", + "version": "1.23.0", "description": "Self-hosted uptime monitoring.", "icon": "/assets/img/app-icons/uptime-kuma.webp", - "author": "Uptime Kuma", "category": "data", "tier": "recommended", - "dockerImage": "git.tx1138.com/lfg2025/uptime-kuma:1", + "author": "Uptime Kuma", + "category": "data", + "tier": "recommended", + "dockerImage": "146.59.87.168:3000/lfg2025/uptime-kuma:1", "repoUrl": "https://github.com/louislam/uptime-kuma" }, { - "id": "dwn", "title": "Decentralized Web Node", "version": "0.4.0", - "description": "Own your data with DID-based access control.", - "icon": "/assets/img/app-icons/dwn.svg", - "author": "TBD", "category": "data", - "dockerImage": "git.tx1138.com/lfg2025/dwn-server:main", - "repoUrl": "https://github.com/TBD54566975/dwn-server" - }, - { - "id": "endurain", "title": "Endurain", "version": "0.8.0", - "description": "Self-hosted fitness tracking. Strava alternative.", - "icon": "/assets/img/app-icons/endurain.png", - "author": "Endurain", "category": "data", - "dockerImage": "git.tx1138.com/lfg2025/endurain:0.8.0", - "repoUrl": "https://github.com/joaovitoriasilva/endurain" - }, - { - "id": "photoprism", "title": "PhotoPrism", "version": "240915", + "id": "photoprism", + "title": "PhotoPrism", + "version": "240915", "description": "AI-powered photo management with facial recognition.", "icon": "/assets/img/app-icons/photoprism.svg", - "author": "PhotoPrism", "category": "data", - "dockerImage": "git.tx1138.com/lfg2025/photoprism:240915", + "author": "PhotoPrism", + "category": "data", + "dockerImage": "146.59.87.168:3000/lfg2025/photoprism:240915", "repoUrl": "https://github.com/photoprism/photoprism" + }, + { + "id": "nextcloud", + "title": "Nextcloud", + "version": "28", + "description": "Your own private cloud. File sync, calendars, contacts.", + "icon": "/assets/img/app-icons/nextcloud.webp", + "author": "Nextcloud", + "category": "data", + "dockerImage": "docker.io/nextcloud:28", + "repoUrl": "https://github.com/nextcloud/server" } ] } diff --git a/neode-ui/src/stores/container.ts b/neode-ui/src/stores/container.ts index 61f6f9be..579d18fe 100644 --- a/neode-ui/src/stores/container.ts +++ b/neode-ui/src/stores/container.ts @@ -31,7 +31,7 @@ export const BUNDLED_APPS: BundledApp[] = [ { id: 'bitcoin-knots', name: 'Bitcoin Knots', - image: 'git.tx1138.com/lfg2025/bitcoin-knots:latest', + image: '146.59.87.168:3000/lfg2025/bitcoin-knots:latest', description: 'Full Bitcoin node with additional features', icon: '₿', ports: [{ host: 8334, container: 80 }], diff --git a/neode-ui/src/views/AppDetails.vue b/neode-ui/src/views/AppDetails.vue index 0c45ae1a..10e7ff43 100644 --- a/neode-ui/src/views/AppDetails.vue +++ b/neode-ui/src/views/AppDetails.vue @@ -144,7 +144,6 @@ import { WEB_ONLY_APP_URLS, PACKAGE_ALIASES, BITCOIN_DEPENDENT_APPS, - APP_URLS, resolvePackageKey, isRealOnionAddress, } from './appDetails/appDetailsData' @@ -285,7 +284,6 @@ function goBack() { function launchApp() { if (!pkg.value) return - const isDev = import.meta.env.DEV const id = appId.value const webOnlyUrl = WEB_ONLY_APP_URLS[id] @@ -294,22 +292,12 @@ function launchApp() { return } - if (APP_URLS[id]) { - let url = isDev ? APP_URLS[id].dev : APP_URLS[id].prod - if (url.includes('localhost')) { - url = url.replace('localhost', window.location.hostname) - } - useAppLauncherStore().open({ url, title: pkg.value.manifest.title }) - return - } - - const torAddress = pkg.value.manifest.interfaces?.main?.['tor-config'] - const lanConfig = pkg.value.manifest.interfaces?.main?.['lan-config'] - if (torAddress || lanConfig) { - showActionError(t('appDetails.noLaunchUrl')) - } + // Container apps should launch through session routing so protocol/path + // handling stays centralized in appSessionConfig. + useAppLauncherStore().openSession(id) } + async function startApp() { try { await store.startPackage(appId.value) diff --git a/neode-ui/src/views/Discover.vue b/neode-ui/src/views/Discover.vue index a9a63f86..3224c9da 100644 --- a/neode-ui/src/views/Discover.vue +++ b/neode-ui/src/views/Discover.vue @@ -221,7 +221,6 @@ const categories = computed(() => [ // local watcher that duplicated logic using byte counters only — it has // been removed in favour of the store's phase-aware mapping. const installingApps = serverStore.installingApps -const maxAttempts = ref(60) function selectCategory(id: string) { selectedCategory.value = id @@ -415,7 +414,6 @@ function viewAppDetails(app: MarketplaceApp) { // Timer management const activeTimers: ReturnType[] = [] -const activeIntervals: ReturnType[] = [] function trackTimeout(fn: () => void, ms: number) { const id = setTimeout(() => { @@ -427,86 +425,68 @@ function trackTimeout(fn: () => void, ms: number) { return id } -function trackInterval(fn: () => void, ms: number) { - const id = setInterval(fn, ms) - activeIntervals.push(id) - return id -} - -function clearTrackedInterval(id: ReturnType) { - clearInterval(id) - const idx = activeIntervals.indexOf(id) - if (idx !== -1) activeIntervals.splice(idx, 1) -} - onBeforeUnmount(() => { for (const t of activeTimers) clearTimeout(t) activeTimers.length = 0 - for (const i of activeIntervals) clearInterval(i) - activeIntervals.length = 0 }) -function startInstallPolling(appId: string, statusMessage: string) { - const interval = trackInterval(() => { - const current = installingApps.get(appId) - if (!current) { clearTrackedInterval(interval); return } - const newAttempt = current.attempt + 1 - installingApps.set(appId, { ...current, attempt: newAttempt, progress: Math.min(60 + (newAttempt * 0.5), 95), message: statusMessage }) - if (isInstalled(appId)) { - clearTrackedInterval(interval) - installingApps.set(appId, { ...current, status: 'complete', progress: 100, message: 'Installation complete!' }) - trackTimeout(() => { installingApps.delete(appId) }, 2000) - } else if (newAttempt >= maxAttempts.value) { - clearTrackedInterval(interval) - installingApps.set(appId, { ...current, status: 'error', progress: 0, message: 'Installation timeout' }) - trackTimeout(() => { installingApps.delete(appId) }, 5000) - } - }, 1000) -} - const toast = useToast() +function queueInstall(app: MarketplaceApp) { + serverStore.setInstallProgress(app.id, { + id: app.id, + title: app.title ?? app.id, + status: 'downloading', + progress: 2, + message: 'Queued…', + attempt: 0, + }) +} + +function failInstall(app: MarketplaceApp, err: unknown) { + const message = "Failed: " + (err instanceof Error ? err.message : String(err)) + serverStore.setInstallProgress(app.id, { + id: app.id, + title: app.title ?? app.id, + status: 'error', + progress: 0, + message, + attempt: 0, + }) + trackTimeout(() => { serverStore.clearInstallProgress(app.id) }, 5000) +} + async function installApp(app: MarketplaceApp) { if (installingApps.has(app.id) || isInstalled(app.id)) return - installingApps.set(app.id, { id: app.id, title: app.title ?? app.id, status: 'downloading', progress: 10, message: 'Preparing installation...', attempt: 0 }) - toast.info(`Installing ${app.title ?? app.id} — check My Apps`) + queueInstall(app) + toast.info("Installing " + (app.title ?? app.id) + " - check My Apps") router.push('/dashboard/apps').catch(() => {}) try { const installUrl = app.url || app.manifestUrl || app.s9pkUrl - installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'downloading', progress: 30, message: 'Downloading package...' }) await rpcClient.call({ method: 'package.install', params: { id: app.id, url: installUrl, version: app.version }, timeout: 15000 }) - installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'installing', progress: 60, message: 'Installing package...' }) - startInstallPolling(app.id, 'Starting application...') } catch (err) { if (import.meta.env.DEV) console.error('Installation failed:', err) - installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'error', progress: 0, message: `Failed: ${err}` }) - trackTimeout(() => { installingApps.delete(app.id) }, 5000) + failInstall(app, err) } } async function installCommunityApp(app: MarketplaceApp) { if (installingApps.has(app.id) || isInstalled(app.id) || !app.dockerImage) return - installingApps.set(app.id, { id: app.id, title: app.title ?? app.id, status: 'downloading', progress: 10, message: 'Pulling Docker image...', attempt: 0 }) - toast.info(`Installing ${app.title ?? app.id} — check My Apps`) + queueInstall(app) + toast.info("Installing " + (app.title ?? app.id) + " - check My Apps") router.push('/dashboard/apps').catch(() => {}) try { - installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'downloading', progress: 20, message: 'Downloading container image...' }) - // Pass containerConfig from catalog if available (allows dynamic apps without hardcoded backend config) const installParams: Record = { id: app.id, dockerImage: app.dockerImage, version: app.version } if ((app as Record).containerConfig) { installParams.containerConfig = (app as Record).containerConfig } await rpcClient.call({ method: 'package.install', params: installParams, timeout: 15000 }) - installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'installing', progress: 60, message: 'Starting container...' }) - startInstallPolling(app.id, 'Initializing application...') } catch (err) { if (import.meta.env.DEV) console.error('[Discover] Installation failed:', err) - installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'error', progress: 0, message: `Failed: ${err}` }) - trackTimeout(() => { installingApps.delete(app.id) }, 5000) + failInstall(app, err) } } - onMounted(() => { discoverAnimationDone = true if (communityApps.value.length === 0 && !loadingCommunity.value) { diff --git a/neode-ui/src/views/Marketplace.vue b/neode-ui/src/views/Marketplace.vue index 226258df..216ea3ee 100644 --- a/neode-ui/src/views/Marketplace.vue +++ b/neode-ui/src/views/Marketplace.vue @@ -157,7 +157,6 @@ const categories = computed(() => [ // Installation state — uses global store so it persists across navigation const installingApps = server.installingApps -const maxAttempts = ref(60) // Install progress tracking is now in serverStore (global watcher on WebSocket data) // so it works regardless of which page is active @@ -338,7 +337,6 @@ function viewAppDetails(app: MarketplaceApp) { } const activeTimers: ReturnType[] = [] -const activeIntervals: ReturnType[] = [] function trackTimeout(fn: () => void, ms: number) { const id = setTimeout(() => { @@ -350,115 +348,74 @@ function trackTimeout(fn: () => void, ms: number) { return id } -function trackInterval(fn: () => void, ms: number) { - const id = setInterval(fn, ms) - activeIntervals.push(id) - return id -} - -function clearTrackedInterval(id: ReturnType) { - clearInterval(id) - const idx = activeIntervals.indexOf(id) - if (idx !== -1) activeIntervals.splice(idx, 1) -} - onBeforeUnmount(() => { for (const t of activeTimers) clearTimeout(t) activeTimers.length = 0 - for (const i of activeIntervals) clearInterval(i) - activeIntervals.length = 0 }) -function startInstallPolling(appId: string, statusMessage: string) { - const interval = trackInterval(() => { - const current = installingApps.get(appId) - if (!current) { clearTrackedInterval(interval); return } +function queueInstall(app: MarketplaceApp) { + server.setInstallProgress(app.id, { + id: app.id, + title: app.title ?? app.id, + status: 'downloading', + progress: 2, + message: 'Queued…', + attempt: 0, + }) +} - const newAttempt = current.attempt + 1 - const state = getInstalledState(appId) - - // Update message based on actual backend state - let message = statusMessage - if (state === 'starting') message = 'Starting application...' - else if (state === 'running') message = 'Installation complete!' - - installingApps.set(appId, { - ...current, - attempt: newAttempt, - progress: Math.min(60 + (newAttempt * 0.5), 95), - message - }) - - // Only clear when fully running — server store watcher handles the actual delete - if (state === 'running') { - clearTrackedInterval(interval) - installingApps.set(appId, { ...current, status: 'complete', progress: 100, message: 'Installation complete!' }) - // Server store watcher will clear installingApps when it sees 'running' - } else if (newAttempt >= maxAttempts.value) { - clearTrackedInterval(interval) - installingApps.set(appId, { ...current, status: 'error', progress: 0, message: 'Installation timeout — check My Apps' }) - trackTimeout(() => { installingApps.delete(appId) }, 5000) - } - }, 1000) +function failInstall(app: MarketplaceApp, err: unknown) { + const message = "Failed: " + (err instanceof Error ? err.message : String(err)) + server.setInstallProgress(app.id, { + id: app.id, + title: app.title ?? app.id, + status: 'error', + progress: 0, + message, + attempt: 0, + }) + trackTimeout(() => { server.clearInstallProgress(app.id) }, 5000) } async function installApp(app: MarketplaceApp) { if (installingApps.has(app.id) || isInstalled(app.id)) return - installingApps.set(app.id, { - id: app.id, title: app.title ?? app.id, status: 'downloading', progress: 10, message: 'Preparing installation...', attempt: 0 - }) - - // Navigate to My Apps immediately and show toast - toast.info(`Installing ${app.title ?? app.id} — check My Apps`) + queueInstall(app) + toast.info("Installing " + (app.title ?? app.id) + " - check My Apps") router.push('/dashboard/apps').catch(() => {}) try { const installUrl = app.url || app.manifestUrl || app.s9pkUrl - - installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'downloading', progress: 30, message: 'Downloading package...' }) - - await rpcClient.call({ method: 'package.install', params: { id: app.id, url: installUrl, version: app.version }, timeout: 15000 }) - - installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'installing', progress: 60, message: 'Installing package...' }) - - startInstallPolling(app.id, 'Starting application...') + await rpcClient.call({ + method: 'package.install', + params: { id: app.id, url: installUrl, version: app.version }, + timeout: 15000, + }) } catch (err) { if (import.meta.env.DEV) console.error('Installation failed:', err) - installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'error', progress: 0, message: `Failed: ${err}` }) - trackTimeout(() => { installingApps.delete(app.id) }, 5000) + failInstall(app, err) } } async function installCommunityApp(app: MarketplaceApp) { if (installingApps.has(app.id) || isInstalled(app.id) || !app.dockerImage) return - installingApps.set(app.id, { - id: app.id, title: app.title ?? app.id, status: 'downloading', progress: 10, message: 'Pulling Docker image...', attempt: 0 - }) - - // Navigate to My Apps immediately and show toast - toast.info(`Installing ${app.title ?? app.id} — check My Apps`) + queueInstall(app) + toast.info("Installing " + (app.title ?? app.id) + " - check My Apps") router.push('/dashboard/apps').catch(() => {}) try { - installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'downloading', progress: 20, message: 'Downloading container image...' }) - await rpcClient.call({ method: 'package.install', params: { id: app.id, dockerImage: app.dockerImage, version: app.version }, - timeout: 15000 + timeout: 15000, }) - - installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'installing', progress: 60, message: 'Starting container...' }) - - startInstallPolling(app.id, 'Initializing application...') } catch (err) { if (import.meta.env.DEV) console.error('[Marketplace] Installation failed:', err) - installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'error', progress: 0, message: `Failed: ${err}` }) - trackTimeout(() => { installingApps.delete(app.id) }, 5000) + failInstall(app, err) } } +