diff --git a/.github/workflows/ci.yml b/.forgejo/workflows/ci.yml similarity index 61% rename from .github/workflows/ci.yml rename to .forgejo/workflows/ci.yml index 7dabcc4..82ee799 100644 --- a/.github/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -12,35 +12,26 @@ permissions: jobs: test: name: Test - runs-on: ubuntu-latest - strategy: - matrix: - go-version: ["1.25"] + runs-on: docker + container: + image: docker.io/library/golang:1.25 steps: - - uses: actions/checkout@v6 - - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version: ${{ matrix.go-version }} + - uses: actions/checkout@v4 - name: Run tests run: go test -v -race -count=1 ./... build: name: Build - runs-on: ubuntu-latest + runs-on: docker + container: + image: docker.io/library/golang:1.25 strategy: matrix: goos: [linux, darwin, windows] goarch: [amd64, arm64] steps: - - uses: actions/checkout@v6 - - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version: "1.25" + - uses: actions/checkout@v4 - name: Build env: @@ -50,30 +41,30 @@ jobs: lint: name: Lint - runs-on: ubuntu-latest + runs-on: docker + container: + image: docker.io/library/golang:1.25 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version: "1.25" + - name: Install golangci-lint + run: | + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/v2.11.4/install.sh \ + | sh -s -- -b /usr/local/bin v2.11.4 - name: Run golangci-lint - uses: golangci/golangci-lint-action@v9 - with: - version: v2.11.4 + run: golangci-lint run ./... coverage: name: Coverage - runs-on: ubuntu-latest + runs-on: docker + container: + image: docker.io/library/golang:1.25 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version: "1.25" + - name: Install python3 + run: apt-get update && apt-get install -y --no-install-recommends python3 - name: Run tests with coverage (all packages) run: | @@ -102,24 +93,13 @@ jobs: print('OK: Coverage meets minimum threshold') " - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v6 - with: - files: ./coverage.out - fail_ci_if_error: false - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - vet: name: Vet - runs-on: ubuntu-latest + runs-on: docker + container: + image: docker.io/library/golang:1.25 steps: - - uses: actions/checkout@v6 - - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version: "1.25" + - uses: actions/checkout@v4 - name: Run go vet run: go vet ./... diff --git a/.forgejo/workflows/docker-rebuild.yml b/.forgejo/workflows/docker-rebuild.yml new file mode 100644 index 0000000..34cc3d6 --- /dev/null +++ b/.forgejo/workflows/docker-rebuild.yml @@ -0,0 +1,61 @@ +# Rebuilds and re-pushes the `latest` image without a version bump so newly +# *fixed* Alpine / ffmpeg / Go patches land between tagged releases. Versioned +# tags are immutable and never touched here. Runs weekly and on demand. +name: Docker rebuild + +on: + schedule: + # Mondays 04:17 UTC (off the hour to avoid the scheduler rush) + - cron: "17 4 * * 1" + workflow_dispatch: + +jobs: + rebuild: + runs-on: docker + container: + image: docker.io/library/docker:27-cli + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install build deps + run: apk add --no-cache curl git bash + + - name: Install buildx + run: | + mkdir -p ~/.docker/cli-plugins + curl -sSL https://github.com/docker/buildx/releases/latest/download/buildx-linux-amd64 \ + -o ~/.docker/cli-plugins/docker-buildx + chmod +x ~/.docker/cli-plugins/docker-buildx + + - name: Set up qemu + run: docker run --rm --privileged tonistiigi/binfmt --install all + + # Stamp the binary with the most recent release tag (not "dev"). + - name: Resolve version + id: ver + run: | + v=$(git describe --tags --abbrev=0 2>/dev/null || echo dev) + echo "version=$v" >> "$GITHUB_OUTPUT" + + - name: Login to Docker Hub + env: + DH_USER: ${{ secrets.DOCKERHUB_USERNAME }} + DH_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + run: echo "$DH_TOKEN" | docker login -u "$DH_USER" --password-stdin + + - name: Build + push (refresh latest) + env: + VERSION: ${{ steps.ver.outputs.version }} + run: | + docker buildx create --name builder --use --driver docker-container + # Refresh the floating tag only — never overwrite a versioned release. + # Force a fresh base pull so apk upgrade picks up new patches. + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --build-arg "VERSION=$VERSION" \ + --tag "torrentclaw/unarr:latest" \ + --no-cache \ + --push \ + . diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..d757612 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,118 @@ +name: Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: write + +jobs: + release: + runs-on: docker + container: + image: docker.io/library/golang:1.25 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install build deps (bash, curl, jq, ffmpeg fetch deps) + run: | + apt-get update + apt-get install -y --no-install-recommends bash curl ca-certificates jq xz-utils unzip + + - name: Install goreleaser + run: | + curl -sSfL https://github.com/goreleaser/goreleaser/releases/latest/download/goreleaser_Linux_x86_64.tar.gz \ + | tar -xz -C /usr/local/bin goreleaser + + - name: Run goreleaser + env: + # Forgejo runner auto-injects GITHUB_TOKEN (a per-job, instance-scoped + # token usable against the Forgejo REST API). goreleaser only accepts + # one token; with both GITHUB_TOKEN + GITEA_TOKEN set it errors out + # ("multiple tokens"). Unset GITHUB_TOKEN before invoking goreleaser so + # it picks the Gitea code path + the gitea_urls block in .goreleaser.yml. + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + # Empty when RELEASE_SIGNING_PUBKEY variable is unset — goreleaser + # accepts it and the resulting binary disables signature checks + # (back-compat: pre-signing releases continue to update). Set + # RELEASE_SIGNING_PUBKEY (variable) + RELEASE_SIGNING_KEY (secret) + # to turn verification on. + RELEASE_SIGNING_PUBKEY: ${{ vars.RELEASE_SIGNING_PUBKEY }} + run: | + unset GITHUB_TOKEN + goreleaser release --clean + + - name: Sign checksums.txt with ed25519 + if: ${{ vars.RELEASE_SIGNING_PUBKEY != '' && secrets.RELEASE_SIGNING_KEY != '' }} + env: + RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }} + RELEASE_TAG: ${{ github.ref_name }} + FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Tailscale IP — domain-agnostic; the runner shares the dokploy-network with + # forgejo (hostname `forgejo`), so the in-cluster hostname is fastest, but the + # Tailscale IP is the documented fallback. + FORGEJO_API: http://forgejo:3000/api/v1 + REPO: torrentclaw/unarr + run: | + set -euo pipefail + go run ./scripts/sign-checksums \ + -key "$RELEASE_SIGNING_KEY" \ + -in dist/checksums.txt \ + -out dist/checksums.txt.sig + + # Find the release ID for this tag, then upload the sig as an asset. + rel_id=$(curl -sSf "$FORGEJO_API/repos/$REPO/releases/tags/$RELEASE_TAG" \ + -H "Authorization: token $FORGEJO_TOKEN" | jq -r '.id') + curl -sSf -X POST \ + "$FORGEJO_API/repos/$REPO/releases/$rel_id/assets?name=checksums.txt.sig" \ + -H "Authorization: token $FORGEJO_TOKEN" \ + -F "attachment=@dist/checksums.txt.sig" + + docker: + needs: release + runs-on: docker + container: + # Docker-in-Docker capable image — buildx + qemu pre-installed. + image: docker.io/library/docker:27-cli + steps: + - uses: actions/checkout@v4 + + - name: Install buildx + run: | + apk add --no-cache curl + mkdir -p ~/.docker/cli-plugins + curl -sSL https://github.com/docker/buildx/releases/latest/download/buildx-linux-amd64 \ + -o ~/.docker/cli-plugins/docker-buildx + chmod +x ~/.docker/cli-plugins/docker-buildx + + - name: Login to Docker Hub + env: + DH_USER: ${{ secrets.DOCKERHUB_USERNAME }} + DH_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + run: echo "$DH_TOKEN" | docker login -u "$DH_USER" --password-stdin + + - name: Set up qemu + run: docker run --rm --privileged tonistiigi/binfmt --install all + + - name: Build + push multi-arch image + env: + VERSION: ${{ github.ref_name }} + run: | + set -euo pipefail + VERSION_SEMVER="${VERSION#v}" + MAJOR_MINOR="${VERSION_SEMVER%.*}" + docker buildx create --name builder --use --driver docker-container + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --build-arg "VERSION=$VERSION" \ + --tag "torrentclaw/unarr:$VERSION_SEMVER" \ + --tag "torrentclaw/unarr:$MAJOR_MINOR" \ + --tag "torrentclaw/unarr:latest" \ + --push \ + . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 8283150..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,163 +0,0 @@ -name: Release - -on: - push: - tags: - - "v*" - -permissions: - contents: write - -jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - uses: actions/setup-go@v6 - with: - go-version-file: go.mod - - - uses: goreleaser/goreleaser-action@v6 - with: - version: "~> v2" - args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - - docker: - needs: release - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v6 - with: - images: torrentclaw/unarr - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,value=latest - - - uses: docker/setup-qemu-action@v4 - - uses: docker/setup-buildx-action@v4 - - - uses: docker/login-action@v4 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - uses: docker/build-push-action@v7 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - build-args: | - VERSION=${{ github.ref_name }} - - - virustotal: - needs: release - runs-on: ubuntu-latest - if: vars.VT_ENABLED == 'true' - steps: - - name: Get release tag - id: tag - run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" - - - name: Download release assets - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - mkdir -p assets - gh release download "${{ steps.tag.outputs.tag }}" \ - --repo "${{ github.repository }}" \ - --dir assets \ - --pattern '*.tar.gz' \ - --pattern '*.zip' \ - --pattern 'checksums.txt' - - - name: Scan assets with VirusTotal - env: - VT_API_KEY: ${{ secrets.VT_API_KEY }} - run: | - mkdir -p results - for file in assets/*; do - filename=$(basename "$file") - echo "Uploading $filename to VirusTotal..." - - response=$(curl -s --request POST \ - --url https://www.virustotal.com/api/v3/files \ - --header "x-apikey: $VT_API_KEY" \ - --form "file=@$file") - - analysis_id=$(echo "$response" | jq -r '.data.id // empty') - if [ -z "$analysis_id" ]; then - echo "::warning::Failed to upload $filename: $response" - continue - fi - - echo "$filename=$analysis_id" >> results/scans.txt - echo " Analysis ID: $analysis_id" - - # Rate limit: VT free tier allows 4 req/min - sleep 16 - done - - - name: Wait for analysis completion - env: - VT_API_KEY: ${{ secrets.VT_API_KEY }} - run: | - echo "Waiting 60s for VirusTotal analysis to complete..." - sleep 60 - - vt_report="## 🛡️ VirusTotal Scan Results\n\n" - vt_report+="| File | Result | Link |\n" - vt_report+="|------|--------|------|\n" - - while IFS='=' read -r filename analysis_id; do - result=$(curl -s --request GET \ - --url "https://www.virustotal.com/api/v3/analyses/$analysis_id" \ - --header "x-apikey: $VT_API_KEY") - - malicious=$(echo "$result" | jq -r '.data.attributes.stats.malicious // 0') - undetected=$(echo "$result" | jq -r '.data.attributes.stats.undetected // 0') - sha256=$(echo "$result" | jq -r '.meta.file_info.sha256 // empty') - - if [ "$malicious" = "0" ]; then - status="✅ Clean ($undetected engines)" - else - status="⚠️ $malicious detections" - fi - - link="https://www.virustotal.com/gui/file/$sha256" - vt_report+="| \`$filename\` | $status | [View]($link) |\n" - - sleep 16 - done < results/scans.txt - - echo -e "$vt_report" > results/report.md - cat results/report.md - - - name: Append scan results to release notes - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - current_body=$(gh release view "${{ steps.tag.outputs.tag }}" \ - --repo "${{ github.repository }}" \ - --json body --jq '.body') - - new_body="${current_body} - - $(cat results/report.md)" - - gh release edit "${{ steps.tag.outputs.tag }}" \ - --repo "${{ github.repository }}" \ - --notes "$new_body" diff --git a/.gitignore b/.gitignore index 81f1284..8015bab 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,7 @@ dist-ffbinaries/ # Docker tmp/ config/ -dist-ffbinaries/ \ No newline at end of file +dist-ffbinaries/ + +# Claude Code: keep entirely local, do not track +.claude/ \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml index 0a5c821..6bc4a51 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -26,6 +26,10 @@ builds: - -s -w - -X github.com/torrentclaw/unarr/internal/cmd.Version={{.Version}} - -X github.com/torrentclaw/unarr/internal/sentry.dsn={{ .Env.SENTRY_DSN }} + # Release-signing public key — verified by the self-updater against + # checksums.txt.sig. Empty when not configured; in that case + # signature verification is skipped and a warning is logged. + - -X github.com/torrentclaw/unarr/internal/upgrade.releasePubKeyBase64={{ .Env.RELEASE_SIGNING_PUBKEY }} archives: - formats: [tar.gz] @@ -55,6 +59,22 @@ changelog: - "^test:" - "^chore:" +# Self-hosted Forgejo at git.torrentclaw.com. goreleaser detects GITEA_TOKEN + +# these URLs and publishes the release there instead of GitHub. Reachable via +# `forgejo` hostname inside the dokploy-network (the runner shares it); for +# local goreleaser runs outside the network, override via env GITEA_API_URL. +# +# In goreleaser v2 `gitea_urls` is a top-level key (was nested under `release` +# in v1). +gitea_urls: + api: http://forgejo:3000/api/v1 + download: https://git.torrentclaw.com + skip_tls_verify: false + +release: + draft: false + prerelease: auto + # Homebrew tap — requires PAT with repo scope (not GITHUB_TOKEN) # Enable when torrentclaw/homebrew-tap PAT is configured as HOMEBREW_TAP_TOKEN # brews: diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/CHANGELOG.md b/CHANGELOG.md index 55bd493..de1dd6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,174 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.15] - 2026-05-27 + + +### Added + +- **sentry**: enhance error handling by skipping user input errors in CaptureError + +### Changed + +- **ci**: point Forgejo URLs at torrentclaw org (post-transfer) +- **sentry**: decouple agent import via string-match, rename predicate + +### Documentation + +- **positioning**: reframe unarr around download/stream/transcode, drop misleading search-first wording + +### Fixed + +- **ci**: unset GITHUB_TOKEN so goreleaser uses GITEA_TOKEN +- **sentry**: skip "daemon not running" stop/reload errors + +### Other + +- **scripts**: harden release.sh against double-release and inline version bumps +- untrack .claude/ (private local config) +## [0.9.14] - 2026-05-27 + + +### Added + +- **vaapi**: hybrid CPU-scale + hwupload encode path (QW2, 0.9.14) + +### CI/CD + +- port workflows from .github/ to .forgejo/ (Forgejo Actions) + +### Fixed + +- **daemon**: defensive IsClosed check in watchSessionReady poll loop +- **daemon**: use parent ctx for MarkSessionReady so cancel propagates +- **release**: move gitea_urls to top-level (goreleaser v2 schema) +## [0.9.13] - 2026-05-27 + + +### Added + +- **agent**: session-ready webhook for SSE-driven player handshake (0.9.13) +- **agent**: send full transcoder diagnostic in register payload (0.9.12) + +### Fixed + +- **daemon**: defer probeCancel so a panic mid-diagnostic still releases ctx + +### Other + +- **release**: add ship.sh end-to-end pipeline as GH Actions backup +- **skills**: add /publish slash command + allow .claude/ in git +## [0.9.11] - 2026-05-27 + + +### Added + +- **hls**: pre-segmentación delantada — 2 s segments + async session start (0.9.10) +- **hls**: faster first-start — probe cache + tighter encoder presets (0.9.9) + +### Changed + +- **hls**: critico-driven hardening of fase 3.2 + +### Fixed + +- **cors**: allow play from .to / staging / onion mirrors +- **library**: classify resolution by width + height, not height alone +- **transcode**: make preset libx264-only + restore quality opt-in + +### Other + +- **release**: 0.9.11 +## [0.9.8] - 2026-05-27 + + +### Fixed + +- **upgrade**: break auto-apply restart loop (0.9.8) +## [0.9.7] - 2026-05-26 + + +### Added + +- **hls**: persistent fMP4 segment cache + integrity + stats (0.9.7) +## [0.9.6] - 2026-05-26 + + +### Added + +- **daemon**: auto-apply upgrades when server signals (0.9.6) +## [0.9.5] - 2026-05-26 + + +### Added + +- **funnel**: cloudflare quick tunnel embedded subprocess (0.9.5) +## [0.9.4] - 2026-05-26 + + +### Added + +- **stream**: retire WebRTC, HLS-only, bump 0.9.4 (**BREAKING**) +## [0.9.3] - 2026-05-26 + + +### Added + +- **usenet**: warn at startup when par2 or extractor is missing + +### Fixed + +- **engine**: truncate errorMessage before reporting status +- **hls**: clamp ffmpeg bitrate to the level we derive from outputHeight +## [0.9.2] - 2026-05-22 + + +### Added + +- **vpn**: unarr vpn command + report/arbitrate the WireGuard slot +## [0.9.1] - 2026-05-21 + + +### Added + +- **mirror**: update fallback URLs to use IPFS and remove GitHub Pages + +### Fixed + +- **security**: bump golang.org/x deps and add container CVE scan gate + +### Other + +- **release**: 0.9.1 +## [0.9.0] - 2026-05-21 + + +### Added + +- **agent**: add mirror failover, agent client refactor, status 401 detection +- **vpn**: local config_file for self-hosted/personal VPN testing +- **vpn**: split-tunnel torrent traffic through managed WireGuard + +### CI/CD + +- deploy install scripts to GitHub Pages + +### Documentation + +- **docker**: refresh Docker Hub README + sync description in CI + +### Fixed + +- **security**: CORS allowlist, URL scheme guard, state perms, ZIP slip, mirror docs +- **security**: UPnP opt-in, bounded SSE reader, signed self-update +- **security**: harden HLS session IDs, /health disclosure, archive password handling +- **upgrade**: fetch releases from TorrentClaw app, not GitHub + +### Other + +- **pages**: add .nojekyll to disable Jekyll processing +- **pages**: set custom domain unarr.torrentclaw.com +- **release**: 0.9.0 ## [0.8.1] - 2026-05-08 @@ -25,6 +193,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Other - **gitignore**: add dist-ffbinaries to ignored files +- **release**: 0.8.1 ## [0.8.0] - 2026-05-08 @@ -238,16 +407,117 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.4.1] - 2026-04-01 +### Added + +- **cli**: add login command and refactor shared helpers +- **stream**: report watch progress to API via HTTP Range tracking + +### Fixed + +- **ci**: fix lint errors and pin CI to Go 1.25 +- **lint**: remove unused newStubCmd function + +### Other + +- **cli**: remove moreseed stub command +- **cli**: remove redundant stub commands (monitor, open, add, compare) +## [0.4.0] - 2026-03-31 + + +### Added + +- **cli**: upgrade command, rich status, and version cache + +### Fixed + +- **progress**: always report status transitions and poll for control signals +## [0.3.7] - 2026-03-31 + + +### CI/CD + +- **docker**: remove dockerhub-description sync step +## [0.3.6] - 2026-03-31 + + +### CI/CD + +- **deps**: bump docker/metadata-action from 5 to 6 +- **deps**: bump docker/setup-qemu-action from 3 to 4 +- **deps**: bump docker/login-action from 3 to 4 +- **deps**: bump docker/build-push-action from 6 to 7 +- **deps**: bump codecov/codecov-action from 5 to 6 +- **docker**: add Docker Hub description sync and DOCKERHUB.md + +### Fixed + +- **ci**: upgrade golangci-lint to v2.11.3 for Go 1.25 support +- **docker**: upgrade alpine packages to patch CVE-2025-60876 and CVE-2026-27171 +- **lint**: use default:none to disable errcheck, fix all gofmt and exhaustive +- **lint**: disable errcheck, tune gosec/exclusions for codebase state +- **lint**: configure linters for codebase maturity, fix gofmt and ineffassign +- **lint**: exclude common fire-and-forget patterns from errcheck +- **lint**: resolve errcheck and bodyclose warnings for golangci-lint v2 +## [0.3.5] - 2026-03-30 + + +### Changed + +- migrate lint config to v2, remove daemon auto-upgrade, add trust badges +## [0.3.3] - 2026-03-30 + + +### Fixed + +- **ci**: remove go-client checkout steps +## [0.3.2] - 2026-03-30 + + +### Added + +- **init**: add 60s countdown, skip key, and cancel detection to browser auth + +### CI/CD + +- **release**: add Docker Hub publish and VirusTotal scan jobs + +### Documentation + +- add beta notice, fix install URLs to get.torrentclaw.com + +### Fixed + +- **ci**: fix virustotal job condition syntax +- **docker**: simplify Dockerfile for CI builds (no local go-client) +- **release**: disable homebrew tap (needs PAT, not GITHUB_TOKEN) + +### Other + +- re-enable homebrew tap in goreleaser +## [0.3.1] - 2026-03-30 + + +### Fixed + +- **build**: unused variable in Windows process check +- **release**: disable homebrew tap until repo is created + +### Other + +- rename module from torrentclaw-cli to unarr + +### Build + +- remove UPX compression (antivirus false positives, startup penalty) +## [0.3.0] - 2026-03-29 + + ### Added - **agent**: add WebSocket transport with HTTP fallback - **auth**: browser-based CLI authentication (like Claude Code) -- **cli**: add login command and refactor shared helpers -- **cli**: upgrade command, rich status, and version cache - **daemon**: add auto-scan, force start, and stall timeout default - **debrid**: add HTTPS downloader for debrid direct URLs -- **init**: add 60s countdown, skip key, and cancel detection to browser auth -- **stream**: report watch progress to API via HTTP Range tracking - **stream**: UPnP port forwarding for remote video playback - **usenet**: implement full NNTP download pipeline - add migrate command, media server detection, and debrid auto-config @@ -257,61 +527,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - improve daemon resilience, streaming, and usenet downloads - initial commit — unarr CLI -### CI/CD - -- **deps**: bump docker/metadata-action from 5 to 6 -- **deps**: bump docker/setup-qemu-action from 3 to 4 -- **deps**: bump docker/login-action from 3 to 4 -- **deps**: bump docker/build-push-action from 6 to 7 -- **deps**: bump codecov/codecov-action from 5 to 6 -- **docker**: remove dockerhub-description sync step -- **docker**: add Docker Hub description sync and DOCKERHUB.md -- **release**: add Docker Hub publish and VirusTotal scan jobs - ### Changed -- migrate lint config to v2, remove daemon auto-upgrade, add trust badges - extract BuildSyncItems to library package, remove duplication ### Documentation -- add beta notice, fix install URLs to get.torrentclaw.com - improve CLI help, shell completion, and README ### Fixed -- **build**: unused variable in Windows process check -- **ci**: fix lint errors and pin CI to Go 1.25 -- **ci**: upgrade golangci-lint to v2.11.3 for Go 1.25 support -- **ci**: remove go-client checkout steps -- **ci**: fix virustotal job condition syntax -- **docker**: upgrade alpine packages to patch CVE-2025-60876 and CVE-2026-27171 -- **docker**: simplify Dockerfile for CI builds (no local go-client) -- **lint**: remove unused newStubCmd function -- **lint**: use default:none to disable errcheck, fix all gofmt and exhaustive -- **lint**: disable errcheck, tune gosec/exclusions for codebase state -- **lint**: configure linters for codebase maturity, fix gofmt and ineffassign -- **lint**: exclude common fire-and-forget patterns from errcheck -- **lint**: resolve errcheck and bodyclose warnings for golangci-lint v2 -- **progress**: always report status transitions and poll for control signals -- **release**: disable homebrew tap (needs PAT, not GITHUB_TOKEN) -- **release**: disable homebrew tap until repo is created - **torrent**: expand tracker list, add DHT persistence and configurable timeouts - force-start tasks bypass HasCapacity check in dispatch loop - add panic recovery to auto-scan, cap DHT nodes at 200 - harden usenet/debrid downloaders from critico review -### Other - -- **cli**: remove moreseed stub command -- **cli**: remove redundant stub commands (monitor, open, add, compare) -- re-enable homebrew tap in goreleaser -- rename module from torrentclaw-cli to unarr - ### Build -- remove UPX compression (antivirus false positives, startup penalty) - add -s -w -trimpath to Makefile, add build-small target with UPX +[0.9.15]: https://github.com/torrentclaw/unarr/compare/v0.9.14...v0.9.15 +[0.9.14]: https://github.com/torrentclaw/unarr/compare/v0.9.13...v0.9.14 +[0.9.13]: https://github.com/torrentclaw/unarr/compare/v0.9.11...v0.9.13 +[0.9.11]: https://github.com/torrentclaw/unarr/compare/v0.9.8...v0.9.11 +[0.9.8]: https://github.com/torrentclaw/unarr/compare/v0.9.7...v0.9.8 +[0.9.7]: https://github.com/torrentclaw/unarr/compare/v0.9.6...v0.9.7 +[0.9.6]: https://github.com/torrentclaw/unarr/compare/v0.9.5...v0.9.6 +[0.9.5]: https://github.com/torrentclaw/unarr/compare/v0.9.4...v0.9.5 +[0.9.4]: https://github.com/torrentclaw/unarr/compare/v0.9.3...v0.9.4 +[0.9.3]: https://github.com/torrentclaw/unarr/compare/v0.9.2...v0.9.3 +[0.9.2]: https://github.com/torrentclaw/unarr/compare/v0.9.1...v0.9.2 +[0.9.1]: https://github.com/torrentclaw/unarr/compare/v0.9.0...v0.9.1 +[0.9.0]: https://github.com/torrentclaw/unarr/compare/v0.8.1...v0.9.0 [0.8.1]: https://github.com/torrentclaw/unarr/compare/v0.8.0...v0.8.1 [0.8.0]: https://github.com/torrentclaw/unarr/compare/v0.7.0...v0.8.0 [0.7.0]: https://github.com/torrentclaw/unarr/compare/v0.6.8...v0.7.0 @@ -331,4 +577,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [0.5.1]: https://github.com/torrentclaw/unarr/compare/v0.5.0...v0.5.1 [0.5.0]: https://github.com/torrentclaw/unarr/compare/v0.4.1...v0.5.0 [0.4.1]: https://github.com/torrentclaw/unarr/compare/v0.4.0...v0.4.1 +[0.4.0]: https://github.com/torrentclaw/unarr/compare/v0.3.7...v0.4.0 +[0.3.7]: https://github.com/torrentclaw/unarr/compare/v0.3.6...v0.3.7 +[0.3.6]: https://github.com/torrentclaw/unarr/compare/v0.3.5...v0.3.6 +[0.3.5]: https://github.com/torrentclaw/unarr/compare/v0.3.3...v0.3.5 +[0.3.3]: https://github.com/torrentclaw/unarr/compare/v0.3.2...v0.3.3 +[0.3.2]: https://github.com/torrentclaw/unarr/compare/v0.3.1...v0.3.2 +[0.3.1]: https://github.com/torrentclaw/unarr/compare/v0.3.0...v0.3.1 +[0.3.0]: https://github.com/torrentclaw/unarr/releases/tag/v0.3.0 diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..d892572 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +unarr.torrentclaw.com \ No newline at end of file diff --git a/DOCKERHUB.md b/DOCKERHUB.md index dfa96c4..3df5b70 100644 --- a/DOCKERHUB.md +++ b/DOCKERHUB.md @@ -1,12 +1,21 @@ # unarr -Powerful terminal tool for torrent search and management. Search 30+ sources, inspect quality, discover popular content, find streaming providers, and manage downloads — all from your terminal. +**The single binary that replaces your whole *arr stack.** Built-in torrent, +debrid, and usenet engines. Stream, transcode, and organize your library from +one terminal — or run it as a headless daemon with a web dashboard, WireGuard +split-tunnel, and Cloudflare Funnel remote access. -**[GitHub](https://github.com/torrentclaw/unarr)** | **[Documentation](https://github.com/torrentclaw/unarr#readme)** | **[Releases](https://github.com/torrentclaw/unarr/releases)** +**[Website & docs](https://torrentclaw.com/unarr)** · **[Install guide](https://torrentclaw.com/cli)** · **[Get an API key](https://torrentclaw.com)** -## Quick Start +> Powered by [TorrentClaw](https://torrentclaw.com) — an aggregator that unifies +> YTS, EZTV, Knaben, Torrentio, Bitmagnet and more, enriched with TMDB metadata +> and a 0–100 quality score per release. -### 1. Setup (interactive wizard) +--- + +## Quick start + +### 1. First-time setup (interactive wizard) ```bash docker run -it --rm \ @@ -14,6 +23,9 @@ docker run -it --rm \ torrentclaw/unarr setup ``` +The wizard asks for your TorrentClaw API key (free at +[torrentclaw.com](https://torrentclaw.com)) and your download directory. + ### 2. Run the daemon ```bash @@ -26,6 +38,10 @@ docker run -d --name unarr \ torrentclaw/unarr ``` +That's it — `unarr` now runs headless, watching for jobs and managing downloads. + +--- + ## Docker Compose ```yaml @@ -45,45 +61,54 @@ services: environment: - TZ=UTC # - UNARR_API_KEY=tc_your_key_here + network_mode: host # recommended for full P2P performance deploy: resources: limits: memory: 512M cpus: "2.0" - network_mode: host volumes: unarr-data: ``` +```bash +docker compose run --rm unarr setup # one-time wizard +docker compose up -d # start the daemon +``` + +--- + ## Volumes -| Path | Purpose | -|------|---------| -| `/config` | Configuration file (`config.toml`) | -| `/downloads` | Finished media downloads | -| `/data` | Internal state: torrent metadata, cache | +| Path | Purpose | +|--------------|--------------------------------------------------| +| `/config` | Configuration file (`config.toml`) | +| `/downloads` | Finished media downloads | +| `/data` | Internal state: torrent metadata, cache | -## Environment Variables +## Environment variables -| Variable | Description | Default | -|----------|-------------|---------| -| `TZ` | Timezone | `UTC` | -| `UNARR_API_KEY` | TorrentClaw API key | from config | -| `UNARR_API_URL` | API endpoint | `https://torrentclaw.com` | -| `UNARR_DOWNLOAD_DIR` | Download directory | `/downloads` | -| `UNARR_CONFIG_DIR` | Config directory | `/config` | -| `UNARR_COUNTRY` | Country code (ISO 3166) | `US` | +| Variable | Description | Default | +|------------------------|--------------------------------------|---------------------------| +| `UNARR_API_KEY` | TorrentClaw API key | from config | +| `UNARR_API_URL` | API endpoint | `https://torrentclaw.com` | +| `UNARR_DOWNLOAD_DIR` | Download directory | `/downloads` | +| `UNARR_CONFIG_DIR` | Config directory | `/config` | +| `UNARR_COUNTRY` | Country code (ISO 3166) | `US` | +| `TZ` | Timezone | `UTC` | + +Any config value can be overridden by its matching `UNARR_*` environment variable. ## Networking -**Host mode** (recommended) gives full P2P performance with no port management: +**Host mode (recommended)** — full P2P performance, no port mapping: ```yaml network_mode: host ``` -**Bridge mode** — more isolated, but requires explicit ports: +**Bridge mode** — more isolated, but you must expose the BitTorrent ports: ```yaml ports: @@ -91,7 +116,7 @@ ports: - "6881-6889:6881-6889/udp" ``` -## Running Commands +## Running commands Use `docker exec` for one-off commands while the daemon is running: @@ -99,32 +124,77 @@ Use `docker exec` for one-off commands while the daemon is running: docker exec unarr unarr search "inception" --quality 1080p docker exec unarr unarr popular --limit 10 docker exec unarr unarr status -docker exec unarr unarr doctor +docker exec unarr unarr doctor # diagnose config / connectivity ``` -## Supported Architectures - -| Architecture | Tag | -|-------------|-----| -| `linux/amd64` | `latest`, `0.3`, `0.3.5` | -| `linux/arm64` | `latest`, `0.3`, `0.3.5` | +--- ## Tags -| Tag | Description | -|-----|-------------| -| `latest` | Latest stable release | -| `X.Y.Z` | Specific version (e.g. `0.3.5`) | -| `X.Y` | Latest patch for minor version (e.g. `0.3`) | +| Tag | Description | +|----------|--------------------------------------------------| +| `latest` | Latest stable release | +| `X.Y.Z` | Exact version (e.g. `0.9.0`) | +| `X.Y` | Latest patch within a minor (e.g. `0.9`) | -## Image Details +Pin a tag in production (`torrentclaw/unarr:0.9.0`) for reproducible deploys. -- **Base image:** Alpine 3.22 -- **User:** `unarr` (UID 1000, GID 1000) +## Supported architectures + +Multi-arch image — Docker pulls the right one automatically: + +- `linux/amd64` +- `linux/arm64` (Apple Silicon, Raspberry Pi 4/5, ARM servers) + +## Image details + +- **Base:** Alpine 3.22 (minimal, regularly patched) +- **User:** `unarr` (UID 1000, GID 1000) — runs as **non-root** - **Entrypoint:** `unarr start` (daemon mode) -- **Read-only filesystem** — only mounted volumes are writable -- **No root required** — runs as non-root by default +- **Read-only rootfs** — only mounted volumes are writable +- **Bundled `ffmpeg` / `ffprobe`** for media inspection — nothing else to install +- **Self-contained updates** — binaries are served from TorrentClaw's own + infrastructure, no third-party registry dependency + +--- + +## Other install methods + +Not using Docker? Install the native binary instead: + +```bash +# Linux / macOS +curl -fsSL https://torrentclaw.com/install.sh | sh + +# Windows (PowerShell) +irm https://torrentclaw.com/install.ps1 | iex + +# Go toolchain +go install github.com/torrentclaw/unarr/cmd/unarr@latest +``` + +## Mirrors + +The installer and release binaries are served from every TorrentClaw mirror, so +you can install even if one domain is blocked in your region. Each mirror is +self-contained (it serves its own binaries — no cross-domain dependency): + +| Mirror | Install command | +|--------|-----------------| +| `torrentclaw.com` (primary) | `curl -fsSL https://torrentclaw.com/install.sh \| sh` | +| `torrentclaw.to` | `curl -fsSL https://torrentclaw.to/install.sh \| sh` | +| Tor (`.onion`) | `torsocks sh -c "$(curl http://torrentf3aifidcsaaanmnmuhv2s53r6hqsl3zkmfidiaxainkeqk5id.onion/install.sh)"` | + +The Tor address routes everything (install script + binaries) through the hidden +service, so no clearnet exit is needed. + +## Links + +- **Website & docs:** https://torrentclaw.com/unarr +- **CLI install guide:** https://torrentclaw.com/cli +- **API & account:** https://torrentclaw.com +- **Mirror status:** https://torrentclaw.com/mirrors ## License -MIT License — see [LICENSE](https://github.com/torrentclaw/unarr/blob/main/LICENSE) for details. +MIT. diff --git a/Dockerfile b/Dockerfile index 1773622..64ea4e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,10 +21,23 @@ FROM alpine:3.22 # Use Alpine's native musl ffmpeg + ffprobe instead of the johnvansickle / # BtbN static glibc builds — those need a glibc shim on Alpine and the # vector-math symbols the GPL builds reference are not satisfiable by -# gcompat. Alpine ships ffmpeg ~7.x which is fine for the WebRTC -# transcoding pipeline (libx264 + libfdk-aac alternatives included). +# gcompat. Alpine ships ffmpeg ~7.x which is fine for the HLS transcoding +# pipeline (libx264 + libfdk-aac alternatives included). RUN apk upgrade --no-cache && \ - apk add --no-cache ca-certificates tzdata ffmpeg + apk add --no-cache ca-certificates tzdata ffmpeg wget + +# Bundle cloudflared so `unarr funnel on` (default: on, see config defaults) +# Just Works on a headless container with no first-run network round-trip. +# TARGETARCH is set automatically by Docker buildx during cross-builds. +ARG TARGETARCH=amd64 +RUN case "$TARGETARCH" in \ + amd64) CF_ARCH=amd64 ;; \ + arm64) CF_ARCH=arm64 ;; \ + arm) CF_ARCH=armhf ;; \ + *) echo "unsupported TARGETARCH=$TARGETARCH" >&2; exit 1 ;; \ + esac && \ + wget -qO /usr/local/bin/cloudflared "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-$CF_ARCH" && \ + chmod +x /usr/local/bin/cloudflared # Non-root user (UID 1000 matches typical host user for volume permissions) RUN addgroup -g 1000 unarr && adduser -u 1000 -G unarr -D -h /home/unarr unarr diff --git a/Docs/plans/library-sync.md b/Docs/plans/library-sync.md new file mode 100644 index 0000000..509e87a --- /dev/null +++ b/Docs/plans/library-sync.md @@ -0,0 +1,170 @@ +# Plan: Sincronización bidireccional de biblioteca (CLI ↔ Web) + +## Context +La biblioteca web solo muestra descargas completadas (download_task + debrid). El `unarr scan` escanea ficheros con ffprobe y los sube al servidor, pero solo soporta un path, no detecta borrados del disco, y no permite borrar ficheros desde la web. El usuario quiere una biblioteca unificada que refleje el estado real de su colección y se sincronice en ambas direcciones. + +## Protocolo de sincronización + +### Forward Sync (Disco → Web) +1. CLI escanea todos los `ScanPaths` configurados +2. Para cada path: descubre ficheros, compara con cache (skip ffprobe si no cambió), sube a `/library-sync` +3. En `isLastBatch=true`: el servidor elimina items con ese `scanPath` que no estén en el batch (ficheros borrados del disco desaparecen de la web) + +### Reverse Sync (Web → Disco) +1. CLI llama a `GET /agent/library-deletions` — items que el usuario soft-deleted desde la web +2. Si `AutoDelete=true` o `--yes`: borra ficheros del disco +3. Si no: muestra lista y pide confirmación interactiva +4. Llama a `POST /agent/library-deletions/confirm` con los IDs confirmados → hard-delete en DB + +### Resolución de conflictos +- Fichero en disco pero no en web → forward sync lo añade +- Fichero en web pero no en disco → forward sync lo elimina (isLastBatch) +- Soft-deleted en web, aún en disco → reverse sync lo borra del disco y confirma +- Soft-deleted en web, ya borrado del disco → reverse sync confirma directamente +- Race condition (user borra en web mientras CLI escanea) → forward sync skippea rows con `deleted_at IS NOT NULL` + +--- + +## Fase 1: Multi-path + Forward Sync mejorado + +### 1.1 CLI — Config multi-path +**Archivo:** `torrentclaw-cli/internal/config/config.go` +- Añadir `ScanPaths []string` a `LibraryConfig` +- Migrar `ScanPath` → `ScanPaths[0]` en `Load()` si `ScanPaths` está vacío +- Añadir `AutoDelete bool` (default false) + +### 1.2 CLI — Cache v2 +**Archivo:** `torrentclaw-cli/internal/library/types.go` +- Cambiar `LibraryCache` a version 2: `Paths map[string][]LibraryItem` +- Migración v1→v2: `Path`+items → `Paths[Path]` + +**Archivo:** `torrentclaw-cli/internal/library/cache.go` +- `LoadCache` detecta versión y migra +- `SaveCache` siempre guarda v2 + +### 1.3 CLI — Scan multi-path +**Archivo:** `torrentclaw-cli/internal/cmd/scan.go` +- `unarr scan` sin args → escanea todos los `ScanPaths` +- `unarr scan /path/a /path/b` → escanea paths específicos y los recuerda en config +- Loop: para cada path, scan + sync con su `scanPath` + +### 1.4 CLI — Nuevo comando `unarr sync` +**Archivo nuevo:** `torrentclaw-cli/internal/cmd/sync.go` +- Forward sync: scan ligero (sin ffprobe para ficheros sin cambios) + upload +- Sin reverse sync todavía (Fase 3) +- Flags: `--dry-run`, `--paths` + +### 1.5 Web — Columna `scan_path` en `library_item` +**Archivo:** `torrentclaw-web/src/lib/db/schema.ts` +- Añadir `scanPath: varchar(2048)` a tabla `libraryItem` +- Generar migración con `pnpm db:generate` + +**Archivo:** `torrentclaw-web/src/lib/services/library-upgrade.ts` +- `syncLibraryItems()`: persistir `scanPath` en cada row al hacer upsert + +### 1.6 CLI — Daemon multi-path +**Archivo:** `torrentclaw-cli/internal/cmd/daemon.go` +- `runAutoScan()` itera sobre todos los `ScanPaths` + +--- + +## Fase 2: Reverse Sync (Web → Disco) + +### 2.1 Web — Soft-delete +**Archivo:** `torrentclaw-web/src/lib/db/schema.ts` +- Añadir `deletedAt: timestamp` a tabla `libraryItem` +- Generar migración + +### 2.2 Web — Endpoints de borrado +**Archivo nuevo:** `torrentclaw-web/src/app/api/internal/library/items/route.ts` +- `DELETE` — session auth, recibe `{itemIds: number[]}`, hace soft-delete (`deletedAt = NOW()`) + +**Archivo nuevo:** `torrentclaw-web/src/app/api/internal/agent/library-deletions/route.ts` +- `GET` — agent auth, devuelve items con `deletedAt IS NOT NULL` para ese usuario +- `POST` — agent auth, recibe `{confirmedIds: number[]}`, hard-delete los rows + +### 2.3 Web — Heartbeat con pendingDeletions +**Archivo:** endpoint de heartbeat del agente +- Añadir `pendingDeletions: number` al response (count de items con `deletedAt IS NOT NULL`) + +### 2.4 Web — Forward sync respeta soft-deletes +**Archivo:** `torrentclaw-web/src/lib/services/library-upgrade.ts` +- `syncLibraryItems()` en `isLastBatch`: la query de DELETE excluye rows con `deletedAt IS NOT NULL` + +### 2.5 CLI — Agent client nuevos métodos +**Archivo:** `torrentclaw-cli/internal/agent/client.go` +- `GetLibraryDeletions(ctx) → []DeletionItem` +- `ConfirmLibraryDeletions(ctx, ids []int) → error` + +**Archivo:** `torrentclaw-cli/internal/agent/types.go` +- `DeletionItem {ID int, FilePath string, DeletedAt string}` + +### 2.6 CLI — Sync reverse +**Archivo:** `torrentclaw-cli/internal/cmd/sync.go` +- Después del forward sync: llama a `GetLibraryDeletions()` +- Valida que cada fichero está dentro de un `ScanPaths` conocido (seguridad) +- Si `AutoDelete` o `--yes`: borra y confirma +- Si no: muestra lista interactiva, pide confirmación +- Flag `--no-delete` para skip reverse sync +- Si `BackupDir` configurado: mover a backup en vez de borrar + +### 2.7 CLI — Daemon auto-delete +**Archivo:** `torrentclaw-cli/internal/cmd/daemon.go` +- Al final de `runAutoSync()`: si `AutoDelete=true`, procesa deletions automáticamente +- Si no: log warning "N files pending deletion, run `unarr sync`" + +--- + +## Fase 3: Web UI (brief) + +- Botón "Eliminar" en items de biblioteca → llama `DELETE /library/items` +- Badge "Pendiente de borrar" en items soft-deleted +- Posibilidad de cancelar el borrado (clear `deletedAt`) +- Vista unificada: scanned items + downloaded items en la misma vista + +--- + +## Archivos clave + +### CLI (Go) +| Archivo | Cambio | +|---------|--------| +| `internal/config/config.go` | ScanPaths, AutoDelete, migración | +| `internal/library/types.go` | Cache v2 con Paths map | +| `internal/library/cache.go` | Load/Save v2, migración v1 | +| `internal/library/sync.go` | BuildSyncItems (sin cambios) | +| `internal/cmd/scan.go` | Multi-path loop | +| `internal/cmd/sync.go` | **Nuevo** — comando sync bidireccional | +| `internal/cmd/daemon.go` | runAutoSync multi-path + reverse | +| `internal/agent/client.go` | GetLibraryDeletions, ConfirmLibraryDeletions | +| `internal/agent/types.go` | DeletionItem type | + +### Web (TypeScript) +| Archivo | Cambio | +|---------|--------| +| `src/lib/db/schema.ts` | scanPath + deletedAt en library_item | +| `src/lib/services/library-upgrade.ts` | persistir scanPath, respetar soft-deletes | +| `src/app/api/internal/agent/library-deletions/route.ts` | **Nuevo** — GET + POST | +| `src/app/api/internal/library/items/route.ts` | **Nuevo** — DELETE soft-delete | +| Endpoint heartbeat del agente | pendingDeletions en response | + +--- + +## Verificación + +### Fase 1 +1. `go build ./cmd/unarr/ && go test ./...` +2. Configurar 2 scan paths en config.toml, ejecutar `unarr scan` → ambos se escanean +3. Borrar un fichero del disco, ejecutar `unarr scan` → desaparece de la web +4. `pnpm build` en torrentclaw-web para verificar tipos + +### Fase 2 +1. Desde la web: borrar un item de la biblioteca +2. Ejecutar `unarr sync` → muestra el fichero pendiente de borrar, pedir confirmación +3. Confirmar → fichero se borra del disco y desaparece de la web +4. `unarr sync --dry-run` → muestra lo que haría sin hacer nada +5. Con `auto_delete = true` en config: el daemon borra automáticamente + +### Fase 3 +1. Verificar visualmente en Chrome DevTools la UI de borrado +2. Verificar que el badge "pendiente" aparece y desaparece correctamente diff --git a/Docs/plans/security-stream-token.md b/Docs/plans/security-stream-token.md new file mode 100644 index 0000000..1a08e21 --- /dev/null +++ b/Docs/plans/security-stream-token.md @@ -0,0 +1,131 @@ +# Phase 2.2 — Per-task stream token (deferred) + +Status: deferred. Requires coordinated change in the web app +(`torrentclaw-web`) and the CLI daemon. Pulled out of the Phase 2 +security pass because the CLI-only fixes (UPnP opt-in, SSE caps, +signed self-update) ship without web-side work; the stream-token +work cannot. + +## Problem + +`/stream`, `/playlist.m3u` and `/hls//...` on the daemon +HTTP server have no authentication. Today, anyone who can reach the +listener and guesses (or learns) the `taskID` (for `/stream`) or +`sessionID` (for `/hls`) can fetch the active file. + +Mitigations already in place after Phase 1+2: + +- `sessionID` is restricted to a safe regex and is a server-issued + UUID v4 (122-bit entropy, not enumerable in practice). +- `/health` no longer leaks the active filename, taskID prefix or + client IP to remote callers (loopback diagnostics preserved). +- UPnP is opt-in, so by default the daemon is not exposed to the + public internet. +- The web client probes `/health` to pick LAN vs Tailscale. + +Residual risk: + +- On a shared LAN (open Wi-Fi, office network, dorm) any device can + reach the listener and brute-force `?id=` against + `/stream`. taskIDs are also UUIDs, so this is high entropy, but + the URL may leak through browser history, sharing, screen capture + or a passive logger and there is no second factor. +- A user who explicitly opts into UPnP exposes the same surface to + the entire internet. + +A per-task secret carried in the URL closes this without breaking +the `