diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9a4deb9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +fuzz/corpus/** binary +crates/carapaced/tests/fixtures/*.b64 text eol=lf +crates/carapaced/tests/fixtures/*.tsv text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dea70d6..50299a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,127 @@ permissions: contents: read jobs: + cbor-oracle: + runs-on: ubuntu-latest + steps: + - name: Checkout Carapace + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + path: Carapace + + - name: Install Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: Carapace/requirements-cbor-vectors.txt + + - name: Install pinned oracle dependencies + run: python -m pip install --requirement Carapace/requirements-cbor-vectors.txt + + - name: Check independent CBOR vectors + working-directory: Carapace + run: python cbor_vectors.py --check + + gui: + runs-on: ubuntu-latest + steps: + - name: Checkout Carapace + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + path: Carapace + + - name: Install pinned Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 24.18.0 + cache: npm + cache-dependency-path: Carapace/gui/package-lock.json + + - name: Install pinned npm + run: npm install --global npm@11.16.0 + + - name: Verify Node.js tools + run: | + test "$(node --version)" = "v24.18.0" + test "$(npm --version)" = "11.16.0" + + - name: Install locked GUI dependencies + working-directory: Carapace/gui + run: npm ci + + - name: Check GUI source + working-directory: Carapace/gui + run: npm run check + + - name: Test GUI contracts + working-directory: Carapace/gui + run: npm test + + - name: Install pinned browser runtime + working-directory: Carapace/gui + run: npx playwright install --with-deps chromium + + - name: Test GUI in a real browser + working-directory: Carapace/gui + run: npm run test:browser + + - name: Build embedded GUI + working-directory: Carapace/gui + run: npm run build + + - name: Verify embedded GUI is current + working-directory: Carapace + run: git diff --exit-code -- crates/carapace-api/static + + supply-chain: + runs-on: ubuntu-latest + steps: + - name: Checkout Carapace + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + path: Carapace + + - name: Read pinned Chela revision + id: chela-pin + shell: bash + run: | + revision="$(tr -d '\r\n' < Carapace/chela-revision.txt)" + [[ "$revision" =~ ^[0-9a-f]{40}$ ]] + echo "revision=$revision" >> "$GITHUB_OUTPUT" + + - name: Checkout chela + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + repository: SecretSplitKit/Chela + ref: ${{ steps.chela-pin.outputs.revision }} + token: ${{ secrets.CHELA_CHECKOUT_TOKEN || github.token }} + path: chela + + - name: Check tracked files for secrets + working-directory: Carapace + run: scripts/check-tracked-secrets.sh + + - name: Check supply-chain configuration + working-directory: Carapace + run: scripts/check-supply-chain-config.sh + + - name: Check dependency licenses, sources, bans, and advisories + uses: EmbarkStudios/cargo-deny-action@d755fbddac377c2d538f556dd0f9c7728c7f73e4 # v2.0.14 + with: + rust-version: 1.95.0 + manifest-path: Carapace/Cargo.toml + arguments: --all-features + command-arguments: --config /github/workspace/Carapace/deny.toml + + - name: Prepare lock file for cargo audit + run: cp Carapace/Cargo.lock Cargo.lock + + - name: Audit Rust dependencies + uses: rustsec/audit-check@69366f33c96575abad1ee0dba8212993eecbe998 # v2.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + test: strategy: fail-fast: false @@ -30,20 +151,77 @@ jobs: with: path: Carapace + - name: Read pinned Chela revision + id: chela-pin + shell: bash + run: | + revision="$(tr -d '\r\n' < Carapace/chela-revision.txt)" + [[ "$revision" =~ ^[0-9a-f]{40}$ ]] + echo "revision=$revision" >> "$GITHUB_OUTPUT" + echo "CHELA_REV=$revision" >> "$GITHUB_ENV" + - name: Checkout chela uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: repository: SecretSplitKit/Chela - ref: main - # The default GITHUB_TOKEN is scoped to THIS repo only and cannot read the - # separate private Chela repo. Provide a PAT/App token as the - # CHELA_CHECKOUT_TOKEN secret; the fallback works only if Chela is public. + ref: ${{ steps.chela-pin.outputs.revision }} + # The default GITHUB_TOKEN is scoped to this repository. Use the + # optional token if Chela becomes private. token: ${{ secrets.CHELA_CHECKOUT_TOKEN || github.token }} path: chela - - name: Install stable Rust + - name: Verify Chela revision + shell: bash + run: | + actual_revision="$(git -C chela rev-parse HEAD)" + test "$actual_revision" = "$CHELA_REV" + + - name: Check release workflow + shell: bash + working-directory: Carapace + run: scripts/check-release-workflow.sh + + - name: Check shared restore layout + shell: bash + working-directory: Carapace + run: scripts/check-restore-layout.sh + + - name: Check requirement-to-test inventory + shell: bash + working-directory: Carapace + run: scripts/check-test-inventory.sh + + - name: Check operational log sources + shell: bash + working-directory: Carapace + run: scripts/check-operational-logs.sh + + - name: Check state fixture coverage and integrity + shell: bash + working-directory: Carapace + run: scripts/state-fixtures.sh check + + - name: Install fuzz toolchain + if: runner.os == 'Linux' + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + with: + toolchain: nightly-2026-06-01 + + - name: Run all fuzz targets with bounded budgets + shell: bash + working-directory: Carapace + run: | + if [[ "$RUNNER_OS" == "Linux" ]]; then + cargo install cargo-fuzz --version 0.13.1 --locked + scripts/check-fuzz-targets.sh --smoke + else + scripts/check-fuzz-targets.sh + fi + + - name: Install pinned Rust uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable with: + toolchain: 1.95.0 components: clippy, rustfmt - name: Cache cargo @@ -67,4 +245,4 @@ jobs: - name: cargo test working-directory: Carapace - run: cargo test --workspace + run: cargo test --workspace --locked diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1052c57..1612d74 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,17 +6,141 @@ on: env: CARGO_TERM_COLOR: always - BIN_NAME: carapaced -# Least privilege by default; the build job elevates to contents: write only for -# the release-asset upload. +# Build jobs are read-only. Only the publication gate can write release assets. permissions: + actions: read contents: read jobs: + quality: + runs-on: ubuntu-latest + steps: + - name: Checkout Carapace + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + path: Carapace + - name: Read pinned Chela revision + id: chela-pin + shell: bash + run: | + revision="$(tr -d '\r\n' < Carapace/chela-revision.txt)" + [[ "$revision" =~ ^[0-9a-f]{40}$ ]] + echo "revision=$revision" >> "$GITHUB_OUTPUT" + - name: Checkout chela + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + repository: SecretSplitKit/Chela + ref: ${{ steps.chela-pin.outputs.revision }} + token: ${{ secrets.CHELA_CHECKOUT_TOKEN || github.token }} + path: chela + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + with: + toolchain: 1.95.0 + components: clippy, rustfmt + - name: Check Rust formatting and lints + working-directory: Carapace + run: | + cargo fmt --all -- --check + cargo clippy --workspace --all-targets -- -D warnings + - name: Check secrets and supply-chain policy + working-directory: Carapace + run: | + scripts/check-tracked-secrets.sh + scripts/check-supply-chain-config.sh + - name: Check dependency policy + uses: EmbarkStudios/cargo-deny-action@d755fbddac377c2d538f556dd0f9c7728c7f73e4 # v2.0.14 + with: + rust-version: 1.95.0 + manifest-path: Carapace/Cargo.toml + arguments: --all-features + command-arguments: --config /github/workspace/Carapace/deny.toml + - name: Install Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + - name: Check schema and independent vectors + working-directory: Carapace + run: | + python -m pip install --requirement requirements-cbor-vectors.txt + python cbor_vectors.py --check + cargo test --workspace --locked + - name: Check source layout, logs, inventory, and fixtures + working-directory: Carapace + run: | + scripts/check-release-workflow.sh + scripts/check-restore-layout.sh + scripts/check-test-inventory.sh + scripts/check-operational-logs.sh + scripts/state-fixtures.sh check + - name: Install pinned fuzz toolchain + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + with: + toolchain: nightly-2026-06-01 + - name: Run all fuzz targets with bounded budgets + working-directory: Carapace + run: | + cargo install cargo-fuzz --version 0.13.1 --locked + scripts/check-fuzz-targets.sh --smoke + - name: Install pinned Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 24.18.0 + cache: npm + cache-dependency-path: Carapace/gui/package-lock.json + - name: Check GUI and browser behavior + working-directory: Carapace/gui + run: | + npm install --global npm@11.16.0 + npm ci + npm run check + npm test + npx playwright install --with-deps chromium + npm run test:browser + npm run build + - name: Verify embedded GUI is current + working-directory: Carapace + run: git diff --exit-code -- crates/carapace-api/static + + native-tests: + needs: quality + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Carapace + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + path: Carapace + - name: Read pinned Chela revision + id: chela-pin + shell: bash + run: | + revision="$(tr -d '\r\n' < Carapace/chela-revision.txt)" + [[ "$revision" =~ ^[0-9a-f]{40}$ ]] + echo "revision=$revision" >> "$GITHUB_OUTPUT" + - name: Checkout chela + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + repository: SecretSplitKit/Chela + ref: ${{ steps.chela-pin.outputs.revision }} + token: ${{ secrets.CHELA_CHECKOUT_TOKEN || github.token }} + path: chela + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + with: + toolchain: 1.95.0 + - name: Run native workspace tests + working-directory: Carapace + run: cargo test --workspace --locked + build: - permissions: - contents: write # softprops/action-gh-release attaches assets to the release + needs: [quality, native-tests] + env: + MACOSX_DEPLOYMENT_TARGET: "11.0" strategy: fail-fast: false matrix: @@ -25,21 +149,11 @@ jobs: target: x86_64-unknown-linux-gnu - os: ubuntu-latest target: aarch64-unknown-linux-gnu - cross: true - os: macos-latest target: x86_64-apple-darwin - os: macos-latest target: aarch64-apple-darwin - - os: windows-latest - target: x86_64-pc-windows-msvc - - os: windows-latest - target: aarch64-pc-windows-msvc - cross: true runs-on: ${{ matrix.os }} - # aarch64 cross targets rely on toolchain components (linux: apt - # cross-gcc, windows: MSVC ARM64 tools) that aren't guaranteed on every - # runner image; don't let a cross-target hiccup block the native builds. - continue-on-error: ${{ matrix.cross == true }} steps: # Path dependency on chela-engine/chela-bip39/chela-share # (`../../../chela/...`) requires both repos as siblings. @@ -48,20 +162,36 @@ jobs: with: path: Carapace + - name: Read pinned Chela revision + id: chela-pin + shell: bash + run: | + revision="$(tr -d '\r\n' < Carapace/chela-revision.txt)" + [[ "$revision" =~ ^[0-9a-f]{40}$ ]] + echo "revision=$revision" >> "$GITHUB_OUTPUT" + echo "CHELA_REV=$revision" >> "$GITHUB_ENV" + - name: Checkout chela uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: repository: SecretSplitKit/Chela - ref: main + ref: ${{ steps.chela-pin.outputs.revision }} # The default GITHUB_TOKEN cannot read the separate private Chela repo; # provide a PAT/App token as CHELA_CHECKOUT_TOKEN. The fallback works only # if Chela is public. token: ${{ secrets.CHELA_CHECKOUT_TOKEN || github.token }} path: chela + - name: Verify Chela revision + shell: bash + run: | + actual_revision="$(git -C chela rev-parse HEAD)" + test "$actual_revision" = "$CHELA_REV" + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable with: + toolchain: 1.95.0 targets: ${{ matrix.target }} - name: Install aarch64 cross-compiler (Linux) @@ -74,16 +204,61 @@ jobs: - name: Build working-directory: Carapace - run: cargo build --release --locked --target ${{ matrix.target }} -p carapaced + run: cargo build --release --locked --target ${{ matrix.target }} -p carapace-api --bin carapaced -p carapace --bin carapace + + - name: Verify expected binaries (Unix) + if: runner.os != 'Windows' + shell: bash + working-directory: Carapace + run: | + test -s "target/${{ matrix.target }}/release/carapaced" + test -x "target/${{ matrix.target }}/release/carapaced" + test -s "target/${{ matrix.target }}/release/carapace" + test -x "target/${{ matrix.target }}/release/carapace" + + - name: Verify expected binaries (Windows) + if: runner.os == 'Windows' + shell: pwsh + working-directory: Carapace + run: | + $daemon = Get-Item "target/${{ matrix.target }}/release/carapaced.exe" + $client = Get-Item "target/${{ matrix.target }}/release/carapace.exe" + if ($daemon.Length -eq 0 -or $client.Length -eq 0) { + throw "A release executable is empty." + } + + - name: Generate CycloneDX SBOM + uses: anchore/sbom-action@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1 + with: + path: Carapace + format: cyclonedx-json + output-file: Carapace/sbom-${{ matrix.target }}.cdx.json + upload-artifact: false - name: Stage artifact (Unix) if: runner.os != 'Windows' shell: bash working-directory: Carapace run: | - staging="${BIN_NAME}-${{ matrix.target }}" + staging="carapace-${{ matrix.target }}" mkdir -p "$staging" - cp "target/${{ matrix.target }}/release/${BIN_NAME}" "$staging/" + cp "target/${{ matrix.target }}/release/carapaced" "$staging/" + cp "target/${{ matrix.target }}/release/carapace" "$staging/" + cp "sbom-${{ matrix.target }}.cdx.json" "$staging/SBOM.cdx.json" + echo "chela_revision=${CHELA_REV}" > "$staging/BUILD-METADATA.txt" + if [[ "${{ matrix.target }}" == *-linux-gnu ]]; then + carapaced_glibc_minimum="$(strings "target/${{ matrix.target }}/release/carapaced" | sed -n 's/.*GLIBC_\([0-9][0-9.]*\).*/\1/p' | sort -V | tail -1)" + carapace_glibc_minimum="$(strings "target/${{ matrix.target }}/release/carapace" | sed -n 's/.*GLIBC_\([0-9][0-9.]*\).*/\1/p' | sort -V | tail -1)" + test -n "$carapaced_glibc_minimum" + test -n "$carapace_glibc_minimum" + echo "carapaced_glibc_minimum=${carapaced_glibc_minimum}" >> "$staging/BUILD-METADATA.txt" + echo "carapace_glibc_minimum=${carapace_glibc_minimum}" >> "$staging/BUILD-METADATA.txt" + else + vtool -show-build "target/${{ matrix.target }}/release/carapaced" | grep -F 'minos 11.0' + vtool -show-build "target/${{ matrix.target }}/release/carapace" | grep -F 'minos 11.0' + echo "carapaced_macos_minimum=11.0" >> "$staging/BUILD-METADATA.txt" + echo "carapace_macos_minimum=11.0" >> "$staging/BUILD-METADATA.txt" + fi tar czf "${staging}.tar.gz" "$staging" echo "ASSET=Carapace/${staging}.tar.gz" >> "$GITHUB_ENV" @@ -92,19 +267,60 @@ jobs: shell: pwsh working-directory: Carapace run: | - $staging = "${env:BIN_NAME}-${{ matrix.target }}" + $staging = "carapace-${{ matrix.target }}" New-Item -ItemType Directory -Path $staging | Out-Null - Copy-Item "target/${{ matrix.target }}/release/${env:BIN_NAME}.exe" "$staging/" + Copy-Item "target/${{ matrix.target }}/release/carapaced.exe" "$staging/" + Copy-Item "target/${{ matrix.target }}/release/carapace.exe" "$staging/" + Copy-Item "sbom-${{ matrix.target }}.cdx.json" "$staging/SBOM.cdx.json" + Set-Content -Path "$staging/BUILD-METADATA.txt" -Value "chela_revision=${env:CHELA_REV}" Compress-Archive -Path $staging -DestinationPath "${staging}.zip" echo "ASSET=Carapace/${staging}.zip" >> $env:GITHUB_ENV - name: Upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: ${{ env.BIN_NAME }}-${{ matrix.target }} + name: carapace-${{ matrix.target }} path: ${{ env.ASSET }} - - name: Attach to GitHub release + publish: + needs: [quality, native-tests, build] + runs-on: ubuntu-latest + permissions: + actions: read + attestations: write + contents: write + id-token: write + steps: + - name: Download all release artifacts + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + with: + path: release-assets + merge-multiple: true + + - name: Create SHA-256 checksums + working-directory: release-assets + run: sha256sum carapace-* > SHA256SUMS + + - name: Create approved detached signature + working-directory: release-assets + env: + RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }} + RELEASE_SIGNING_FINGERPRINT: ${{ secrets.RELEASE_SIGNING_FINGERPRINT }} + run: | + test -n "$RELEASE_SIGNING_KEY" + test -n "$RELEASE_SIGNING_FINGERPRINT" + printf '%s' "$RELEASE_SIGNING_KEY" | gpg --batch --import + actual="$(gpg --batch --with-colons --fingerprint "$RELEASE_SIGNING_FINGERPRINT" | awk -F: '$1 == "fpr" { print $10; exit }')" + test "$actual" = "$RELEASE_SIGNING_FINGERPRINT" + gpg --batch --local-user "$RELEASE_SIGNING_FINGERPRINT" --detach-sign --armor --output SHA256SUMS.asc SHA256SUMS + + - name: Attest release artifact provenance + uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 + with: + subject-path: release-assets/carapace-* + + - name: Attach complete artifact set to GitHub release uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3 with: - files: ${{ env.ASSET }} + fail_on_unmatched_files: true + files: release-assets/* diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..ca5c350 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.18.0 diff --git a/Cargo.lock b/Cargo.lock index d53cebd..06de826 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -117,6 +123,17 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +[[package]] +name = "apple-native-keyring-store" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "797f94b6a53d7d10b56dc18290e0d40a2158352f108bb4ff32350825081a9f29" +dependencies = [ + "keyring-core", + "log", + "security-framework", +] + [[package]] name = "arc-swap" version = "1.9.2" @@ -189,6 +206,126 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -415,6 +552,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -424,6 +570,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -466,14 +625,19 @@ dependencies = [ "axum", "carapace-wire", "carapaced", + "ed25519-dalek 2.2.0", "getrandom 0.2.17", "hex", + "rpassword", "rust-embed", + "rustix", "serde", "serde_json", "subtle", "tempfile", "tokio", + "tower", + "zeroize", ] [[package]] @@ -499,6 +663,7 @@ version = "0.1.0" dependencies = [ "blake3", "carapace-crypto", + "carapace-restore", "carapace-wire", "ed25519-dalek 2.2.0", "tempfile", @@ -564,6 +729,17 @@ dependencies = [ "hex", ] +[[package]] +name = "carapace-restore" +version = "0.1.0" +dependencies = [ + "blake3", + "getrandom 0.2.17", + "rustix", + "tempfile", + "unicode-normalization", +] + [[package]] name = "carapace-share" version = "0.1.0" @@ -579,6 +755,7 @@ version = "0.1.0" dependencies = [ "blake3", "carapace-crypto", + "carapace-restore", "carapace-wire", "chacha20poly1305", "ed25519-dalek 2.2.0", @@ -601,6 +778,7 @@ name = "carapaced" version = "0.1.0" dependencies = [ "anyhow", + "base64", "blake3", "carapace-crypto", "carapace-disclose", @@ -613,16 +791,28 @@ dependencies = [ "carapace-wire", "chela-share", "ed25519-dalek 2.2.0", + "flate2", "getrandom 0.2.17", "iroh", "iroh-blobs", + "keyring", "notify", "redb", + "rustix", "tempfile", "tokio", "zeroize", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.67" @@ -824,6 +1014,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -934,6 +1133,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -1388,6 +1596,12 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + [[package]] name = "enum-assoc" version = "1.3.0" @@ -1399,6 +1613,27 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1415,6 +1650,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastbloom" version = "0.17.0" @@ -1467,6 +1722,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1522,7 +1787,7 @@ dependencies = [ "diatomic-waker", "futures-core", "pin-project-lite", - "spin 0.10.0", + "spin 0.10.1", ] [[package]] @@ -1795,7 +2060,7 @@ dependencies = [ "hash32", "rustc_version", "serde", - "spin 0.9.8", + "spin 0.9.9", "stable_deref_trait", ] @@ -1805,6 +2070,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -2236,6 +2507,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ + "block-padding", "generic-array", ] @@ -2670,6 +2942,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keyring" +version = "4.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0298a59b384c540e408a600c8b375a09b49c3f97debc080e2c30675d79a6368a" +dependencies = [ + "apple-native-keyring-store", + "keyring-core", + "windows-native-keyring-store", + "zbus-secret-service-keyring-store", +] + +[[package]] +name = "keyring-core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e621458ca9c51aa110bd0339d4751a056b9576bf1253aee1aa560dda0fc9d" +dependencies = [ + "log", +] + [[package]] name = "kqueue" version = "1.2.0" @@ -2796,6 +3089,15 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -2818,6 +3120,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.1" @@ -3140,6 +3452,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.8" @@ -3150,6 +3476,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -3165,6 +3500,27 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3332,6 +3688,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "p256" version = "0.13.2" @@ -3459,6 +3825,17 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -3492,6 +3869,20 @@ dependencies = [ "time", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -3853,6 +4244,18 @@ dependencies = [ "windows", ] +[[package]] +name = "regex" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.15" @@ -3946,6 +4349,27 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rpassword" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.61.2", +] + +[[package]] +name = "rtoolbox" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "rust-embed" version = "8.12.0" @@ -4183,6 +4607,25 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secret-service" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a62d7f86047af0077255a29494136b9aaaf697c76ff70b8e49cded4e2623c14" +dependencies = [ + "aes", + "cbc", + "futures-util", + "generic-array", + "getrandom 0.2.17", + "hkdf", + "num", + "once_cell", + "serde", + "sha2 0.10.9", + "zbus", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -4298,6 +4741,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "serde_spanned" version = "1.1.1" @@ -4419,6 +4873,12 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "simd_cesu8" version = "1.1.1" @@ -4494,18 +4954,18 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" dependencies = [ "lock_api", ] [[package]] name = "spin" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" [[package]] name = "spki" @@ -4588,6 +5048,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn-mid" version = "0.5.4" @@ -5079,6 +5550,17 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "unicase" version = "2.9.0" @@ -5091,6 +5573,15 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.13.3" @@ -5186,6 +5677,7 @@ checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a" dependencies = [ "getrandom 0.4.3", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -5488,6 +5980,19 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-native-keyring-store" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063426e76fdec7438d56bb777f67e318a84a25c707b07e575cb8b78e10c028f8" +dependencies = [ + "byteorder", + "keyring-core", + "regex", + "windows-sys 0.61.2", + "zeroize", +] + [[package]] name = "windows-numerics" version = "0.3.1" @@ -5545,6 +6050,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -5889,6 +6403,78 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-secret-service-keyring-store" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ccede190ba363386a24e8021c7f3848393976609ec9f5d1f8c6c09ef37075b4" +dependencies = [ + "keyring-core", + "secret-service", + "zbus", +] + +[[package]] +name = "zbus_macros" +version = "5.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.118", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.54" @@ -5988,3 +6574,43 @@ name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zvariant" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.118", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.118", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml index 12ef416..af51742 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = ["crates/*"] [workspace.package] edition = "2021" +rust-version = "1.95" license = "Apache-2.0 OR MIT" repository = "https://github.com/SecretSplitKit/Carapace" diff --git a/cbor_vectors.py b/cbor_vectors.py index 8a0ac34..b034364 100644 --- a/cbor_vectors.py +++ b/cbor_vectors.py @@ -10,9 +10,20 @@ sorted bytewise-lexicographically on their encoded form - bool/null as simple values; floats PROHIBITED """ +import argparse import hashlib +import re +from pathlib import Path from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +parser = argparse.ArgumentParser(description="Generate or check Carapace CBOR vectors") +parser.add_argument( + "--check", + action="store_true", + help="compare all generated vectors with the Rust golden source without writing files", +) +args = parser.parse_args() + # ---------------- deterministic CBOR encoder (reference) ---------------- def enc_uint(n, major=0): @@ -332,30 +343,32 @@ def hexwrap(b, indent=""): lines.append(hexwrap(e)) lines.append("```") lines.append("") -with open("appendix_b8_fragment.md", "w") as fh: - fh.write("\n".join(lines)) +if not args.check: + with open("appendix_b8_fragment.md", "w") as fh: + fh.write("\n".join(lines)) -print(f"\nPART 2: {len(part2)} frames + {len(docs)} documents generated, all signatures verified OK") -print("fragment written to appendix_b8_fragment.md") + print(f"\nPART 2: {len(part2)} frames + {len(docs)} documents generated, all signatures verified OK") + print("fragment written to appendix_b8_fragment.md") # ---------------- output ---------------- -print("== test keys ==") -for label, kp in [("USER_A", USER_A), ("USER_B", USER_B), - ("NODE_A1", NODE_A1), ("NODE_B1", NODE_B1)]: - print(f"{label}: seed={kp.private_bytes_raw().hex()}") - print(f"{label}: pub ={kp.public_key().public_bytes_raw().hex()}") -print(f"T0={T0}") -print() -for name, mt, body, f, note in vectors: - print(f"== {name} (type {mt}) == {note}") - print(f"frame ({len(f)} bytes):") - h = f.hex() - for i in range(0, len(h), 64): - print(" " + h[i:i+64]) +if not args.check: + print("== test keys ==") + for label, kp in [("USER_A", USER_A), ("USER_B", USER_B), + ("NODE_A1", NODE_A1), ("NODE_B1", NODE_B1)]: + print(f"{label}: seed={kp.private_bytes_raw().hex()}") + print(f"{label}: pub ={kp.public_key().public_bytes_raw().hex()}") + print(f"T0={T0}") print() -print("== InviteTicket URI ==") -print(ticket_uri) + for name, mt, body, f, note in vectors: + print(f"== {name} (type {mt}) == {note}") + print(f"frame ({len(f)} bytes):") + h = f.hex() + for i in range(0, len(h), 64): + print(" " + h[i:i+64]) + print() + print("== InviteTicket URI ==") + print(ticket_uri) # sanity: verify every signature verifies from cryptography.exceptions import InvalidSignature @@ -367,4 +380,33 @@ def hexwrap(b, indent=""): signer = pubmap[body.get(22, body[0])].public_key() body_wo = {k2: v for k2, v in body.items() if k2 != 23} signer.verify(body[23], DOMAIN + enc([mt, body_wo])) -print("\nall signatures verified OK") +def check_vectors(generated_vectors, generated_docs): + """Compare all oracle values with the committed Rust golden source.""" + rust_source = Path(__file__).resolve().parent / "crates/carapace-wire/tests/vectors.rs" + text = rust_source.read_text(encoding="utf-8") + rust_frames = re.findall(r'assert_frame\(\s*"([0-9a-f]+)"', text) + rust_documents = re.findall( + r'assert_eq!\(\s*"([0-9a-f]+)",\s*hex::encode\([^)]*\.to_bytes\(\)\)', + text, + ) + generated = [(name, data.hex()) for name, _, _, data, _ in generated_vectors] + generated.extend((name, data.hex()) for name, _, data, _ in generated_docs) + rust = rust_frames + rust_documents + if len(rust) != len(generated): + raise SystemExit( + f"vector count mismatch: oracle generated {len(generated)}, Rust has {len(rust)}" + ) + mismatches = [ + f"{index + 1} {name}" + for index, ((name, actual), expected) in enumerate(zip(generated, rust)) + if actual != expected + ] + if mismatches: + raise SystemExit("CBOR vector mismatch: " + ", ".join(mismatches)) + print(f"checked {len(generated)} independent CBOR vectors against {rust_source}") + + +if args.check: + check_vectors(vectors + part2, docs) +else: + print("\nall signatures verified OK") diff --git a/chela-revision.txt b/chela-revision.txt new file mode 100644 index 0000000..bd731fc --- /dev/null +++ b/chela-revision.txt @@ -0,0 +1 @@ +2f56b8333bd8b3882d47ce55c4fbba65542a70ce diff --git a/crates/carapace-api/Cargo.toml b/crates/carapace-api/Cargo.toml index f679afe..03dd644 100644 --- a/crates/carapace-api/Cargo.toml +++ b/crates/carapace-api/Cargo.toml @@ -2,6 +2,7 @@ name = "carapace-api" version = "0.1.0" edition.workspace = true +rust-version.workspace = true license.workspace = true [lints] @@ -19,7 +20,14 @@ rust-embed = { version = "8", features = ["mime-guess"] } hex = "0.4" getrandom = "0.2" anyhow = "1" +rpassword = "7" +zeroize = "1" + +[target.'cfg(unix)'.dependencies] +rustix = { version = "1.1.4", features = ["fs"] } [dev-dependencies] tempfile = "3" hex = "0.4" +ed25519-dalek = "2" +tower = "0.5" diff --git a/crates/carapace-api/src/auth.rs b/crates/carapace-api/src/auth.rs index 33daf5f..f19969a 100644 --- a/crates/carapace-api/src/auth.rs +++ b/crates/carapace-api/src/auth.rs @@ -86,7 +86,7 @@ pub fn is_loopback_origin(origin: &str) -> bool { /// DNS-rebinding + CSRF guard applied to EVERY request (including the health check /// and the static GUI). Rejects a non-loopback `Host`, and a present-but-non-loopback /// `Origin`. -pub async fn guard_host_origin(req: Request, next: Next) -> Result { +pub async fn guard_host_origin(req: Request, next: Next) -> Result { let headers = req.headers(); let host_ok = headers .get(header::HOST) @@ -94,11 +94,17 @@ pub async fn guard_host_origin(req: Request, next: Next) -> Result>, req: Request, next: Next, -) -> Result { +) -> Result { let presented = bearer(req.headers().get(header::AUTHORIZATION)); match presented { Some(t) if ct_eq_str(t, &token) => Ok(next.run(req).await), - _ => Err(StatusCode::UNAUTHORIZED), + _ => Err(crate::handlers::error_response( + StatusCode::UNAUTHORIZED, + "missing or invalid token", + )), } } diff --git a/crates/carapace-api/src/bin/carapaced.rs b/crates/carapace-api/src/bin/carapaced.rs index 3c2392e..6d63d6a 100644 --- a/crates/carapace-api/src/bin/carapaced.rs +++ b/crates/carapace-api/src/bin/carapaced.rs @@ -2,6 +2,7 @@ //! //! Usage: //! carapaced run --state-dir [--publish [--watch] --vid <64-hex>] [--api-port ] +//! carapaced claimant --state-dir [--api-port ] //! //! `run` loads/generates the device state, starts the daemon (serving the blob //! store + `carapace/1` control protocol), optionally publishes a vault, starts the @@ -14,20 +15,145 @@ use std::sync::Arc; use anyhow::{bail, Context, Result}; use carapaced::{Daemon, NetConfig, State}; +use zeroize::Zeroizing; + +#[path = "carapaced/state_ops.rs"] +mod state_ops; /// Default bind for the embedded relay when `--relay` is given no explicit socket. const DEFAULT_RELAY_BIND: &str = "0.0.0.0:9991"; +trait PassphrasePrompt { + fn read(&self) -> Result>; +} + +struct ControllingTerminalPrompt; + +impl PassphrasePrompt for ControllingTerminalPrompt { + fn read(&self) -> Result> { + #[cfg(unix)] + let _terminal = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open("/dev/tty") + .context("terminal-passphrase mode requires a controlling terminal")?; + let passphrase = Zeroizing::new( + rpassword::prompt_password("Carapace identity passphrase: ") + .context("read passphrase from the controlling terminal")?, + ); + ensure_nonempty_passphrase(passphrase) + } +} + +fn ensure_nonempty_passphrase(passphrase: Zeroizing) -> Result> { + if passphrase.is_empty() { + bail!("the terminal passphrase is empty"); + } + Ok(passphrase) +} + +fn read_operator_passphrase(prompt: &dyn PassphrasePrompt) -> Result> { + ensure_nonempty_passphrase(prompt.read()?) +} + #[tokio::main] async fn main() -> Result<()> { let mut args = std::env::args().skip(1); match args.next().as_deref() { Some("run") => run(args.collect()).await, - Some(other) => bail!("unknown command {other:?}; try: carapaced run --state-dir "), - None => bail!( - "usage: carapaced run --state-dir [--publish --vid <64-hex>] [--api-port ]" - ), + Some("claimant") => claimant(args.collect()).await, + Some("inspect-state") => state_ops::inspect_state(args.collect()), + Some("initialize-empty") => state_ops::initialize_empty(args.collect()), + Some("restore-backup") => state_ops::restore_backup(args.collect()), + Some("reset-security-state") => state_ops::reset_security_state(args.collect()), + Some("migrate-legacy-state") => state_ops::migrate_legacy_state(args.collect()), + Some("migrate-keys") => migrate_keys(args.collect()), + Some(other) => { + bail!("unknown command {other:?}; try: carapaced run, claimant, or a state operator command") + } + None => bail!("usage: carapaced --state-dir "), + } +} + +#[derive(Debug, PartialEq, Eq)] +struct ClaimantArgs { + state_dir: PathBuf, + api_port: u16, +} + +fn parse_claimant_args(rest: Vec) -> Result { + let mut state_dir = None; + let mut api_port = 0; + let mut it = rest.into_iter(); + while let Some(flag) = it.next() { + match flag.as_str() { + "--state-dir" => { + state_dir = Some(it.next().context("--state-dir needs a value")?.into()) + } + "--api-port" => { + api_port = it + .next() + .context("--api-port needs a value")? + .parse() + .context("--api-port must be a u16 port")? + } + other => bail!("unknown claimant flag {other:?}"), + } + } + Ok(ClaimantArgs { + state_dir: state_dir.context("--state-dir is required")?, + api_port, + }) +} + +async fn claimant(rest: Vec) -> Result<()> { + let args = parse_claimant_args(rest)?; + let api = carapace_api::serve_claimant(&args.state_dir, args.api_port).await?; + println!("claimant API: {}", api.url()); + println!( + "claimant API token: {} (bearer, 0600)", + args.state_dir.join("claimant-api-token").display() + ); + println!("claimant recovery is active; press Ctrl-C to stop"); + wait_for_stop().await?; + api.shutdown(); + Ok(()) +} + +fn migrate_keys(rest: Vec) -> Result<()> { + let mut state_dir: Option = None; + let mut confirmed: Option = None; + let mut it = rest.into_iter(); + while let Some(flag) = it.next() { + match flag.as_str() { + "--state-dir" => { + state_dir = Some(it.next().context("--state-dir needs a value")?.into()) + } + "--confirm-state-dir" => { + confirmed = Some( + it.next() + .context("--confirm-state-dir needs a value")? + .into(), + ) + } + other => bail!("unknown migrate-keys flag {other:?}"), + } } + let state_dir = state_dir.context("--state-dir is required")?; + let confirmed = confirmed.context("--confirm-state-dir is required")?; + let canonical = std::fs::canonicalize(&state_dir) + .with_context(|| format!("resolve state directory {state_dir:?}"))?; + let confirmed_canonical = std::fs::canonicalize(&confirmed) + .with_context(|| format!("resolve confirmed state directory {confirmed:?}"))?; + if canonical != confirmed_canonical { + bail!("confirmed state directory does not match --state-dir"); + } + let backup = State::migrate_legacy_keys(&canonical)?; + println!( + "key migration complete; protected backup: {}", + backup.display() + ); + Ok(()) } async fn run(rest: Vec) -> Result<()> { @@ -40,6 +166,8 @@ async fn run(rest: Vec) -> Result<()> { let mut relays: Vec = Vec::new(); let mut run_relay: Option = None; let mut relay_host: Option = None; + let mut insecure_plaintext_keys = false; + let mut terminal_passphrase = false; let mut it = rest.into_iter().peekable(); while let Some(flag) = it.next() { @@ -80,6 +208,8 @@ async fn run(rest: Vec) -> Result<()> { "--relay-host" => { relay_host = Some(it.next().context("--relay-host needs a host or ip")?) } + "--insecure-plaintext-keys" => insecure_plaintext_keys = true, + "--terminal-passphrase" => terminal_passphrase = true, // A friend's self-hosted relay URL to consume (repeatable). These form // this node's usable relay set for relay fallback (§6). "--relay-url" => relays.push( @@ -93,8 +223,27 @@ async fn run(rest: Vec) -> Result<()> { } let state_dir = state_dir.context("--state-dir is required")?; - let state = State::load_or_generate(&state_dir)?; let networked = bind.is_some() || run_relay.is_some() || !relays.is_empty(); + if insecure_plaintext_keys && networked { + bail!("--insecure-plaintext-keys cannot be used with non-loopback or relay operation"); + } + if insecure_plaintext_keys && terminal_passphrase { + bail!("--terminal-passphrase cannot be combined with --insecure-plaintext-keys"); + } + let state = if insecure_plaintext_keys { + eprintln!( + "carapace: WARNING insecure development mode stores identity secrets in local files" + ); + State::load_or_generate_insecure(&state_dir)? + } else if terminal_passphrase { + eprintln!( + "carapace: WARNING terminal-passphrase mode stays locked after unattended restart; recovery takeover delay and alarms cannot run while locked" + ); + let passphrase = read_operator_passphrase(&ControllingTerminalPrompt)?; + State::load_protected_local(&state_dir, passphrase.as_bytes())? + } else { + State::load_or_generate(&state_dir)? + }; let daemon = Arc::new(if networked { let cfg = NetConfig { bind, @@ -195,3 +344,58 @@ fn hex(b: &[u8]) -> String { } s } + +#[cfg(test)] +mod tests { + use super::*; + + fn values(items: &[&str]) -> Vec { + items.iter().map(|item| (*item).to_string()).collect() + } + + #[test] + fn claimant_arguments_default_to_an_ephemeral_port() { + let parsed = parse_claimant_args(values(&["--state-dir", "/tmp/claimant"])).unwrap(); + assert_eq!(parsed.state_dir, PathBuf::from("/tmp/claimant")); + assert_eq!(parsed.api_port, 0); + } + + #[test] + fn claimant_arguments_accept_an_api_port() { + let parsed = parse_claimant_args(values(&[ + "--state-dir", + "/tmp/claimant", + "--api-port", + "4711", + ])) + .unwrap(); + assert_eq!(parsed.api_port, 4711); + } + + #[test] + fn claimant_arguments_reject_missing_and_extra_values() { + let missing = parse_claimant_args(Vec::new()).unwrap_err(); + assert!(missing.to_string().contains("--state-dir is required")); + let extra = + parse_claimant_args(values(&["--state-dir", "/tmp/c", "--bind", "x"])).unwrap_err(); + assert!(extra.to_string().contains("unknown claimant flag")); + } + + struct FakePrompt(&'static str); + impl PassphrasePrompt for FakePrompt { + fn read(&self) -> Result> { + Ok(Zeroizing::new(self.0.to_string())) + } + } + + #[test] + fn terminal_passphrase_prompt_rejects_empty_input() { + assert!(read_operator_passphrase(&FakePrompt("")).is_err()); + assert_eq!( + read_operator_passphrase(&FakePrompt("operator secret")) + .unwrap() + .as_str(), + "operator secret" + ); + } +} diff --git a/crates/carapace-api/src/bin/carapaced/state_ops.rs b/crates/carapace-api/src/bin/carapaced/state_ops.rs new file mode 100644 index 0000000..32d8d73 --- /dev/null +++ b/crates/carapace-api/src/bin/carapaced/state_ops.rs @@ -0,0 +1,581 @@ +//! Read-only state operator commands for the `carapaced` binary. + +use std::path::{Path, PathBuf}; + +use anyhow::{bail, Context, Result}; +use carapaced::{ + initialize_empty_state, inspect_existing_state, inspect_state_database, + migrate_legacy_state as migrate_legacy_database, State, +}; + +const AUDIT_LOG: &str = "operator-audit.log"; +const RESET_CONFIRMATION: &str = "RESET SECURITY STATE"; + +pub(crate) fn inspect_state(rest: Vec) -> Result<()> { + let (state_dir, insecure) = parse_inspect_args(rest)?; + let state = load_existing_identity(&state_dir, insecure)?; + let report = inspect_existing_state(&state)?; + + println!("state directory: {}", state_dir.display()); + println!("identity: valid"); + println!("database: valid"); + println!("owned vaults: {}", report.owned_vaults); + println!("held replicas: {}", report.held_replicas); + println!("held shares: {}", report.held_shares); + println!("recovery sets: {}", report.recovery_sets); + println!("ceremonies: {}", report.ceremonies); + Ok(()) +} + +pub(crate) fn migrate_legacy_state(rest: Vec) -> Result<()> { + let (state_dir, insecure) = parse_confirmed_existing_args(rest)?; + let state = load_existing_identity(&state_dir, insecure)?; + let backup = migrate_legacy_database(&state)?; + append_audit( + &state_dir, + "migrate-legacy-state", + "validated and migrated legacy state to schema 2; preserved backup", + )?; + println!( + "legacy state migration complete; protected backup: {}", + backup.display() + ); + Ok(()) +} + +fn parse_confirmed_existing_args(rest: Vec) -> Result<(PathBuf, bool)> { + let mut state_dir = None; + let mut confirmed = None; + let mut insecure = false; + let mut it = rest.into_iter(); + while let Some(flag) = it.next() { + match flag.as_str() { + "--state-dir" => state_dir = Some(next_path(&mut it, "--state-dir")?), + "--confirm-state-dir" => confirmed = Some(next_path(&mut it, "--confirm-state-dir")?), + "--insecure-plaintext-keys" => insecure = true, + other => bail!("unknown legacy migration flag {other:?}"), + } + } + Ok((confirmed_dir(state_dir, confirmed)?, insecure)) +} + +pub(crate) fn initialize_empty(rest: Vec) -> Result<()> { + let (state_dir, insecure) = parse_initialize_args(rest)?; + let state = load_or_create_identity(&state_dir, insecure)?; + append_audit( + &state_dir, + "initialize-empty-start", + "validated explicit state directory", + )?; + initialize_empty_state(&state)?; + append_audit( + &state_dir, + "initialize-empty", + "created identity-bound empty state", + )?; + println!("initialized empty state: {}", state_dir.display()); + Ok(()) +} + +pub(crate) fn restore_backup(rest: Vec) -> Result<()> { + let mut state_dir = None; + let mut backup = None; + let mut confirmed = None; + let mut insecure = false; + let mut it = rest.into_iter(); + while let Some(flag) = it.next() { + match flag.as_str() { + "--state-dir" => state_dir = Some(next_path(&mut it, "--state-dir")?), + "--backup" => backup = Some(next_path(&mut it, "--backup")?), + "--confirm-state-dir" => confirmed = Some(next_path(&mut it, "--confirm-state-dir")?), + "--insecure-plaintext-keys" => insecure = true, + other => bail!("unknown restore-backup flag {other:?}"), + } + } + let state_dir = confirmed_dir(state_dir, confirmed)?; + let backup = backup.context("--backup is required")?; + let state = load_existing_identity(&state_dir, insecure)?; + inspect_state_database(&state, &backup).context("validate backup before restore")?; + append_audit( + &state_dir, + "restore-backup-start", + &format!("validated backup {}", backup.display()), + )?; + + let active = state_dir.join("state.redb"); + let prior = state_dir.join("state.redb.before-restore"); + let staged = state_dir.join("state.redb.restore-staged"); + if prior.exists() || staged.exists() { + bail!("a prior restore artifact exists; inspect it before retrying"); + } + copy_durable(&backup, &staged)?; + inspect_state_database(&state, &staged).context("validate staged backup")?; + if active.exists() { + std::fs::rename(&active, &prior).context("preserve current state database")?; + } + if let Err(error) = std::fs::rename(&staged, &active) { + if prior.exists() { + let _ = std::fs::rename(&prior, &active); + } + return Err(error).context("activate restored state database"); + } + if let Err(error) = inspect_existing_state(&state) { + let failed = state_dir.join("state.redb.failed-restore"); + let _ = std::fs::rename(&active, &failed); + if prior.exists() { + std::fs::rename(&prior, &active).context("roll back failed restore")?; + } + sync_dir(&state_dir)?; + return Err(error).context("validate activated state database; prior state restored"); + } + sync_dir(&state_dir)?; + append_audit( + &state_dir, + "restore-backup", + &format!("restored {}; prior database preserved", backup.display()), + )?; + println!("restored state database: {}", active.display()); + if prior.exists() { + println!("prior database: {}", prior.display()); + } + Ok(()) +} + +pub(crate) fn reset_security_state(rest: Vec) -> Result<()> { + let (state_dir, insecure) = parse_reset_args(rest)?; + let state = load_existing_identity(&state_dir, insecure)?; + inspect_existing_state(&state).context("validate current state before reset")?; + + eprintln!("WARNING: reset-security-state clears rollback and replay history, fetch authorization, friendships, replica placement, and recovery ceremony state."); + eprintln!("The current database and encrypted blobs will move to a recoverable backup."); + eprintln!("Confirmed state directory: {}", state_dir.display()); + append_audit( + &state_dir, + "reset-security-state-start", + "validated current state and explicit destructive confirmation", + )?; + + let backup_dir = state_dir.join("security-reset-backup"); + if backup_dir.exists() { + bail!("reset backup already exists at {backup_dir:?}"); + } + create_private_dir(&backup_dir)?; + let active = state_dir.join("state.redb"); + let backup_db = backup_dir.join("state.redb"); + std::fs::rename(&active, &backup_db).context("preserve state database for reset")?; + let blobs = state_dir.join("blobs"); + let backup_blobs = backup_dir.join("blobs"); + if blobs.exists() { + if let Err(error) = std::fs::rename(&blobs, &backup_blobs) { + let _ = std::fs::rename(&backup_db, &active); + return Err(error).context("preserve blob store for reset"); + } + } + if let Err(error) = initialize_empty_state(&state) { + let _ = std::fs::remove_file(&active); + let _ = std::fs::rename(&backup_db, &active); + if backup_blobs.exists() { + let _ = std::fs::rename(&backup_blobs, &blobs); + } + return Err(error).context("initialize replacement security state"); + } + sync_dir(&state_dir)?; + append_audit( + &state_dir, + "reset-security-state", + "reset rollback, authorization, friendship, replica, and recovery state; preserved prior database and blobs", + )?; + println!( + "security state reset; recoverable backup: {}", + backup_dir.display() + ); + Ok(()) +} + +fn parse_inspect_args(rest: Vec) -> Result<(PathBuf, bool)> { + let mut state_dir = None; + let mut insecure = false; + let mut it = rest.into_iter(); + while let Some(flag) = it.next() { + match flag.as_str() { + "--state-dir" => { + state_dir = Some(it.next().context("--state-dir needs a value")?.into()) + } + "--insecure-plaintext-keys" => insecure = true, + other => bail!("unknown inspect-state flag {other:?}"), + } + } + Ok((state_dir.context("--state-dir is required")?, insecure)) +} + +fn parse_initialize_args(rest: Vec) -> Result<(PathBuf, bool)> { + let mut state_dir = None; + let mut confirmed = None; + let mut insecure = false; + let mut it = rest.into_iter(); + while let Some(flag) = it.next() { + match flag.as_str() { + "--state-dir" => state_dir = Some(next_path(&mut it, "--state-dir")?), + "--confirm-state-dir" => confirmed = Some(next_path(&mut it, "--confirm-state-dir")?), + "--insecure-plaintext-keys" => insecure = true, + other => bail!("unknown initialize-empty flag {other:?}"), + } + } + Ok((confirmed_dir(state_dir, confirmed)?, insecure)) +} + +fn parse_reset_args(rest: Vec) -> Result<(PathBuf, bool)> { + let mut state_dir = None; + let mut confirmed = None; + let mut phrase = None; + let mut insecure = false; + let mut it = rest.into_iter(); + while let Some(flag) = it.next() { + match flag.as_str() { + "--state-dir" => state_dir = Some(next_path(&mut it, "--state-dir")?), + "--confirm-state-dir" => confirmed = Some(next_path(&mut it, "--confirm-state-dir")?), + "--confirm-reset" => phrase = Some(it.next().context("--confirm-reset needs a value")?), + "--insecure-plaintext-keys" => insecure = true, + other => bail!("unknown reset-security-state flag {other:?}"), + } + } + if phrase.as_deref() != Some(RESET_CONFIRMATION) { + bail!("--confirm-reset must be exactly {RESET_CONFIRMATION:?}"); + } + Ok((confirmed_dir(state_dir, confirmed)?, insecure)) +} + +fn confirmed_dir(state_dir: Option, confirmed: Option) -> Result { + let state_dir = state_dir.context("--state-dir is required")?; + let confirmed = confirmed.context("--confirm-state-dir is required")?; + let state_dir = std::fs::canonicalize(&state_dir) + .with_context(|| format!("resolve state directory {state_dir:?}"))?; + let confirmed = std::fs::canonicalize(&confirmed) + .with_context(|| format!("resolve confirmed state directory {confirmed:?}"))?; + if state_dir != confirmed { + bail!("confirmed state directory does not match --state-dir"); + } + Ok(state_dir) +} + +fn next_path(it: &mut impl Iterator, flag: &str) -> Result { + Ok(it + .next() + .with_context(|| format!("{flag} needs a value"))? + .into()) +} + +fn load_or_create_identity(state_dir: &Path, insecure: bool) -> Result { + if insecure { + State::load_or_generate_insecure(state_dir) + } else { + State::load_or_generate(state_dir) + } +} + +fn load_existing_identity(state_dir: &Path, insecure: bool) -> Result { + let credential = state_dir.join("credential.id"); + let node = state_dir.join("node.key"); + let root = state_dir.join("root.key"); + + if insecure { + if !node.is_file() || !root.is_file() { + bail!( + "insecure state inspection requires existing root.key and node.key files in {state_dir:?}" + ); + } + return State::load_or_generate_insecure(state_dir); + } + + if !credential.is_file() { + bail!( + "secure state inspection requires an existing credential.id in {state_dir:?}; use --insecure-plaintext-keys only for an existing development state" + ); + } + if node.exists() || root.exists() { + bail!("legacy key files exist beside credential.id in {state_dir:?}"); + } + State::load_or_generate(state_dir) +} + +fn copy_durable(source: &Path, destination: &Path) -> Result<()> { + #[cfg(unix)] + let mut input = { + use rustix::fs::{openat, Mode, OFlags, CWD}; + let descriptor = openat( + CWD, + source, + OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC, + Mode::empty(), + ) + .map_err(std::io::Error::from) + .with_context(|| format!("open backup without following links {source:?}"))?; + std::fs::File::from(descriptor) + }; + #[cfg(not(unix))] + let mut input = + std::fs::File::open(source).with_context(|| format!("open backup {source:?}"))?; + let mut output = create_private_file(destination)?; + std::io::copy(&mut input, &mut output) + .with_context(|| format!("copy backup to {destination:?}"))?; + output.sync_all().context("sync staged state database")?; + sync_dir( + destination + .parent() + .context("staged database has no parent")?, + ) +} + +fn append_audit(state_dir: &Path, action: &str, detail: &str) -> Result<()> { + use std::io::Write; + let path = state_dir.join(AUDIT_LOG); + let mut file = open_private_append(&path)?; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .context("system clock is before Unix epoch")? + .as_secs(); + let action = action.replace(['\t', '\r', '\n'], " "); + let detail = detail.replace(['\t', '\r', '\n'], " "); + writeln!(file, "{now}\t{action}\t{detail}").context("write operator audit record")?; + file.sync_all().context("sync operator audit record")?; + sync_dir(state_dir) +} + +#[cfg(unix)] +fn create_private_file(path: &Path) -> Result { + use std::os::unix::fs::OpenOptionsExt; + std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .mode(0o600) + .open(path) + .with_context(|| format!("create private file {path:?}")) +} + +#[cfg(not(unix))] +fn create_private_file(path: &Path) -> Result { + std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(path) + .with_context(|| format!("create private file {path:?}")) +} + +#[cfg(unix)] +fn open_private_append(path: &Path) -> Result { + use std::os::unix::fs::OpenOptionsExt; + std::fs::OpenOptions::new() + .append(true) + .create(true) + .mode(0o600) + .open(path) + .with_context(|| format!("open private audit log {path:?}")) +} + +#[cfg(not(unix))] +fn open_private_append(path: &Path) -> Result { + std::fs::OpenOptions::new() + .append(true) + .create(true) + .open(path) + .with_context(|| format!("open audit log {path:?}")) +} + +#[cfg(unix)] +fn create_private_dir(path: &Path) -> Result<()> { + use std::os::unix::fs::DirBuilderExt; + std::fs::DirBuilder::new() + .mode(0o700) + .create(path) + .with_context(|| format!("create private backup directory {path:?}")) +} + +#[cfg(not(unix))] +fn create_private_dir(path: &Path) -> Result<()> { + std::fs::create_dir(path).with_context(|| format!("create backup directory {path:?}")) +} + +#[cfg(unix)] +fn sync_dir(path: &Path) -> Result<()> { + std::fs::File::open(path) + .with_context(|| format!("open directory {path:?} for sync"))? + .sync_all() + .with_context(|| format!("sync directory {path:?}")) +} + +#[cfg(not(unix))] +fn sync_dir(_path: &Path) -> Result<()> { + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn inspect_requires_an_explicit_state_directory() { + let error = parse_inspect_args(Vec::new()).unwrap_err(); + assert!(error.to_string().contains("--state-dir is required")); + } + + #[test] + fn legacy_migration_requires_the_matching_state_directory() { + let root = tempfile::tempdir().unwrap(); + let state = root.path().join("state"); + let other = root.path().join("other"); + std::fs::create_dir(&state).unwrap(); + std::fs::create_dir(&other).unwrap(); + let missing = parse_confirmed_existing_args(vec![ + "--state-dir".to_string(), + state.display().to_string(), + ]) + .unwrap_err(); + assert!(missing + .to_string() + .contains("--confirm-state-dir is required")); + + let mismatch = parse_confirmed_existing_args(vec![ + "--state-dir".to_string(), + state.display().to_string(), + "--confirm-state-dir".to_string(), + other.display().to_string(), + ]) + .unwrap_err(); + assert!(mismatch + .to_string() + .contains("confirmed state directory does not match")); + } + + #[test] + fn inspect_refuses_to_create_an_identity() { + let dir = tempfile::tempdir().unwrap(); + let error = match load_existing_identity(dir.path(), false) { + Err(error) => error, + Ok(_) => panic!("inspection unexpectedly created an identity"), + }; + + assert!(error.to_string().contains("credential.id")); + assert!(!dir.path().join("credential.id").exists()); + assert!(!dir.path().join("root.key").exists()); + assert!(!dir.path().join("node.key").exists()); + } + + #[test] + fn insecure_inspect_requires_a_complete_existing_pair() { + let dir = tempfile::tempdir().unwrap(); + std::fs::write(dir.path().join("root.key"), [0u8; 32]).unwrap(); + + let error = match load_existing_identity(dir.path(), true) { + Err(error) => error, + Ok(_) => panic!("inspection unexpectedly accepted an incomplete identity"), + }; + assert!(error.to_string().contains("root.key and node.key")); + assert!(!dir.path().join("node.key").exists()); + } + + fn insecure_args(command: &str, dir: &Path) -> Vec { + let mut args = vec![ + "--state-dir".to_string(), + dir.display().to_string(), + "--insecure-plaintext-keys".to_string(), + ]; + if matches!(command, "initialize" | "restore" | "reset") { + args.extend(["--confirm-state-dir".to_string(), dir.display().to_string()]); + } + if command == "reset" { + args.extend([ + "--confirm-reset".to_string(), + RESET_CONFIRMATION.to_string(), + ]); + } + args + } + + #[test] + fn initialize_creates_valid_state_and_an_audit_record() { + let dir = tempfile::tempdir().unwrap(); + initialize_empty(insecure_args("initialize", dir.path())).unwrap(); + + assert!(dir.path().join("state.redb").is_file()); + assert!(dir.path().join(AUDIT_LOG).is_file()); + inspect_state(insecure_args("inspect", dir.path())).unwrap(); + assert!(initialize_empty(insecure_args("initialize", dir.path())).is_err()); + } + + #[test] + fn reset_preserves_database_and_blob_data() { + let dir = tempfile::tempdir().unwrap(); + initialize_empty(insecure_args("initialize", dir.path())).unwrap(); + std::fs::create_dir(dir.path().join("blobs")).unwrap(); + std::fs::write(dir.path().join("blobs/retained"), b"ciphertext").unwrap(); + + reset_security_state(insecure_args("reset", dir.path())).unwrap(); + + let backup = dir.path().join("security-reset-backup"); + assert!(backup.join("state.redb").is_file()); + assert_eq!( + std::fs::read(backup.join("blobs/retained")).unwrap(), + b"ciphertext" + ); + assert!(dir.path().join("state.redb").is_file()); + inspect_state(insecure_args("inspect", dir.path())).unwrap(); + let audit = std::fs::read_to_string(dir.path().join(AUDIT_LOG)).unwrap(); + assert!(audit.contains("reset-security-state-start")); + assert!(audit.contains("\treset-security-state\t")); + } + + #[test] + fn reset_requires_the_exact_confirmation_phrase() { + let dir = tempfile::tempdir().unwrap(); + let mut args = insecure_args("restore", dir.path()); + args.extend(["--confirm-reset".to_string(), "yes".to_string()]); + + let error = parse_reset_args(args).unwrap_err(); + assert!(error.to_string().contains(RESET_CONFIRMATION)); + } + + #[test] + fn restore_validates_before_it_replaces_current_state() { + let dir = tempfile::tempdir().unwrap(); + initialize_empty(insecure_args("initialize", dir.path())).unwrap(); + let backup = dir.path().join("external-backup.redb"); + std::fs::copy(dir.path().join("state.redb"), &backup).unwrap(); + let mut args = insecure_args("restore", dir.path()); + args.extend(["--backup".to_string(), backup.display().to_string()]); + + restore_backup(args).unwrap(); + + assert!(dir.path().join("state.redb.before-restore").is_file()); + inspect_state(insecure_args("inspect", dir.path())).unwrap(); + assert!(std::fs::read_to_string(dir.path().join(AUDIT_LOG)) + .unwrap() + .contains("\trestore-backup\t")); + } + + #[test] + fn restore_and_reset_interruption_leave_start_without_completion() { + let restore_dir = tempfile::tempdir().unwrap(); + initialize_empty(insecure_args("initialize", restore_dir.path())).unwrap(); + let backup = restore_dir.path().join("external-backup.redb"); + std::fs::copy(restore_dir.path().join("state.redb"), &backup).unwrap(); + std::fs::write( + restore_dir.path().join("state.redb.restore-staged"), + b"stop", + ) + .unwrap(); + let mut restore_args = insecure_args("restore", restore_dir.path()); + restore_args.extend(["--backup".to_string(), backup.display().to_string()]); + assert!(restore_backup(restore_args).is_err()); + let restore_audit = std::fs::read_to_string(restore_dir.path().join(AUDIT_LOG)).unwrap(); + assert!(restore_audit.contains("restore-backup-start")); + assert!(!restore_audit.contains("\trestore-backup\t")); + + let reset_dir = tempfile::tempdir().unwrap(); + initialize_empty(insecure_args("initialize", reset_dir.path())).unwrap(); + std::fs::create_dir(reset_dir.path().join("security-reset-backup")).unwrap(); + assert!(reset_security_state(insecure_args("reset", reset_dir.path())).is_err()); + let reset_audit = std::fs::read_to_string(reset_dir.path().join(AUDIT_LOG)).unwrap(); + assert!(reset_audit.contains("reset-security-state-start")); + assert!(!reset_audit.contains("\treset-security-state\t")); + } +} diff --git a/crates/carapace-api/src/claimant.rs b/crates/carapace-api/src/claimant.rs new file mode 100644 index 0000000..2a4b24b --- /dev/null +++ b/crates/carapace-api/src/claimant.rs @@ -0,0 +1,633 @@ +//! A separate loopback API for a device that does not yet have an identity. +//! +//! This router cannot call normal daemon handlers because its state has no `Daemon`. +//! The process owns the claimant ceremony key and node seed. The browser receives only +//! their public values. On completion, the process reconstructs `K_root` in memory and +//! sends it directly to the operating-system credential-store activation boundary. + +use std::{ + fs, + io::Write, + path::{Path, PathBuf}, + sync::Arc, +}; + +use anyhow::{Context, Result}; +use axum::{ + extract::State, + http::{header, StatusCode}, + response::{IntoResponse, Response}, + Json, +}; +use carapace_wire::{messages::Message as _, messages::Signed as _, RecoveryOpen}; +use carapaced::{activate_recovered_identity, ClaimantDevice}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use tokio::sync::Mutex; + +const CLAIMANT_HTML: &[u8] = include_bytes!("../static/claimant.html"); +const CLAIMANT_SCRIPT: &[u8] = include_bytes!("../static/claimant.js"); +const CLAIMANT_STYLESHEET: &[u8] = include_bytes!("../static/claimant.css"); + +fn claimant_asset(path: &str) -> Option<&'static [u8]> { + match path { + "claimant.html" => Some(CLAIMANT_HTML), + "claimant.js" => Some(CLAIMANT_SCRIPT), + "claimant.css" => Some(CLAIMANT_STYLESHEET), + _ => None, + } +} + +/// State for the claimant-only server. It deliberately has no normal daemon. +#[derive(Clone)] +pub struct ClaimantState { + pub(crate) claimant: Arc>>, + pub(crate) state_dir: Arc, + pub(crate) token: Arc, +} + +impl ClaimantState { + pub(crate) fn new(state_dir: PathBuf, token: Arc) -> Result { + Ok(Self { + claimant: Arc::new(Mutex::new(Some(ClaimantDevice::new()?))), + state_dir: Arc::new(state_dir), + token, + }) + } +} + +#[derive(Clone, Deserialize, Serialize)] +pub(crate) struct TrusteeAddress { + node: String, + #[serde(default)] + addrs: Vec, +} + +#[derive(Clone, Deserialize, Serialize)] +pub(crate) struct RecoveryAnnounceRef { + vid: String, + epoch: u64, + digest: String, +} + +#[derive(Deserialize)] +pub(crate) struct CompleteRequest { + open_hex: String, + confirmed_subject: String, + roster: Vec, + trustees: Vec, + #[serde(default)] + announce_refs: Vec, +} + +#[derive(Deserialize)] +pub(crate) struct PreviewRequest { + open_hex: String, +} + +fn bad(message: impl Into) -> ClaimantError { + ClaimantError(StatusCode::BAD_REQUEST, message.into()) +} + +#[derive(Debug)] +pub(crate) struct ClaimantError(StatusCode, String); + +impl IntoResponse for ClaimantError { + fn into_response(self) -> Response { + crate::handlers::error_response(self.0, self.1) + } +} + +/// Serve the claimant-only shell. The token is a fixed-length hexadecimal value. +pub(crate) async fn shell(State(state): State) -> Response { + let Some(asset) = claimant_asset("claimant.html") else { + return StatusCode::NOT_FOUND.into_response(); + }; + let html = String::from_utf8_lossy(asset).replace("__CARAPACE_CLAIMANT_TOKEN__", &state.token); + ( + [ + (header::CONTENT_TYPE, "text/html; charset=utf-8"), + (header::CACHE_CONTROL, "no-store"), + (header::X_CONTENT_TYPE_OPTIONS, "nosniff"), + ( + header::CONTENT_SECURITY_POLICY, + "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'", + ), + ], + html, + ) + .into_response() +} + +/// Serve one fixed claimant asset. No normal GUI asset fallback is available here. +pub(crate) async fn asset(path: &'static str, content_type: &'static str) -> Response { + let Some(asset) = claimant_asset(path) else { + return StatusCode::NOT_FOUND.into_response(); + }; + ( + [ + (header::CONTENT_TYPE, content_type), + (header::CACHE_CONTROL, "no-store"), + (header::X_CONTENT_TYPE_OPTIONS, "nosniff"), + ], + asset, + ) + .into_response() +} + +pub(crate) async fn script() -> Response { + asset("claimant.js", "text/javascript; charset=utf-8").await +} + +pub(crate) async fn stylesheet() -> Response { + asset("claimant.css", "text/css; charset=utf-8").await +} + +/// Return the public inputs that a sponsor must put in `RecoveryOpen`. +pub(crate) async fn status(State(state): State) -> Json { + let guard = state.claimant.lock().await; + match guard.as_ref() { + Some(claimant) => { + let ceremony_enc = hex::encode(claimant.ceremony_enc()); + let new_node = hex::encode(claimant.new_node()); + let handoff = json!({ + "type": "carapace.claimant-handoff", + "version": 1, + "ceremony_enc": ceremony_enc.clone(), + "new_node": new_node.clone(), + }) + .to_string(); + Json(json!({ + "phase": "waiting_for_open", + "handoff": handoff, + "ceremony_enc": ceremony_enc, + "new_node": new_node, + })) + } + None => Json(json!({ + "phase": "activation_complete", + "restart_required": true, + })), + } +} + +/// Cancel the current attempt, drop its secret keys, and create a fresh retry session. +pub(crate) async fn cancel( + State(state): State, +) -> Result, ClaimantError> { + let fresh = ClaimantDevice::new().map_err(|_| { + ClaimantError( + StatusCode::INTERNAL_SERVER_ERROR, + "could not create a fresh claimant session".into(), + ) + })?; + let old = state.claimant.lock().await.replace(fresh); + drop(old); + crate::ops::log("claimant.cancelled", None); + Ok(Json(json!({ + "phase": "waiting_for_open", + "cancelled": true, + "retry_ready": true, + }))) +} + +fn decode_open(open_hex: &str) -> Result { + let open_bytes = + hex::decode(open_hex).map_err(|error| bad(format!("invalid open hex: {error}")))?; + let open = RecoveryOpen::decode_frame(&open_bytes) + .map_err(|error| bad(format!("invalid recovery open: {error}")))?; + open.verify() + .map_err(|_| bad("the recovery open signature is invalid"))?; + Ok(open) +} + +/// Verify a signed open and show its subject before any share collection or activation. +pub(crate) async fn preview( + State(state): State, + Json(request): Json, +) -> Result, ClaimantError> { + let open = decode_open(&request.open_hex)?; + let guard = state.claimant.lock().await; + let claimant = guard.as_ref().ok_or_else(|| { + ClaimantError( + StatusCode::CONFLICT, + "claimant activation is complete".into(), + ) + })?; + if open.ceremony_enc != claimant.ceremony_enc() || open.new_node != claimant.new_node() { + return Err(bad("the recovery open does not name this claimant session")); + } + Ok(Json(json!({ + "subject": hex::encode(open.subject), + "sponsor": hex::encode(open.by), + "claimant_display": open.claimant_display, + "reason": open.reason, + "session_bound": true, + }))) +} + +/// Collect sealed shares, reconstruct in memory, and activate secure local state. +pub(crate) async fn complete( + State(state): State, + Json(request): Json, +) -> Result, ClaimantError> { + let open = decode_open(&request.open_hex)?; + let confirmed_subject = decode_32("confirmed subject", &request.confirmed_subject)?; + if confirmed_subject != open.subject { + return Err(bad( + "the confirmed subject does not match the signed recovery open", + )); + } + let roster = request + .roster + .iter() + .map(|value| decode_32("roster user", value)) + .collect::, _>>()?; + let trustees = request + .trustees + .iter() + .map(|trustee| { + Ok(( + decode_32("trustee node", &trustee.node)?, + trustee.addrs.clone(), + )) + }) + .collect::, ClaimantError>>()?; + if roster.is_empty() { + return Err(bad("the trustee roster is empty")); + } + if !roster.contains(&open.by) { + return Err(bad("the recovery-open signer is not in the trustee roster")); + } + if trustees.is_empty() { + return Err(bad("the trustee address list is empty")); + } + + // Take the claimant out during the network operation. This prevents two requests + // from collecting and activating the same ceremony at the same time. + let claimant = state.claimant.lock().await.take().ok_or_else(|| { + ClaimantError( + StatusCode::CONFLICT, + "claimant activation is already complete or active".into(), + ) + })?; + + if open.ceremony_enc != claimant.ceremony_enc() || open.new_node != claimant.new_node() { + *state.claimant.lock().await = Some(claimant); + return Err(bad("the recovery open does not name this claimant session")); + } + + let recovered = match claimant.recover_at(&open, &roster, &trustees).await { + Ok(recovered) => recovered, + Err(error) => { + let _ = error; + crate::ops::log("claimant.recovery_failed", None); + *state.claimant.lock().await = Some(claimant); + return Err(ClaimantError( + StatusCode::BAD_GATEWAY, + "recovery did not complete".into(), + )); + } + }; + if recovered.user_id != open.subject || recovered.new_node != open.new_node { + *state.claimant.lock().await = Some(claimant); + return Err(bad( + "the recovered identity does not match the recovery open", + )); + } + + let user_id = recovered.user_id; + let new_node = recovered.new_node; + let node_seed = claimant.node_seed(); + if persist_restart_handoff(&state.state_dir, &request.trustees, &request.announce_refs).is_err() + { + *state.claimant.lock().await = Some(claimant); + return Err(ClaimantError( + StatusCode::INTERNAL_SERVER_ERROR, + "the restart recovery handoff could not be saved".into(), + )); + } + let activated = + match activate_recovered_identity(&state.state_dir, node_seed, *recovered.k_root) + .context("activate recovered identity") + { + Ok(activated) => activated, + Err(error) => { + let _ = error; + let _ = fs::remove_file(state.state_dir.join("recovery-restart-handoff.json")); + crate::ops::log("claimant.activation_failed", None); + *state.claimant.lock().await = Some(claimant); + return Err(ClaimantError( + StatusCode::INTERNAL_SERVER_ERROR, + "recovered identity activation failed".into(), + )); + } + }; + drop(activated); + drop(recovered); + drop(claimant); + + Ok(Json(json!({ + "phase": "activation_complete", + "user_id": hex::encode(user_id), + "new_node": hex::encode(new_node), + "restart_required": true, + }))) +} + +#[derive(Serialize)] +struct RestartHandoff<'a> { + r#type: &'static str, + version: u8, + trustees: &'a [TrusteeAddress], + announce_refs: &'a [RecoveryAnnounceRef], +} + +fn persist_restart_handoff( + state_dir: &Path, + trustees: &[TrusteeAddress], + announce_refs: &[RecoveryAnnounceRef], +) -> Result<()> { + fs::create_dir_all(state_dir)?; + let path = state_dir.join("recovery-restart-handoff.json"); + let temporary = state_dir.join(".recovery-restart-handoff.tmp"); + let bytes = serde_json::to_vec(&RestartHandoff { + r#type: "carapace.recovery-restart-handoff", + version: 1, + trustees, + announce_refs, + })?; + let mut options = fs::OpenOptions::new(); + options.write(true).create(true).truncate(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let mut file = options.open(&temporary)?; + file.write_all(&bytes)?; + file.sync_all()?; + fs::rename(temporary, path)?; + Ok(()) +} + +fn decode_32(label: &str, value: &str) -> Result<[u8; 32], ClaimantError> { + let bytes = hex::decode(value).map_err(|error| bad(format!("invalid {label} hex: {error}")))?; + bytes + .try_into() + .map_err(|_| bad(format!("{label} must contain 32 bytes"))) +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::{body::to_bytes, http::Request}; + use ed25519_dalek::SigningKey; + use tower::ServiceExt; + + fn test_state(dir: PathBuf) -> ClaimantState { + ClaimantState::new(dir, Arc::from("test-token")).unwrap() + } + + #[tokio::test] + async fn status_exposes_only_public_claimant_values() { + let dir = tempfile::tempdir().unwrap(); + let Json(value) = status(State(test_state(dir.path().to_path_buf()))).await; + let object = value.as_object().unwrap(); + assert_eq!(object.get("phase").unwrap(), "waiting_for_open"); + assert_eq!( + object.get("ceremony_enc").unwrap().as_str().unwrap().len(), + 64 + ); + assert_eq!(object.get("new_node").unwrap().as_str().unwrap().len(), 64); + let handoff: Value = serde_json::from_str(object["handoff"].as_str().unwrap()).unwrap(); + assert_eq!(handoff["type"], "carapace.claimant-handoff"); + assert_eq!(handoff["version"], 1); + assert_eq!(handoff["ceremony_enc"], object["ceremony_enc"]); + assert_eq!(handoff["new_node"], object["new_node"]); + for secret_name in ["k_root", "node_seed", "ceremony_private", "shares"] { + assert!( + !object.contains_key(secret_name), + "status exposed {secret_name}" + ); + } + } + + #[tokio::test] + async fn complete_rejects_an_open_for_another_claimant_before_network_access() { + let dir = tempfile::tempdir().unwrap(); + let state = test_state(dir.path().to_path_buf()); + let sponsor = SigningKey::from_bytes(&[5; 32]); + let mut open = RecoveryOpen { + ceremony_id: [1; 16], + subject: [2; 32], + rsid: 1, + claimant_display: "Test claimant".into(), + ceremony_enc: [3; 32], + new_node: [4; 32], + reason: "Test recovery".into(), + opened_at: 1, + by: [0; 32], + sig: [0; 64], + }; + open.sign(&sponsor); + let request = CompleteRequest { + open_hex: hex::encode(open.encode_frame()), + confirmed_subject: hex::encode(open.subject), + roster: vec![hex::encode(sponsor.verifying_key().to_bytes())], + trustees: vec![TrusteeAddress { + node: hex::encode([6; 32]), + addrs: Vec::new(), + }], + announce_refs: Vec::new(), + }; + let error = complete(State(state.clone()), Json(request)) + .await + .unwrap_err(); + assert_eq!(error.0, StatusCode::BAD_REQUEST); + assert!(error.1.contains("does not name this claimant session")); + assert!( + state.claimant.lock().await.is_some(), + "claimant must remain usable" + ); + } + + #[tokio::test] + async fn complete_rejects_an_invalid_open_signature_before_network_access() { + let dir = tempfile::tempdir().unwrap(); + let state = test_state(dir.path().to_path_buf()); + let (ceremony_enc, new_node) = { + let guard = state.claimant.lock().await; + let claimant = guard.as_ref().unwrap(); + (claimant.ceremony_enc(), claimant.new_node()) + }; + let sponsor = SigningKey::from_bytes(&[7; 32]); + let open = RecoveryOpen { + ceremony_id: [1; 16], + subject: [2; 32], + rsid: 1, + claimant_display: "Test claimant".into(), + ceremony_enc, + new_node, + reason: "Test recovery".into(), + opened_at: 1, + by: sponsor.verifying_key().to_bytes(), + sig: [0; 64], + }; + let request = CompleteRequest { + open_hex: hex::encode(open.encode_frame()), + confirmed_subject: hex::encode(open.subject), + roster: vec![hex::encode(sponsor.verifying_key().to_bytes())], + trustees: vec![TrusteeAddress { + node: hex::encode([6; 32]), + addrs: Vec::new(), + }], + announce_refs: Vec::new(), + }; + let error = complete(State(state.clone()), Json(request)) + .await + .unwrap_err(); + assert_eq!(error.0, StatusCode::BAD_REQUEST); + assert_eq!(error.1, "the recovery open signature is invalid"); + assert!(state.claimant.lock().await.is_some()); + } + + #[tokio::test] + async fn preview_shows_the_signed_subject_and_complete_requires_that_confirmation() { + let dir = tempfile::tempdir().unwrap(); + let state = test_state(dir.path().to_path_buf()); + let (ceremony_enc, new_node) = { + let guard = state.claimant.lock().await; + let claimant = guard.as_ref().unwrap(); + (claimant.ceremony_enc(), claimant.new_node()) + }; + let sponsor = SigningKey::from_bytes(&[8; 32]); + let mut open = RecoveryOpen { + ceremony_id: [1; 16], + subject: [9; 32], + rsid: 1, + claimant_display: "Test claimant".into(), + ceremony_enc, + new_node, + reason: "Test recovery".into(), + opened_at: 1, + by: [0; 32], + sig: [0; 64], + }; + open.sign(&sponsor); + let open_hex = hex::encode(open.encode_frame()); + let Json(value) = preview( + State(state.clone()), + Json(PreviewRequest { + open_hex: open_hex.clone(), + }), + ) + .await + .unwrap(); + assert_eq!(value["subject"], hex::encode(open.subject)); + assert_eq!(value["session_bound"], true); + + let error = complete( + State(state.clone()), + Json(CompleteRequest { + open_hex, + confirmed_subject: hex::encode([7; 32]), + roster: vec![hex::encode(sponsor.verifying_key().to_bytes())], + trustees: vec![TrusteeAddress { + node: hex::encode([6; 32]), + addrs: Vec::new(), + }], + announce_refs: Vec::new(), + }), + ) + .await + .unwrap_err(); + assert_eq!(error.0, StatusCode::BAD_REQUEST); + assert!(error.1.contains("confirmed subject does not match")); + assert!(state.claimant.lock().await.is_some()); + } + + #[tokio::test] + async fn claimant_router_serves_only_the_claimant_shell_and_api() { + let dir = tempfile::tempdir().unwrap(); + let state = test_state(dir.path().to_path_buf()); + let app = crate::claimant_app(state); + let response = app + .clone() + .oneshot( + Request::builder() + .uri("/") + .header(header::HOST, "127.0.0.1") + .body(axum::body::Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.headers()[header::CACHE_CONTROL], "no-store"); + let body = to_bytes(response.into_body(), 1_000_000).await.unwrap(); + let html = String::from_utf8(body.to_vec()).unwrap(); + assert!(html.contains("Claimant mode")); + assert!(html.contains("carapace-claimant-token")); + for forbidden in ["K_root", "node_seed", "ceremony_private", "share_json"] { + assert!( + !html.contains(forbidden), + "claimant shell exposed {forbidden}" + ); + } + + let response = app + .oneshot( + Request::builder() + .uri("/api/status") + .header(header::HOST, "127.0.0.1") + .body(axum::body::Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } + + #[tokio::test] + async fn cancellation_replaces_keys_and_leaves_a_safe_retry_session() { + let dir = tempfile::tempdir().unwrap(); + let state = test_state(dir.path().to_path_buf()); + let before = state.claimant.lock().await.as_ref().unwrap().ceremony_enc(); + let Json(result) = cancel(State(state.clone())).await.unwrap(); + let after = state.claimant.lock().await.as_ref().unwrap().ceremony_enc(); + assert_ne!(before, after); + assert_eq!(result["cancelled"], true); + assert_eq!(result["retry_ready"], true); + } + + #[test] + fn restart_handoff_is_public_versioned_and_private_on_unix() { + let dir = tempfile::tempdir().unwrap(); + let trustees = vec![TrusteeAddress { + node: hex::encode([3; 32]), + addrs: vec!["127.0.0.1:9000".into()], + }]; + let refs = vec![RecoveryAnnounceRef { + vid: hex::encode([4; 32]), + epoch: 7, + digest: hex::encode([5; 32]), + }]; + persist_restart_handoff(dir.path(), &trustees, &refs).unwrap(); + let path = dir.path().join("recovery-restart-handoff.json"); + let text = fs::read_to_string(&path).unwrap(); + assert!(text.contains("carapace.recovery-restart-handoff")); + assert!(text.contains("\"epoch\":7")); + for secret in ["k_root", "node_seed", "share_json", "ceremony_private"] { + assert!(!text.contains(secret)); + } + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + assert_eq!( + fs::metadata(path).unwrap().permissions().mode() & 0o777, + 0o600 + ); + } + } +} diff --git a/crates/carapace-api/src/handlers.rs b/crates/carapace-api/src/handlers.rs index 5f58cd6..10e3c8e 100644 --- a/crates/carapace-api/src/handlers.rs +++ b/crates/carapace-api/src/handlers.rs @@ -8,17 +8,18 @@ use std::time::Duration; use axum::{ extract::{ ws::{Message, WebSocket, WebSocketUpgrade}, - Path, Query, State, + Path, State, }, - http::{header, StatusCode, Uri}, + http::{header, HeaderMap, StatusCode, Uri}, response::{IntoResponse, Response}, Json, }; use carapace_wire::messages::Message as _; -use carapace_wire::{FileGrant, InviteTicket}; +use carapace_wire::{AnnounceRef, FileGrant, InviteTicket}; use carapaced::{Daemon, PendingResplitStatus, RecoveryScope, ResplitStatus}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; +use std::sync::atomic::{AtomicU64, Ordering}; use crate::{auth, AppState}; @@ -40,7 +41,7 @@ pub async fn static_asset(State(st): State, uri: Uri) -> Response { // An unmatched `/api/*` path is a missing endpoint, not a client route: 404 JSON, // never the token-injected shell. Only non-`/api` paths get the SPA fallback. if path == "api" || path.starts_with("api/") { - return (StatusCode::NOT_FOUND, Json(json!({ "error": "not found" }))).into_response(); + return error_response(StatusCode::NOT_FOUND, "not found"); } // `index.html` must always route through injection, never be served raw. if path.is_empty() || path == "index.html" { @@ -120,12 +121,14 @@ fn serve_index(token: &str) -> Response { base-uri 'none'; \ frame-ancestors 'none'" ); + let cookie = format!("carapace_session={token}; HttpOnly; SameSite=Strict; Path=/api/events"); ( [ (header::CONTENT_TYPE, "text/html; charset=utf-8"), (header::CONTENT_SECURITY_POLICY, csp.as_str()), (header::X_CONTENT_TYPE_OPTIONS, "nosniff"), (header::CACHE_CONTROL, "no-store"), + (header::SET_COOKIE, cookie.as_str()), ], rendered, ) @@ -134,24 +137,92 @@ fn serve_index(token: &str) -> Response { // ---- error type -------------------------------------------------------- +/// Sequence for references that connect a safe client error to the server log. +static ERROR_REFERENCE: AtomicU64 = AtomicU64::new(1); + /// A handler error rendered as a JSON body with an HTTP status. -pub struct ApiError(StatusCode, String); +pub struct ApiError { + status: StatusCode, + client_message: String, + internal: Option, +} + +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum ApiErrorCode { + BadRequest, + Unauthorized, + Forbidden, + NotFound, + Conflict, + UpstreamFailure, + Internal, +} + +#[derive(Serialize)] +pub(crate) struct ApiErrorBody { + pub(crate) code: ApiErrorCode, + pub(crate) error: String, +} + +pub(crate) fn error_response(status: StatusCode, message: impl Into) -> Response { + let code = match status { + StatusCode::BAD_REQUEST => ApiErrorCode::BadRequest, + StatusCode::UNAUTHORIZED => ApiErrorCode::Unauthorized, + StatusCode::FORBIDDEN => ApiErrorCode::Forbidden, + StatusCode::NOT_FOUND => ApiErrorCode::NotFound, + StatusCode::CONFLICT => ApiErrorCode::Conflict, + StatusCode::BAD_GATEWAY | StatusCode::SERVICE_UNAVAILABLE => ApiErrorCode::UpstreamFailure, + _ => ApiErrorCode::Internal, + }; + ( + status, + Json(ApiErrorBody { + code, + error: message.into(), + }), + ) + .into_response() +} + +impl ApiError { + fn client(status: StatusCode, message: impl Into) -> Self { + Self { + status, + client_message: message.into(), + internal: None, + } + } +} impl IntoResponse for ApiError { fn into_response(self) -> Response { - (self.0, Json(json!({ "error": self.1 }))).into_response() + let message = match self.internal { + Some(detail) => { + let reference = ERROR_REFERENCE.fetch_add(1, Ordering::Relaxed); + let _ = detail; + crate::ops::log("api.internal_error", Some(reference)); + format!("internal server error; reference {reference}") + } + None => self.client_message, + }; + error_response(self.status, message) } } impl From for ApiError { fn from(e: anyhow::Error) -> Self { - ApiError(StatusCode::INTERNAL_SERVER_ERROR, format!("{e:#}")) + Self { + status: StatusCode::INTERNAL_SERVER_ERROR, + client_message: "internal server error".to_string(), + internal: Some(format!("{e:#}")), + } } } /// A 400 from a bad-input message. fn bad(msg: impl Into) -> ApiError { - ApiError(StatusCode::BAD_REQUEST, msg.into()) + ApiError::client(StatusCode::BAD_REQUEST, msg) } fn hexs(b: &[u8]) -> String { @@ -184,47 +255,256 @@ pub async fn health() -> Json { Json(json!({ "ok": true })) } +#[derive(Clone, Debug, Serialize)] +pub struct PeerOptionResponse { + user: String, + display: String, + node: String, + addrs: Vec, +} + +#[derive(Clone, Debug, Serialize)] +pub struct PublishedVaultResponse { + vid: String, + epoch: u64, + name: String, +} + +#[derive(Clone, Debug, Serialize)] +struct FriendGrantResponse { + user: String, + grant_bytes: u64, +} +#[derive(Clone, Debug, Serialize)] +struct FriendsResponse { + count: usize, + list: Vec, + grants: Vec, +} +#[derive(Clone, Debug, Serialize)] +struct VaultsResponse { + published: Vec, + held_replicas: Vec, +} +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +enum RecoveryScopeResponse { + Root, + Vault { vid: String }, +} +#[derive(Clone, Debug, Serialize)] +struct TrusteeDeliveryResponse { + user: String, + delivered: bool, +} +#[derive(Clone, Debug, Serialize)] +struct RecoverySetResponse { + rsid: u64, + scope: RecoveryScopeResponse, + threshold: usize, + issued: usize, + trustees: Vec, + warnings: Vec, +} +#[derive(Clone, Debug, Serialize)] +struct RecoveryHealthResponse { + rsid: u64, + live: usize, + target: usize, + recommendation: String, + needed: usize, +} +#[derive(Clone, Debug, Serialize)] +struct ShareHealthResponse { + recovery_sets_owned: usize, + shares_held: usize, + sets: Vec, + recovery: Vec, +} +#[derive(Clone, Debug, Serialize)] +struct AnnounceRefResponse { + vid: String, + epoch: u64, +} +#[derive(Clone, Debug, Serialize)] +struct MintedGrantResponse { + rsid: u64, + subject: String, + trustees: Vec, + refs: Vec, +} +#[derive(Clone, Debug, Serialize)] +struct RecoveryGrantsResponse { + minted: Vec, + held: Vec, +} +#[derive(Clone, Debug, Serialize)] +struct CeremonyResponse { + ceremony_id: String, + subject: String, + sponsor: String, + claimant_display: String, + reason: String, + phase: String, + approvals: usize, + threshold: usize, + is_self_subject: bool, + takeover: bool, + trustee: bool, + approved: bool, + alarm: bool, +} +#[derive(Clone, Debug, Serialize)] +struct ResplitFriendResponse { + node: String, + role: String, + online: bool, + done: bool, + status: &'static str, +} +#[derive(Clone, Debug, Serialize)] +pub(crate) struct ResplitResponse { + old_rsid: u64, + new_rsid: u64, + ex_trustee: String, + phase: String, + new_attested: usize, + new_total: usize, + new_set_live: bool, + old_destroyed: usize, + old_total: usize, + remaining: Vec, +} +#[derive(Clone, Debug, Serialize)] +struct PendingTrusteeResponse { + user: String, + node: Option, + online: bool, +} +#[derive(Clone, Debug, Serialize)] +struct PendingResplitResponse { + old_rsid: u64, + ex_trustee: String, + suggested: Vec, +} + +#[derive(Clone, Debug, Serialize)] +pub struct StatusSnapshot { + node_id: String, + addr: Vec, + relay_url: Option, + friends: FriendsResponse, + peers: Vec, + vaults: VaultsResponse, + share_health: ShareHealthResponse, + recovery_grants: RecoveryGrantsResponse, + ceremonies: Vec, + resplits: Vec, + pending_resplits: Vec, + reachability: &'static str, + relay_networks: usize, + relay_diversity_warning: bool, + por_latency_anomaly_count: usize, +} + /// Build the status snapshot pushed over WS and returned by `GET /api/status`. -fn status_snapshot(d: &Daemon) -> Value { +fn status_snapshot(d: &Daemon) -> StatusSnapshot { let friends: Vec = d.friend_ids().iter().map(|f| hexs(f)).collect(); - let vaults: Vec = d - .published_vaults() + let friend_grants: Vec = d + .friend_grants() .iter() - .map(|(v, e)| json!({ "vid": hexs(v), "epoch": e })) + .map(|report| FriendGrantResponse { + user: hexs(&report.user), + grant_bytes: report.grant_bytes, + }) + .collect(); + let vaults: Vec = d + .vault_options() + .iter() + .map(|vault| PublishedVaultResponse { + vid: hexs(&vault.vid), + epoch: vault.epoch, + name: vault.name.clone(), + }) + .collect(); + let peers = d + .peer_options() + .iter() + .map(|peer| PeerOptionResponse { + user: hexs(&peer.user), + display: peer.display.clone(), + node: hexs(&peer.node), + addrs: peer.addrs.clone(), + }) .collect(); let held: Vec = d.held_replica_vids().iter().map(|v| hexs(v)).collect(); let (sets, shares) = d.share_health_counts(); + let recovery_sets: Vec = d + .recovery_sets() + .iter() + .map(|set| { + let scope = match set.scope { + RecoveryScope::Root => RecoveryScopeResponse::Root, + RecoveryScope::Vault(vid) => RecoveryScopeResponse::Vault { vid: hexs(&vid) }, + }; + RecoverySetResponse { + rsid: set.rsid, + scope, + threshold: set.threshold, + issued: set.issued, + trustees: set + .trustees + .iter() + .map(|(user, delivered)| TrusteeDeliveryResponse { + user: hexs(user), + delivered: *delivered, + }) + .collect(), + warnings: set + .warnings + .iter() + .map(|warning| format!("{warning:?}")) + .collect(), + } + }) + .collect(); // W4 (§10.2): per owned recovery set, the attested-live count, target, and drift // recommendation (healthy / extend / resplit) the maintenance loop keeps current. - let recovery: Vec = d + let recovery: Vec = d .recovery_health() .iter() - .map(|r| { - json!({ - "rsid": r.rsid, - "live": r.live, - "target": r.target, - "recommendation": r.recommendation, - "needed": r.needed, - }) + .map(|r| RecoveryHealthResponse { + rsid: r.rsid, + live: r.live, + target: r.target, + recommendation: r.recommendation.to_owned(), + needed: r.needed, }) .collect(); // W3 (§8, §7.3): per owned recovery set, which trustees hold a minted ShareGrant // and the announce-ref freshness (vid + epoch) the maintenance loop keeps current. - let grants: Vec = d + let grants: Vec = d .recovery_grants() .iter() - .map(|g| { - json!({ - "rsid": g.rsid, - "subject": hexs(&g.subject), - "trustees": g.trustees.iter() - .map(|(u, delivered)| json!({ "user": hexs(u), "delivered": delivered })) - .collect::>(), - "refs": g.refs.iter() - .map(|(vid, epoch)| json!({ "vid": hexs(vid), "epoch": epoch })) - .collect::>(), - }) + .map(|g| MintedGrantResponse { + rsid: g.rsid, + subject: hexs(&g.subject), + trustees: g + .trustees + .iter() + .map(|(u, delivered)| TrusteeDeliveryResponse { + user: hexs(u), + delivered: *delivered, + }) + .collect(), + refs: g + .refs + .iter() + .map(|(vid, epoch)| AnnounceRefResponse { + vid: hexs(vid), + epoch: *epoch, + }) + .collect(), }) .collect(); // W3 trustee side: subject users whose grants this daemon holds for others. @@ -233,71 +513,254 @@ fn status_snapshot(d: &Daemon) -> Value { // W4 (§6 MUST): warn when the usable relay set spans fewer than 2 distinct // networks - a single relay is a single point of failure and metadata choke. let relay_networks = d.relay_network_count(); - json!({ - "node_id": hexs(&d.node_id()), - "addr": d.dialable_addr_strings(), - "relay_url": relay_url, - "friends": { "count": friends.len(), "list": friends }, - "vaults": { "published": vaults, "held_replicas": held }, - "share_health": { "recovery_sets_owned": sets, "shares_held": shares, "recovery": recovery }, - "recovery_grants": { "minted": grants, "held": held_grants }, - // W2 (§8.5): live recovery ceremonies + the anti-silent-takeover alarm. - "ceremonies": ceremony_rows(d), - // W5 (§9.3 step 4): open trustee re-splits after an unfriend, with the live - // reachability of the remaining friends who get the new share / destroy step. - "resplits": d.resplit_statuses().iter().map(resplit_json).collect::>(), - // §9.3.4: re-splits detected on unfriend but awaiting the user's prompt to start. - "pending_resplits": d.pending_resplit_statuses().iter().map(pending_resplit_json).collect::>(), - "reachability": if relay_url.is_some() { "relay" } else { "direct" }, - "relay_networks": relay_networks, - "relay_diversity_warning": relay_networks < 2, - }) + StatusSnapshot { + node_id: hexs(&d.node_id()), + addr: d.dialable_addr_strings(), + relay_url: relay_url.clone(), + friends: FriendsResponse { + count: friends.len(), + list: friends, + grants: friend_grants, + }, + peers, + vaults: VaultsResponse { + published: vaults, + held_replicas: held, + }, + share_health: ShareHealthResponse { + recovery_sets_owned: sets, + shares_held: shares, + sets: recovery_sets, + recovery, + }, + recovery_grants: RecoveryGrantsResponse { + minted: grants, + held: held_grants, + }, + ceremonies: ceremony_rows(d), + resplits: d.resplit_statuses().iter().map(resplit_json).collect(), + pending_resplits: d + .pending_resplit_statuses() + .iter() + .map(pending_resplit_json) + .collect(), + reachability: if relay_url.is_some() { + "relay" + } else { + "direct" + }, + relay_networks, + relay_diversity_warning: relay_networks < 2, + por_latency_anomaly_count: d.por_latency_anomaly_count(), + } } /// One re-split's §9.3 step-4 prompt surface as JSON: phase, new-set liveness gate, /// old-set destroy progress, and each remaining friend's online/queued status. -fn resplit_json(rs: &ResplitStatus) -> Value { - json!({ - "old_rsid": rs.old_rsid, - "new_rsid": rs.new_rsid, - "ex_trustee": hexs(&rs.ex_trustee), - "phase": rs.phase, - // New set going live is the destroy gate (>= M + slack attested). - "new_attested": rs.new_attested, - "new_total": rs.new_total, - "new_set_live": rs.new_live, - "old_destroyed": rs.old_destroyed, - "old_total": rs.old_total, - "remaining": rs.remaining.iter().map(|f| json!({ - "node": hexs(&f.node), - "role": f.role, - "online": f.online, - "done": f.done, - "status": if f.done { "done" } else if f.online { "online" } else { "will_queue" }, - })).collect::>(), - }) +fn resplit_json(rs: &ResplitStatus) -> ResplitResponse { + ResplitResponse { + old_rsid: rs.old_rsid, + new_rsid: rs.new_rsid, + ex_trustee: hexs(&rs.ex_trustee), + phase: rs.phase.to_owned(), + new_attested: rs.new_attested, + new_total: rs.new_total, + new_set_live: rs.new_live, + old_destroyed: rs.old_destroyed, + old_total: rs.old_total, + remaining: rs + .remaining + .iter() + .map(|f| ResplitFriendResponse { + node: hexs(&f.node), + role: f.role.to_owned(), + online: f.online, + done: f.done, + status: if f.done { + "done" + } else if f.online { + "online" + } else { + "will_queue" + }, + }) + .collect(), + } } /// One PENDING re-split's §9.3.4 prompt surface as JSON: the ex-trustee and the suggested /// new trustee set with each member's live reachability, so the GUI can render the prompt /// and pre-fill `POST /api/recovery/{rsid}/resplit-start`. -fn pending_resplit_json(p: &PendingResplitStatus) -> Value { - json!({ - "old_rsid": p.old_rsid, - "ex_trustee": hexs(&p.ex_trustee), - "suggested": p.suggested.iter().map(|t| json!({ - "user": hexs(&t.user), - "node": t.node.map(|n| hexs(&n)), - "online": t.online, - })).collect::>(), - }) +fn pending_resplit_json(p: &PendingResplitStatus) -> PendingResplitResponse { + PendingResplitResponse { + old_rsid: p.old_rsid, + ex_trustee: hexs(&p.ex_trustee), + suggested: p + .suggested + .iter() + .map(|t| PendingTrusteeResponse { + user: hexs(&t.user), + node: t.node.map(|n| hexs(&n)), + online: t.online, + }) + .collect(), + } } /// `GET /api/status`. -pub async fn status(State(st): State) -> Json { +pub async fn status(State(st): State) -> Json { Json(status_snapshot(&st.daemon)) } +const METRIC_COUNT_LIMIT: usize = 1_000_000; + +#[derive(Serialize)] +struct MetricLimits { + count_ceiling: usize, +} +#[derive(Serialize)] +struct StorageMetrics { + owned_vaults: u64, + held_replica_vaults: u64, + refetch_needed: u64, +} +#[derive(Serialize)] +struct ReplicaMetrics { + peer_capacity_count: u64, + held_count: u64, + assignments: u64, + granted_capacity_bytes: u64, +} +#[derive(Serialize)] +struct MaintenanceMetrics { + running: bool, + recovery_sets_checked: u64, + relay_networks: u64, + last_completed_at: u64, + last_failure_at: u64, + last_failure_count: u64, + consecutive_failed_rounds: u64, +} +#[derive(Serialize)] +struct GarbageCollectionMetrics { + last_succeeded: bool, + live_set_capacity: usize, +} +#[derive(Serialize)] +struct CeremonyMetrics { + active_or_retained: u64, + active_tracked: u64, + capacity_ceiling: u64, + per_subject_capacity: u64, + fanout_capacity: u64, + tombstones: u64, + tombstone_capacity: u64, + subject_rate_keys: u64, + sponsor_rate_keys: u64, + at_capacity: bool, + tombstones_at_capacity: bool, +} +#[derive(Serialize)] +struct MigrationMetrics { + required: bool, + state_schema_ready: bool, + legacy_migration_is_explicit: bool, +} +#[derive(Serialize)] +struct RecoveryMetrics { + owned_sets: u64, + held_shares: u64, + open_resplits: u64, + pending_resplits: u64, +} +#[derive(Serialize)] +pub struct MetricsSnapshot { + schema: u8, + limits: MetricLimits, + storage: StorageMetrics, + replicas: ReplicaMetrics, + maintenance: MaintenanceMetrics, + garbage_collection: GarbageCollectionMetrics, + ceremonies: CeremonyMetrics, + migrations: MigrationMetrics, + recovery: RecoveryMetrics, +} + +fn bounded_count(value: usize) -> u64 { + value.min(METRIC_COUNT_LIMIT) as u64 +} + +/// `GET /api/metrics`: authenticated, identity-free operational health and capacity. +pub async fn metrics(State(st): State) -> Json { + let published = st.daemon.published_vaults().len(); + let held_replicas = st.daemon.held_replica_vids().len(); + let friends = st.daemon.friend_ids().len(); + let (recovery_sets, held_shares) = st.daemon.share_health_counts(); + let recovery = st.daemon.recovery_health(); + let ceremonies = ceremony_rows(&st.daemon); + let resplits = st.daemon.resplit_statuses(); + let pending_resplits = st.daemon.pending_resplit_statuses(); + let capacity = st.daemon.operational_capacity(); + let history = st.daemon.operational_history(); + Json(MetricsSnapshot { + schema: 1, + limits: MetricLimits { + count_ceiling: METRIC_COUNT_LIMIT, + }, + storage: StorageMetrics { + owned_vaults: bounded_count(published), + held_replica_vaults: bounded_count(held_replicas), + refetch_needed: bounded_count(capacity.storage_refetch_needed), + }, + replicas: ReplicaMetrics { + peer_capacity_count: bounded_count(friends), + held_count: bounded_count(held_replicas), + assignments: bounded_count(capacity.replica_assignments), + granted_capacity_bytes: capacity.replica_grant_bytes, + }, + maintenance: MaintenanceMetrics { + running: true, + recovery_sets_checked: bounded_count(recovery.len()), + relay_networks: bounded_count(st.daemon.relay_network_count()), + last_completed_at: history.last_maintenance_at, + last_failure_at: history.last_failure_at, + last_failure_count: bounded_count(history.last_failure_count), + consecutive_failed_rounds: history + .consecutive_failed_rounds + .min(METRIC_COUNT_LIMIT as u64), + }, + garbage_collection: GarbageCollectionMetrics { + last_succeeded: history.last_gc_succeeded, + live_set_capacity: 1_000_000, + }, + ceremonies: CeremonyMetrics { + active_or_retained: bounded_count(ceremonies.len()), + active_tracked: bounded_count(capacity.ceremony_active), + capacity_ceiling: bounded_count(capacity.ceremony_capacity), + per_subject_capacity: bounded_count(capacity.ceremony_per_subject_capacity), + fanout_capacity: bounded_count(capacity.ceremony_fanout_capacity), + tombstones: bounded_count(capacity.ceremony_tombstones), + tombstone_capacity: bounded_count(capacity.ceremony_tombstone_capacity), + subject_rate_keys: bounded_count(capacity.ceremony_subject_rate_keys), + sponsor_rate_keys: bounded_count(capacity.ceremony_sponsor_rate_keys), + at_capacity: capacity.ceremony_active >= capacity.ceremony_capacity, + tombstones_at_capacity: capacity.ceremony_tombstones + >= capacity.ceremony_tombstone_capacity, + }, + migrations: MigrationMetrics { + required: false, + state_schema_ready: true, + legacy_migration_is_explicit: true, + }, + recovery: RecoveryMetrics { + owned_sets: bounded_count(recovery_sets), + held_shares: bounded_count(held_shares), + open_resplits: bounded_count(resplits.len()), + pending_resplits: bounded_count(pending_resplits.len()), + }, + }) +} + // ---- vaults ------------------------------------------------------------ #[derive(Deserialize)] @@ -407,10 +870,10 @@ pub async fn unfriend( pub async fn resplit_status( Path(rsid): Path, State(st): State, -) -> Result, ApiError> { +) -> Result, ApiError> { match st.daemon.resplit_status(rsid) { Some(rs) => Ok(Json(resplit_json(&rs))), - None => Err(ApiError( + None => Err(ApiError::client( StatusCode::NOT_FOUND, format!("no open re-split for recovery set {rsid}"), )), @@ -433,7 +896,7 @@ pub async fn recovery_paper( let html = st .daemon .paper_cards(rsid) - .map_err(|e| ApiError(StatusCode::NOT_FOUND, format!("{e:#}")))?; + .map_err(|e| ApiError::client(StatusCode::NOT_FOUND, format!("{e:#}")))?; Ok(( [ (header::CONTENT_TYPE, "text/html; charset=utf-8"), @@ -461,7 +924,7 @@ pub async fn resplit_start( Path(rsid): Path, State(st): State, body: Option>, -) -> Result, ApiError> { +) -> Result, ApiError> { let override_set = match body.and_then(|Json(b)| b.trustees) { Some(hexes) => { let mut users = Vec::with_capacity(hexes.len()); @@ -480,7 +943,7 @@ pub async fn resplit_start( .iter() .any(|p| p.old_rsid == rsid); if !tracked { - return Err(ApiError( + return Err(ApiError::client( StatusCode::NOT_FOUND, format!("no pending or open re-split for recovery set {rsid}"), )); @@ -501,6 +964,98 @@ pub struct PeerReq { addrs: Vec, } +#[derive(Deserialize)] +pub struct SyncReq { + peer: PeerReq, + out_dir: String, +} + +#[derive(Deserialize)] +struct RestartHandoff { + r#type: String, + version: u8, + trustees: Vec, + announce_refs: Vec, +} + +#[derive(Deserialize)] +struct RestartRef { + vid: String, + epoch: u64, + digest: String, +} + +#[derive(Deserialize)] +pub struct RestartRestoreReq { + out_dir: String, +} + +/// Restore retained vaults after claimant activation. The durable public handoff +/// limits results to the maximum signed-grant epoch for each vault. +pub async fn restart_restore( + State(st): State, + Json(req): Json, +) -> Result, ApiError> { + let path = st.state_dir.join("recovery-restart-handoff.json"); + let bytes = std::fs::read(&path).map_err(|_| { + ApiError::client( + StatusCode::NOT_FOUND, + "no claimant restart handoff is available", + ) + })?; + let handoff: RestartHandoff = + serde_json::from_slice(&bytes).map_err(|_| bad("invalid claimant restart handoff"))?; + if handoff.r#type != "carapace.recovery-restart-handoff" || handoff.version != 1 { + return Err(bad("unsupported claimant restart handoff")); + } + let mut refs = Vec::new(); + for reference in handoff.announce_refs { + let vid = parse_hex32(&reference.vid)?; + let digest = parse_hex32(&reference.digest)?; + refs.push(AnnounceRef { + vid, + epoch: reference.epoch, + digest, + }); + } + let mut trustees = Vec::new(); + for trustee in handoff.trustees { + trustees.push((parse_hex32(&trustee.node)?, trustee.addrs)); + } + let maximum_refs = carapaced::max_epoch_refs(&refs).len(); + let restored = st + .daemon + .recover_retained_at(&trustees, &refs, std::path::Path::new(&req.out_dir)) + .await?; + Ok(Json(json!({ + "restored": restored.iter().map(|vault| json!({ + "vid": hexs(&vault.vid), + "epoch": vault.epoch, + "out_dir": vault.out_dir.display().to_string(), + })).collect::>(), + "maximum_epoch_refs": maximum_refs, + }))) +} + +/// `POST /api/sync`: pull owned-vault documents and blobs from another authorized device. +pub async fn sync_owned( + State(st): State, + Json(req): Json, +) -> Result, ApiError> { + let node = parse_hex32(&req.peer.node)?; + let restored = st + .daemon + .sync_from_at(node, &req.peer.addrs, std::path::Path::new(&req.out_dir)) + .await?; + Ok(Json(json!({ + "restored": restored.iter().map(|v| json!({ + "vid": hexs(&v.vid), + "epoch": v.epoch, + "out_dir": v.out_dir.display().to_string(), + })).collect::>(), + }))) +} + #[derive(Deserialize)] pub struct PlaceReq { peers: Vec, @@ -766,10 +1321,38 @@ pub async fn ceremony_open( unix_now(), )?; let reached = st.daemon.ceremony_fanout(&open).await.unwrap_or(0); + let trustee_hints = st.daemon.claimant_trustee_hints(&subject)?; + let announce_refs = st.daemon.claimant_announce_refs(&subject)?; + let roster: Vec = trustee_hints + .iter() + .map(|trustee| hexs(&trustee.user)) + .collect(); + let trustees: Vec = trustee_hints + .iter() + .map(|trustee| { + json!({ + "node": hexs(&trustee.node), + "addrs": trustee.addrs, + }) + }) + .collect(); + let sponsor_package = json!({ + "type": "carapace.sponsor-ceremony", + "version": 1, + "open_hex": hexs(&open.encode_frame()), + "roster": roster, + "trustees": trustees, + "announce_refs": announce_refs.iter().map(|reference| json!({ + "vid": hexs(&reference.vid), + "epoch": reference.epoch, + "digest": hexs(&reference.digest), + })).collect::>(), + }); Ok(Json(json!({ "ceremony_id": hexs(&id), "open_hex": hexs(&open.encode_frame()), "fanout_reached": reached, + "sponsor_package": sponsor_package.to_string(), }))) } @@ -815,26 +1398,23 @@ pub async fn ceremony_abort( /// The recovery-ceremony status rows (§8.5 step 2/6) for the status surface: each /// ceremony this device has seen, its phase, approvals, and the alarm flags. -fn ceremony_rows(d: &Daemon) -> Vec { +fn ceremony_rows(d: &Daemon) -> Vec { d.ceremony_statuses() .iter() - .map(|c| { - json!({ - "ceremony_id": hexs(&c.ceremony_id), - "subject": hexs(&c.subject), - "sponsor": hexs(&c.sponsor), - "claimant_display": c.claimant_display, - "reason": c.reason, - "phase": c.phase, - "approvals": c.approvals, - "threshold": c.threshold, - "is_self_subject": c.is_self_subject, - "takeover": c.takeover, - "trustee": c.trustee, - "approved": c.approved, - // The anti-silent-takeover banner: a live ceremony against OUR account. - "alarm": c.is_self_subject && !c.takeover, - }) + .map(|c| CeremonyResponse { + ceremony_id: hexs(&c.ceremony_id), + subject: hexs(&c.subject), + sponsor: hexs(&c.sponsor), + claimant_display: c.claimant_display.clone(), + reason: c.reason.clone(), + phase: c.phase.to_owned(), + approvals: c.approvals, + threshold: c.threshold, + is_self_subject: c.is_self_subject, + takeover: c.takeover, + trustee: c.trustee, + approved: c.approved, + alarm: c.is_self_subject && !c.takeover, }) .collect() } @@ -846,22 +1426,25 @@ pub async fn ceremony_status(State(st): State) -> Json { // ---- events (WebSocket) ------------------------------------------------ -#[derive(Deserialize)] -pub struct TokenQuery { - token: Option, -} - -/// `GET /api/events` (WebSocket). Browsers cannot set an `Authorization` header on a -/// WS handshake, so the token is passed as the `token` query parameter and validated -/// in CONSTANT TIME before the upgrade. A missing/wrong token => 401, no upgrade. +/// `GET /api/events` (WebSocket). The browser sends the short-lived, HTTP-only session +/// cookie that the loopback shell sets. This keeps the bearer value out of URLs and logs. pub async fn events( ws: WebSocketUpgrade, - Query(q): Query, + headers: HeaderMap, State(st): State, ) -> Response { - let presented = q.token.unwrap_or_default(); - if !auth::ct_eq_str(&presented, &st.token) { - return (StatusCode::UNAUTHORIZED, "missing or invalid token").into_response(); + let presented = headers + .get(header::COOKIE) + .and_then(|value| value.to_str().ok()) + .and_then(|cookies| { + cookies.split(';').find_map(|cookie| { + let (name, value) = cookie.trim().split_once('=')?; + (name == "carapace_session").then_some(value) + }) + }) + .unwrap_or_default(); + if !auth::ct_eq_str(presented, &st.token) { + return error_response(StatusCode::UNAUTHORIZED, "missing or invalid token"); } ws.on_upgrade(move |socket| push_status(socket, st)) } @@ -870,11 +1453,81 @@ pub async fn events( /// closes. A periodic full snapshot is the v1 live-status feed (§ design doc). async fn push_status(mut socket: WebSocket, st: AppState) { let mut ticker = tokio::time::interval(Duration::from_secs(5)); - loop { - let snap = status_snapshot(&st.daemon).to_string(); + while let Ok(snap) = serde_json::to_string(&status_snapshot(&st.daemon)) { if socket.send(Message::Text(snap.into())).await.is_err() { break; } ticker.tick().await; } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn gui_status_contract_fixture_matches_typed_server_shape() { + let fixture: serde_json::Value = serde_json::from_str(include_str!( + "../../../gui/tests/fixtures/status-contract.json" + )) + .unwrap(); + let expected = serde_json::to_value(StatusSnapshot { + node_id: String::new(), + addr: Vec::new(), + relay_url: None, + friends: FriendsResponse { + count: 0, + list: Vec::new(), + grants: Vec::new(), + }, + peers: Vec::new(), + vaults: VaultsResponse { + published: Vec::new(), + held_replicas: Vec::new(), + }, + share_health: ShareHealthResponse { + recovery_sets_owned: 0, + shares_held: 0, + sets: Vec::new(), + recovery: Vec::new(), + }, + recovery_grants: RecoveryGrantsResponse { + minted: Vec::new(), + held: Vec::new(), + }, + ceremonies: Vec::new(), + resplits: Vec::new(), + pending_resplits: Vec::new(), + reachability: "direct", + relay_networks: 0, + relay_diversity_warning: true, + por_latency_anomaly_count: 0, + }) + .unwrap(); + assert_eq!(fixture, expected); + } + + #[test] + fn internal_error_detail_is_not_the_client_message() { + let error = ApiError::from(anyhow::anyhow!( + "secret path /private/state/root.key could not be read" + )); + + assert_eq!(error.status, StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(error.client_message, "internal server error"); + assert!(error + .internal + .as_deref() + .is_some_and(|detail| detail.contains("/private/state/root.key"))); + assert!(!error.client_message.contains("root.key")); + } + + #[test] + fn expected_client_error_has_no_internal_detail() { + let error = bad("invalid recovery set"); + + assert_eq!(error.status, StatusCode::BAD_REQUEST); + assert_eq!(error.client_message, "invalid recovery set"); + assert!(error.internal.is_none()); + } +} diff --git a/crates/carapace-api/src/lib.rs b/crates/carapace-api/src/lib.rs index 21fee19..516f494 100644 --- a/crates/carapace-api/src/lib.rs +++ b/crates/carapace-api/src/lib.rs @@ -6,19 +6,20 @@ //! `Host` check (DNS-rebinding defense), and a loopback `Origin` check (CSRF //! defense). See [`auth`] for the guards and [`handlers`] for the endpoints. //! -//! Public routes (no token): `GET /api/health`, the WebSocket `GET /api/events` -//! (which validates the token from a query parameter instead, since browsers cannot -//! set an `Authorization` header on a WS handshake), and the embedded static GUI. +//! Public routes (no bearer header): `GET /api/health`, the WebSocket `GET /api/events` +//! (which validates an HTTP-only, same-site session cookie), and the embedded static GUI. //! //! The GUI is embedded with `rust-embed` from `static/` (the SvelteKit build). The //! served `index.html` gets the session token injected as `window.__CARAPACE_TOKEN__` //! under a strict per-response CSP nonce; see [`handlers::static_asset`]. mod auth; +mod claimant; mod handlers; +mod ops; use std::net::{Ipv4Addr, SocketAddr}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::{ensure, Context, Result}; @@ -36,6 +37,8 @@ pub struct AppState { pub daemon: Arc, /// The per-session bearer token (hex of 32 CSPRNG bytes). pub token: Arc, + /// Local state directory for public restart-handoff metadata. + pub state_dir: Arc, } /// A running control API. Dropping or [`ApiServer::shutdown`] stops it. @@ -49,6 +52,28 @@ pub struct ApiServer { _maintenance: MaintenanceHandle, } +/// A running claimant-only API. It never owns a normal daemon. +pub struct ClaimantApiServer { + /// The per-session bearer token, also written to `/claimant-api-token`. + pub token: String, + /// The actual bound loopback address. + pub local_addr: SocketAddr, + handle: tokio::task::JoinHandle<()>, +} + +impl ClaimantApiServer { + /// The base URL for the claimant client. + #[must_use] + pub fn url(&self) -> String { + format!("http://{}", self.local_addr) + } + + /// Stop the claimant server. A completed activation needs a normal daemon restart. + pub fn shutdown(self) { + self.handle.abort(); + } +} + impl ApiServer { /// The base URL the GUI/clients hit. #[must_use] @@ -68,6 +93,12 @@ impl ApiServer { pub fn app(state: AppState) -> Router { let protected = Router::new() .route("/api/status", get(handlers::status)) + .route("/api/metrics", get(handlers::metrics)) + .route("/api/sync", post(handlers::sync_owned)) + .route( + "/api/recovery/restart-restore", + post(handlers::restart_restore), + ) .route( "/api/vaults", get(handlers::list_vaults).post(handlers::publish_vault), @@ -128,6 +159,28 @@ pub fn app(state: AppState) -> Router { .with_state(state) } +/// Assemble the claimant-only router. Its type cannot route to normal daemon handlers. +fn claimant_app(state: claimant::ClaimantState) -> Router { + let protected = Router::new() + .route("/api/claimant/status", get(claimant::status)) + .route("/api/claimant/preview", post(claimant::preview)) + .route("/api/claimant/complete", post(claimant::complete)) + .route("/api/claimant/cancel", post(claimant::cancel)) + .layer(middleware::from_fn_with_state( + state.token.clone(), + auth::require_token, + )); + let shell = Router::new() + .route("/", get(claimant::shell)) + .route("/claimant.js", get(claimant::script)) + .route("/claimant.css", get(claimant::stylesheet)); + Router::new() + .merge(protected) + .merge(shell) + .layer(middleware::from_fn(auth::guard_host_origin)) + .with_state(state) +} + /// Generate a 32-byte CSPRNG token, hex-encode it, and write it to /// `/api-token` with `0600` permissions on unix. fn mint_and_write_token(state_dir: &Path) -> Result { @@ -169,11 +222,15 @@ fn write_token_file(path: &Path, token: &str) -> Result<()> { .with_context(|| format!("write {path:?}")) } -#[cfg(not(unix))] +#[cfg(windows)] +fn write_token_file(_path: &Path, _token: &str) -> Result<()> { + anyhow::bail!( + "control API startup on Windows is disabled until Carapace enforces a private ACL on API token files" + ) +} + +#[cfg(all(not(unix), not(windows)))] fn write_token_file(path: &Path, token: &str) -> Result<()> { - // ponytail: no OS-ACL restriction here (needs a Windows-specific crate). The - // token still gates every request; tighten the file ACL on non-unix if the - // state dir is shared. std::fs::write(path, token).with_context(|| format!("write {path:?}")) } @@ -217,12 +274,14 @@ pub async fn serve(daemon: Arc, state_dir: &Path, port: u16) -> Result, state_dir: &Path, port: u16) -> Result Result { + std::fs::create_dir_all(state_dir) + .with_context(|| format!("create claimant state directory {state_dir:?}"))?; + let token = { + let mut raw = [0u8; 32]; + getrandom::getrandom(&mut raw) + .map_err(|error| anyhow::anyhow!("generate claimant API token: {error}"))?; + let token = hex::encode(raw); + write_token_file(&state_dir.join("claimant-api-token"), &token)?; + token + }; + let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, port)); + ensure!( + addr.ip().is_loopback(), + "refusing non-loopback claimant bind {addr}" + ); + let listener = tokio::net::TcpListener::bind(addr) + .await + .with_context(|| format!("bind claimant API on {addr}"))?; + let local_addr = listener.local_addr().context("read claimant API address")?; + let url_path = state_dir.join("claimant-api-url"); + std::fs::write(&url_path, format!("http://{local_addr}")) + .with_context(|| format!("write {url_path:?}"))?; + + let state = claimant::ClaimantState::new(state_dir.to_path_buf(), Arc::from(token.as_str()))?; + let app = claimant_app(state); + let handle = tokio::spawn(async move { + if let Err(error) = axum::serve(listener, app).await { + let _ = error; + ops::log("server.claimant_exit_error", None); + } + }); + Ok(ClaimantApiServer { + token, + local_addr, + handle, + }) +} + #[cfg(all(test, unix))] mod tests { use super::write_token_file; @@ -274,3 +378,17 @@ mod tests { assert_eq!(mode & 0o777, 0o600, "rewritten token must be 0600"); } } + +#[cfg(all(test, windows))] +mod windows_tests { + use super::write_token_file; + + #[test] + fn token_creation_fails_closed_without_private_acls() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("api-token"); + let error = write_token_file(&path, "secret").unwrap_err(); + assert!(error.to_string().contains("private ACL")); + assert!(!path.exists()); + } +} diff --git a/crates/carapace-api/src/ops.rs b/crates/carapace-api/src/ops.rs new file mode 100644 index 0000000..ab73349 --- /dev/null +++ b/crates/carapace-api/src/ops.rs @@ -0,0 +1,34 @@ +//! Secret-redacted operational events. + +use serde_json::{json, Value}; + +/// Build one stable, structured operational event. +/// +/// Callers must supply only fixed event identifiers and bounded numeric fields. This +/// function does not accept free-form error text, paths, or identity values. +pub(crate) fn event(event_id: &'static str, reference: Option) -> Value { + json!({ + "component": "carapace-api", + "event_id": event_id, + "reference": reference, + }) +} + +/// Write one event as a single JSON line to the local process log. +pub(crate) fn log(event_id: &'static str, reference: Option) { + eprintln!("{}", event(event_id, reference)); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn event_contract_has_only_redacted_stable_fields() { + let value = event("api.internal_error", Some(7)); + assert_eq!(value["component"], "carapace-api"); + assert_eq!(value["event_id"], "api.internal_error"); + assert_eq!(value["reference"], 7); + assert_eq!(value.as_object().unwrap().len(), 3); + } +} diff --git a/crates/carapace-api/static/_app/immutable/assets/0.BIBOqY7u.css b/crates/carapace-api/static/_app/immutable/assets/0.BNChGHTQ.css similarity index 98% rename from crates/carapace-api/static/_app/immutable/assets/0.BIBOqY7u.css rename to crates/carapace-api/static/_app/immutable/assets/0.BNChGHTQ.css index d2b68ce..a900e14 100644 --- a/crates/carapace-api/static/_app/immutable/assets/0.BIBOqY7u.css +++ b/crates/carapace-api/static/_app/immutable/assets/0.BNChGHTQ.css @@ -1 +1 @@ -@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(./ibm-plex-mono-cyrillic-ext-400-normal.xuaO2J-f.woff2)format("woff2"),url(./ibm-plex-mono-cyrillic-ext-400-normal.DMdlQ8Kv.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(./ibm-plex-mono-cyrillic-400-normal.BSMlKf0J.woff2)format("woff2"),url(./ibm-plex-mono-cyrillic-400-normal.CEL4l2ZJ.woff)format("woff");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(./ibm-plex-mono-vietnamese-400-normal.BulugwFq.woff2)format("woff2"),url(./ibm-plex-mono-vietnamese-400-normal.DDuiU_S-.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(./ibm-plex-mono-latin-ext-400-normal.BmRBH3aV.woff2)format("woff2"),url(./ibm-plex-mono-latin-ext-400-normal.D3D2R8hC.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(./ibm-plex-mono-latin-400-normal.DMJ8VG8y.woff2)format("woff2"),url(./ibm-plex-mono-latin-400-normal.CvHOgSBP.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(./ibm-plex-mono-cyrillic-ext-500-normal.BqneJy0T.woff2)format("woff2"),url(./ibm-plex-mono-cyrillic-ext-500-normal.BIfNGwUT.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(./ibm-plex-mono-cyrillic-500-normal.Bq9vWWag.woff2)format("woff2"),url(./ibm-plex-mono-cyrillic-500-normal.Ael50iVv.woff)format("woff");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(./ibm-plex-mono-vietnamese-500-normal.DZ4AoWbu.woff2)format("woff2"),url(./ibm-plex-mono-vietnamese-500-normal.C8zxqsMH.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(./ibm-plex-mono-latin-ext-500-normal.CAhNIIs5.woff2)format("woff2"),url(./ibm-plex-mono-latin-ext-500-normal.CZ70TYgx.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(./ibm-plex-mono-latin-500-normal.DSY6xOcd.woff2)format("woff2"),url(./ibm-plex-mono-latin-500-normal.CB9ihrfo.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:600;src:url(./ibm-plex-mono-cyrillic-ext-600-normal.V-xxqcpd.woff2)format("woff2"),url(./ibm-plex-mono-cyrillic-ext-600-normal.9HEixskS.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:600;src:url(./ibm-plex-mono-cyrillic-600-normal.CTOM6hUh.woff2)format("woff2"),url(./ibm-plex-mono-cyrillic-600-normal.fLZuRloM.woff)format("woff");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:600;src:url(./ibm-plex-mono-vietnamese-600-normal.D2EvbN8M.woff2)format("woff2"),url(./ibm-plex-mono-vietnamese-600-normal.iLQfcSjf.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:600;src:url(./ibm-plex-mono-latin-ext-600-normal.D38SheWl.woff2)format("woff2"),url(./ibm-plex-mono-latin-ext-600-normal.DmB0ttJJ.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:600;src:url(./ibm-plex-mono-latin-600-normal.BgSNZQsw.woff2)format("woff2"),url(./ibm-plex-mono-latin-600-normal.DWFSQ4vo.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:400;src:url(./hanken-grotesk-cyrillic-ext-400-normal.BLTEyOai.woff2)format("woff2"),url(./hanken-grotesk-cyrillic-ext-400-normal.C910xUUL.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:400;src:url(./hanken-grotesk-vietnamese-400-normal.BLrFBAHj.woff2)format("woff2"),url(./hanken-grotesk-vietnamese-400-normal.C-iWyKLC.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:400;src:url(./hanken-grotesk-latin-ext-400-normal.DR7lHpW4.woff2)format("woff2"),url(./hanken-grotesk-latin-ext-400-normal.DI-aIsWt.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:400;src:url(./hanken-grotesk-latin-400-normal.BG6hkEXj.woff2)format("woff2"),url(./hanken-grotesk-latin-400-normal.CjyVwvJV.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:500;src:url(./hanken-grotesk-cyrillic-ext-500-normal.DJxU5DEV.woff2)format("woff2"),url(./hanken-grotesk-cyrillic-ext-500-normal.klSdyF8A.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:500;src:url(./hanken-grotesk-vietnamese-500-normal.DxwlvJEc.woff2)format("woff2"),url(./hanken-grotesk-vietnamese-500-normal.DkDHNoXI.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:500;src:url(./hanken-grotesk-latin-ext-500-normal.DLb9JDK8.woff2)format("woff2"),url(./hanken-grotesk-latin-ext-500-normal.BNvrJ0Ju.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:500;src:url(./hanken-grotesk-latin-500-normal.DrDcrrxK.woff2)format("woff2"),url(./hanken-grotesk-latin-500-normal.Bo-NxEKf.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:600;src:url(./hanken-grotesk-cyrillic-ext-600-normal.gC6IfhiA.woff2)format("woff2"),url(./hanken-grotesk-cyrillic-ext-600-normal.CZKgvMB-.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:600;src:url(./hanken-grotesk-vietnamese-600-normal.Cp8QQjQf.woff2)format("woff2"),url(./hanken-grotesk-vietnamese-600-normal.DHaFH8q1.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:600;src:url(./hanken-grotesk-latin-ext-600-normal.FY8kSObK.woff2)format("woff2"),url(./hanken-grotesk-latin-ext-600-normal.DHIm05DD.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:600;src:url(./hanken-grotesk-latin-600-normal.CIXX6EOa.woff2)format("woff2"),url(./hanken-grotesk-latin-600-normal.NEn2C4Q3.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:700;src:url(./hanken-grotesk-cyrillic-ext-700-normal.t5HJuhUd.woff2)format("woff2"),url(./hanken-grotesk-cyrillic-ext-700-normal.CANZoffZ.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:700;src:url(./hanken-grotesk-vietnamese-700-normal.C4RNfQp6.woff2)format("woff2"),url(./hanken-grotesk-vietnamese-700-normal.CslVg6dq.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:700;src:url(./hanken-grotesk-latin-ext-700-normal.CK2OfQqO.woff2)format("woff2"),url(./hanken-grotesk-latin-ext-700-normal.BXHnjv6S.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:700;src:url(./hanken-grotesk-latin-700-normal.CeQ8H3UY.woff2)format("woff2"),url(./hanken-grotesk-latin-700-normal.6IGCzoPh.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--ink:#14100c;--plate:#1f1913;--plate-raised:#241d16;--hairline:#2e251b;--bronze:#c8843c;--bronze-strong:#e0a05c;--verdigris:#4e8d7c;--coral:#e0674a;--bone:#eae0d2;--muted:#9a8f7e;--font-sans:"Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif;--font-mono:"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;--step--1:.833rem;--step-0:1rem;--step-1:1.2rem;--step-2:1.44rem;--step-3:1.728rem;--step-4:2.074rem;--step-5:2.488rem;--radius:10px;--transition:.16s ease;background:var(--ink);color:var(--bone)}:root[data-theme=light]{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--ink:#f4ede0;--plate:#e7dac2;--plate-raised:#f0e6d3;--hairline:#cdb994;--bronze:#9c5f22;--bronze-strong:#7a4a1a;--verdigris:#2f6e5c;--coral:#b83f27;--bone:#241d16;--muted:#6b5f4d}@media (prefers-color-scheme:light){:root:not([data-theme=dark]){--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--ink:#f4ede0;--plate:#e7dac2;--plate-raised:#f0e6d3;--hairline:#cdb994;--bronze:#9c5f22;--bronze-strong:#7a4a1a;--verdigris:#2f6e5c;--coral:#b83f27;--bone:#241d16;--muted:#6b5f4d}}*{box-sizing:border-box}html,body{min-height:100%;margin:0;padding:0}body{background:var(--ink);color:var(--bone);font-family:var(--font-sans);font-size:var(--step-0);transition:background var(--transition), color var(--transition);line-height:1.5}h1,h2,h3{font-family:var(--font-sans);margin:0 0 .5em;font-weight:700;line-height:1.2}h1{font-size:var(--step-4)}h2{font-size:var(--step-2)}h3{font-size:var(--step-1)}code,.mono{font-family:var(--font-mono);font-variant-ligatures:none}a{color:var(--bronze-strong)}button,input,select,textarea{font-family:inherit;font-size:inherit;color:inherit}button{background:var(--plate-raised);border:1px solid var(--hairline);color:var(--bone);border-radius:var(--radius);cursor:pointer;transition:background var(--transition), border-color var(--transition);padding:.55em 1em}button:hover{border-color:var(--bronze)}button.primary{background:var(--bronze);border-color:var(--bronze);color:var(--ink);font-weight:600}button.primary:hover{background:var(--bronze-strong);border-color:var(--bronze-strong)}button:disabled{opacity:.5;cursor:not-allowed}input,select,textarea{background:var(--plate);border:1px solid var(--hairline);border-radius:calc(var(--radius) * .6);padding:.5em .7em}:focus-visible{outline:2px solid var(--bronze);outline-offset:2px}.card{background:var(--plate);border:1px solid var(--hairline);border-radius:var(--radius);padding:1.25rem}.muted{color:var(--muted)}.healthy{color:var(--verdigris)}.at-risk{color:var(--coral)}.visually-hidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.001ms!important;animation-duration:.001ms!important;animation-iteration-count:1!important}} +@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(./ibm-plex-mono-cyrillic-ext-400-normal.xuaO2J-f.woff2)format("woff2"),url(./ibm-plex-mono-cyrillic-ext-400-normal.DMdlQ8Kv.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(./ibm-plex-mono-cyrillic-400-normal.BSMlKf0J.woff2)format("woff2"),url(./ibm-plex-mono-cyrillic-400-normal.CEL4l2ZJ.woff)format("woff");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(./ibm-plex-mono-vietnamese-400-normal.BulugwFq.woff2)format("woff2"),url(./ibm-plex-mono-vietnamese-400-normal.DDuiU_S-.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(./ibm-plex-mono-latin-ext-400-normal.BmRBH3aV.woff2)format("woff2"),url(./ibm-plex-mono-latin-ext-400-normal.D3D2R8hC.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(./ibm-plex-mono-latin-400-normal.DMJ8VG8y.woff2)format("woff2"),url(./ibm-plex-mono-latin-400-normal.CvHOgSBP.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(./ibm-plex-mono-cyrillic-ext-500-normal.BqneJy0T.woff2)format("woff2"),url(./ibm-plex-mono-cyrillic-ext-500-normal.BIfNGwUT.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(./ibm-plex-mono-cyrillic-500-normal.Bq9vWWag.woff2)format("woff2"),url(./ibm-plex-mono-cyrillic-500-normal.Ael50iVv.woff)format("woff");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(./ibm-plex-mono-vietnamese-500-normal.DZ4AoWbu.woff2)format("woff2"),url(./ibm-plex-mono-vietnamese-500-normal.C8zxqsMH.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(./ibm-plex-mono-latin-ext-500-normal.CAhNIIs5.woff2)format("woff2"),url(./ibm-plex-mono-latin-ext-500-normal.CZ70TYgx.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(./ibm-plex-mono-latin-500-normal.DSY6xOcd.woff2)format("woff2"),url(./ibm-plex-mono-latin-500-normal.CB9ihrfo.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:600;src:url(./ibm-plex-mono-cyrillic-ext-600-normal.V-xxqcpd.woff2)format("woff2"),url(./ibm-plex-mono-cyrillic-ext-600-normal.9HEixskS.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:600;src:url(./ibm-plex-mono-cyrillic-600-normal.CTOM6hUh.woff2)format("woff2"),url(./ibm-plex-mono-cyrillic-600-normal.fLZuRloM.woff)format("woff");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:600;src:url(./ibm-plex-mono-vietnamese-600-normal.D2EvbN8M.woff2)format("woff2"),url(./ibm-plex-mono-vietnamese-600-normal.iLQfcSjf.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:600;src:url(./ibm-plex-mono-latin-ext-600-normal.D38SheWl.woff2)format("woff2"),url(./ibm-plex-mono-latin-ext-600-normal.DmB0ttJJ.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:600;src:url(./ibm-plex-mono-latin-600-normal.BgSNZQsw.woff2)format("woff2"),url(./ibm-plex-mono-latin-600-normal.DWFSQ4vo.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:400;src:url(./hanken-grotesk-cyrillic-ext-400-normal.BLTEyOai.woff2)format("woff2"),url(./hanken-grotesk-cyrillic-ext-400-normal.C910xUUL.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:400;src:url(./hanken-grotesk-vietnamese-400-normal.BLrFBAHj.woff2)format("woff2"),url(./hanken-grotesk-vietnamese-400-normal.C-iWyKLC.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:400;src:url(./hanken-grotesk-latin-ext-400-normal.DR7lHpW4.woff2)format("woff2"),url(./hanken-grotesk-latin-ext-400-normal.DI-aIsWt.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:400;src:url(./hanken-grotesk-latin-400-normal.BG6hkEXj.woff2)format("woff2"),url(./hanken-grotesk-latin-400-normal.CjyVwvJV.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:500;src:url(./hanken-grotesk-cyrillic-ext-500-normal.DJxU5DEV.woff2)format("woff2"),url(./hanken-grotesk-cyrillic-ext-500-normal.klSdyF8A.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:500;src:url(./hanken-grotesk-vietnamese-500-normal.DxwlvJEc.woff2)format("woff2"),url(./hanken-grotesk-vietnamese-500-normal.DkDHNoXI.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:500;src:url(./hanken-grotesk-latin-ext-500-normal.DLb9JDK8.woff2)format("woff2"),url(./hanken-grotesk-latin-ext-500-normal.BNvrJ0Ju.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:500;src:url(./hanken-grotesk-latin-500-normal.DrDcrrxK.woff2)format("woff2"),url(./hanken-grotesk-latin-500-normal.Bo-NxEKf.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:600;src:url(./hanken-grotesk-cyrillic-ext-600-normal.gC6IfhiA.woff2)format("woff2"),url(./hanken-grotesk-cyrillic-ext-600-normal.CZKgvMB-.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:600;src:url(./hanken-grotesk-vietnamese-600-normal.Cp8QQjQf.woff2)format("woff2"),url(./hanken-grotesk-vietnamese-600-normal.DHaFH8q1.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:600;src:url(./hanken-grotesk-latin-ext-600-normal.FY8kSObK.woff2)format("woff2"),url(./hanken-grotesk-latin-ext-600-normal.DHIm05DD.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:600;src:url(./hanken-grotesk-latin-600-normal.CIXX6EOa.woff2)format("woff2"),url(./hanken-grotesk-latin-600-normal.NEn2C4Q3.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:700;src:url(./hanken-grotesk-cyrillic-ext-700-normal.t5HJuhUd.woff2)format("woff2"),url(./hanken-grotesk-cyrillic-ext-700-normal.CANZoffZ.woff)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:700;src:url(./hanken-grotesk-vietnamese-700-normal.C4RNfQp6.woff2)format("woff2"),url(./hanken-grotesk-vietnamese-700-normal.CslVg6dq.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:700;src:url(./hanken-grotesk-latin-ext-700-normal.CK2OfQqO.woff2)format("woff2"),url(./hanken-grotesk-latin-ext-700-normal.BXHnjv6S.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Hanken Grotesk;font-style:normal;font-display:swap;font-weight:700;src:url(./hanken-grotesk-latin-700-normal.CeQ8H3UY.woff2)format("woff2"),url(./hanken-grotesk-latin-700-normal.6IGCzoPh.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--ink:#14100c;--plate:#1f1913;--plate-raised:#241d16;--hairline:#2e251b;--bronze:#c8843c;--bronze-strong:#e0a05c;--verdigris:#4e8d7c;--coral:#e0674a;--bone:#eae0d2;--muted:#9a8f7e;--font-sans:"Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif;--font-mono:"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;--step--1:.833rem;--step-0:1rem;--step-1:1.2rem;--step-2:1.44rem;--step-3:1.728rem;--step-4:2.074rem;--step-5:2.488rem;--radius:10px;--transition:.16s ease;background:var(--ink);color:var(--bone)}:root[data-theme=light]{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--ink:#f4ede0;--plate:#e7dac2;--plate-raised:#f0e6d3;--hairline:#cdb994;--bronze:#754515;--bronze-strong:#60370f;--verdigris:#2f6e5c;--coral:#b83f27;--bone:#241d16;--muted:#6b5f4d}@media (prefers-color-scheme:light){:root:not([data-theme=dark]){--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--ink:#f4ede0;--plate:#e7dac2;--plate-raised:#f0e6d3;--hairline:#cdb994;--bronze:#754515;--bronze-strong:#60370f;--verdigris:#2f6e5c;--coral:#b83f27;--bone:#241d16;--muted:#6b5f4d}}*{box-sizing:border-box}html,body{min-height:100%;margin:0;padding:0}body{background:var(--ink);color:var(--bone);font-family:var(--font-sans);font-size:var(--step-0);transition:background var(--transition), color var(--transition);line-height:1.5}h1,h2,h3{font-family:var(--font-sans);margin:0 0 .5em;font-weight:700;line-height:1.2}h1{font-size:var(--step-4)}h2{font-size:var(--step-2)}h3{font-size:var(--step-1)}code,.mono{font-family:var(--font-mono);font-variant-ligatures:none}a{color:var(--bronze-strong)}button,input,select,textarea{font-family:inherit;font-size:inherit;color:inherit}button{background:var(--plate-raised);border:1px solid var(--hairline);color:var(--bone);border-radius:var(--radius);cursor:pointer;transition:background var(--transition), border-color var(--transition);padding:.55em 1em}button:hover{border-color:var(--bronze)}button.primary{background:var(--bronze);border-color:var(--bronze);color:var(--ink);font-weight:600}button.primary:hover{background:var(--bronze-strong);border-color:var(--bronze-strong)}button:disabled{opacity:.5;cursor:not-allowed}input,select,textarea{background:var(--plate);border:1px solid var(--hairline);border-radius:calc(var(--radius) * .6);padding:.5em .7em}:focus-visible{outline:2px solid var(--bronze);outline-offset:2px}.card{background:var(--plate);border:1px solid var(--hairline);border-radius:var(--radius);padding:1.25rem}.muted{color:var(--muted)}.healthy{color:var(--verdigris)}.at-risk{color:var(--coral)}.visually-hidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.001ms!important;animation-duration:.001ms!important;animation-iteration-count:1!important}} diff --git a/crates/carapace-api/static/_app/immutable/assets/2.BO-zofLV.css b/crates/carapace-api/static/_app/immutable/assets/2.BO-zofLV.css new file mode 100644 index 0000000..b8ede71 --- /dev/null +++ b/crates/carapace-api/static/_app/immutable/assets/2.BO-zofLV.css @@ -0,0 +1 @@ +.banner.svelte-vde8u4{background:var(--plate);border:1px solid var(--coral);color:var(--coral);border-radius:var(--radius);justify-content:space-between;align-items:center;gap:1rem;margin-bottom:1rem;padding:.75rem 1rem;display:flex}.banner.svelte-vde8u4 button:where(.svelte-vde8u4){border-color:var(--coral);color:var(--coral);background:0 0;flex-shrink:0}.shell.svelte-v2pnom{grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1.25rem;display:grid}.plate-group.svelte-v2pnom{background:var(--plate);border:1px solid var(--hairline);border-radius:var(--radius);animation:.42s backwards svelte-v2pnom-reveal;animation-delay:calc(var(--i) * 80ms);padding:1.25rem}@keyframes svelte-v2pnom-reveal{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.segments.svelte-v2pnom{margin-bottom:.9rem;display:flex}.segment.svelte-v2pnom{border:2px solid var(--hairline);clip-path:polygon(14% 0,100% 0,86% 100%,0 100%);background:0 0;flex:1;height:34px;margin-left:-10px}.segment.svelte-v2pnom:first-child{margin-left:0}.state-healthy.svelte-v2pnom .segment.filled:where(.svelte-v2pnom){background:var(--verdigris);border-color:var(--verdigris)}.state-at-risk.svelte-v2pnom .segment.filled:where(.svelte-v2pnom){background:var(--coral);border-color:var(--coral)}.state-at-risk.svelte-v2pnom .segment:where(.svelte-v2pnom):not(.filled){border-color:var(--coral);border-style:dashed}.state-empty.svelte-v2pnom .segment:where(.svelte-v2pnom){border-style:dashed}.label.svelte-v2pnom{font-size:var(--step--1);text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}.value.svelte-v2pnom{font-size:var(--step-3);margin:.15em 0;font-weight:600}.state-healthy.svelte-v2pnom .value:where(.svelte-v2pnom){color:var(--verdigris)}.state-at-risk.svelte-v2pnom .value:where(.svelte-v2pnom){color:var(--coral)}.note.svelte-v2pnom{font-size:var(--step--1)}.hex.svelte-3sdcoe{cursor:pointer;color:inherit;background:0 0;border:none;align-items:baseline;gap:.5em;padding:0;display:inline-flex}.hex.svelte-3sdcoe:hover .hint:where(.svelte-3sdcoe),.hex.svelte-3sdcoe:focus-visible .hint:where(.svelte-3sdcoe){color:var(--bronze)}.hint.svelte-3sdcoe{font-size:var(--step--1);font-family:var(--font-sans)}.node.svelte-156cahi{flex-wrap:wrap;gap:2rem;margin:1.5rem 0;display:flex}.label.svelte-156cahi{font-size:var(--step--1);text-transform:uppercase;letter-spacing:.06em;margin-bottom:.3em}.actions.svelte-156cahi{flex-wrap:wrap;gap:.75rem;display:flex}.button-link.svelte-156cahi{background:var(--plate-raised);border:1px solid var(--hairline);color:var(--bone);border-radius:var(--radius);transition:border-color var(--transition);padding:.55em 1em;text-decoration:none}.button-link.svelte-156cahi:hover,.button-link.svelte-156cahi:focus-visible{border-color:var(--bronze)}.publish-form.svelte-1t2bjk0 label:where(.svelte-1t2bjk0){font-size:var(--step--1);color:var(--muted);margin-bottom:.4rem;display:block}.publish-form.svelte-1t2bjk0+.publish-form:where(.svelte-1t2bjk0){margin-top:1rem}.publish-form.svelte-1t2bjk0>input:where(.svelte-1t2bjk0){width:100%;margin-bottom:.6rem}.publish-form.svelte-1t2bjk0>button:where(.svelte-1t2bjk0){margin-top:.4rem}.row.svelte-1t2bjk0{flex-wrap:wrap;align-items:center;gap:.6rem;display:flex}.row.svelte-1t2bjk0 input:where(.svelte-1t2bjk0){flex:1;min-width:180px}.list.svelte-1t2bjk0{flex-direction:column;gap:.75rem;margin-top:1.5rem;display:flex}.vault-row.svelte-1t2bjk0{grid-template-columns:1fr 2fr auto;align-items:start;gap:1.5rem;display:grid}@media (width<=640px){.vault-row.svelte-1t2bjk0{grid-template-columns:1fr;gap:.9rem}.row.svelte-1t2bjk0>input:where(.svelte-1t2bjk0),.peer-row.svelte-1t2bjk0>input:where(.svelte-1t2bjk0){width:100%;min-width:0}}.member-list.svelte-1t2bjk0{flex-direction:column;gap:.2rem;margin:.3rem 0 0;padding:0;list-style:none;display:flex}.label.svelte-1t2bjk0{font-size:var(--step--1);text-transform:uppercase;letter-spacing:.06em}.peer-row.svelte-1t2bjk0{margin-bottom:.5rem}.grid.svelte-1owrzkc{grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem;margin:1rem 0;display:grid}form.svelte-1owrzkc label:where(.svelte-1owrzkc),.ticket.svelte-1owrzkc label:where(.svelte-1owrzkc){font-size:var(--step--1);margin:.6rem 0 .3rem;display:block}form.svelte-1owrzkc input:where(.svelte-1owrzkc){width:100%}form.svelte-1owrzkc button:where(.svelte-1owrzkc){margin-top:1rem}.row.svelte-1owrzkc{gap:.5rem;display:flex}.row.svelte-1owrzkc input:where(.svelte-1owrzkc){flex:1}.list.svelte-1owrzkc{flex-direction:column;gap:.6rem;display:flex}.friend-row.svelte-1owrzkc{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.5rem;display:flex}.confirm.svelte-1owrzkc{flex-wrap:wrap;align-items:center;gap:.5rem;display:flex}button.danger.svelte-1owrzkc{border-color:var(--coral);color:var(--coral)}button.danger.svelte-1owrzkc:hover{background:var(--coral);color:var(--ink)}.row.svelte-imw02{flex-wrap:wrap;align-items:center;gap:1rem;margin-bottom:.75rem;display:flex}.row.svelte-imw02 label:where(.svelte-imw02){font-size:var(--step--1);color:var(--muted);align-items:center;gap:.4rem;display:flex}fieldset.svelte-imw02{border:1px solid var(--hairline);border-radius:var(--radius);margin:0 0 .75rem;padding:.75rem}fieldset.svelte-imw02>label:where(.svelte-imw02),.trustee-list.svelte-imw02 label:where(.svelte-imw02){align-items:center;gap:.5rem;margin:.35rem 0;display:flex}legend.svelte-imw02{font-weight:600}.trustee-list.svelte-imw02{grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:0 1rem;display:grid}.list.svelte-imw02{flex-direction:column;gap:.5rem;display:flex}.set-row.svelte-imw02{gap:1.5rem;display:flex}.share-row.svelte-imw02{align-items:center;gap:.6rem;margin-bottom:.5rem;display:flex}.share-text.svelte-imw02{word-break:break-all;background:var(--plate-raised);border-radius:6px;flex:1;padding:.4em .6em}.grid.svelte-imw02{grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:1rem;display:grid}.grid.svelte-imw02 label:where(.svelte-imw02){font-size:var(--step--1);margin:.5rem 0 .3rem;display:block}.grid.svelte-imw02 input:where(.svelte-imw02),.grid.svelte-imw02 select:where(.svelte-imw02){width:100%;margin-bottom:.5rem}.ceremony.alarm.svelte-imw02{border-color:var(--coral)}.alarm-text.svelte-imw02{color:var(--coral);font-weight:600}dl.svelte-imw02{gap:.5rem;display:grid}dl.svelte-imw02 div:where(.svelte-imw02){grid-template-columns:minmax(6rem,.25fr) 1fr;gap:.75rem;display:grid}dt.svelte-imw02{color:var(--muted)}dd.svelte-imw02{overflow-wrap:anywhere;min-width:0;margin:0}button.danger.svelte-imw02{border-color:var(--coral);color:var(--coral)}.confirm-action.svelte-imw02{align-items:center;gap:.5rem;display:flex!important}.confirm-action.svelte-imw02 input:where(.svelte-imw02){width:auto;margin:0}@media (width<=640px){.share-row.svelte-imw02,.set-row.svelte-imw02{flex-direction:column;align-items:stretch}dl.svelte-imw02 div:where(.svelte-imw02){grid-template-columns:1fr;gap:.1rem}}.resplit-head.svelte-imw02{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:1rem;display:flex}.phase.svelte-imw02{font-size:var(--step--1);border:1px solid var(--hairline);border-radius:999px;padding:.2em .6em}.phase.complete.svelte-imw02{color:var(--verdigris);border-color:var(--verdigris)}.phase.ready_to_destroy.svelte-imw02{color:var(--bronze-strong);border-color:var(--bronze)}.phase.required.svelte-imw02{color:var(--coral);border-color:var(--coral)}.gauges.svelte-imw02{grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1rem;margin-top:.75rem;display:grid}.reach.svelte-imw02{flex-direction:column;gap:.4rem;margin-top:.4rem;display:flex}.reach-row.svelte-imw02{flex-wrap:wrap;align-items:center;gap:.6rem;display:flex}.dot.svelte-imw02{background:var(--muted);border-radius:50%;flex:none;width:.6rem;height:.6rem}.dot.done.svelte-imw02{background:var(--verdigris)}.dot.online.svelte-imw02{background:var(--bronze)}.dot.will_queue.svelte-imw02,.dot.offline.svelte-imw02{background:var(--muted)}.role.svelte-imw02{font-size:var(--step--1);color:var(--muted)}form.svelte-d3lnpc label:where(.svelte-d3lnpc){font-size:var(--step--1);margin:.6rem 0 .3rem;display:block}form.svelte-d3lnpc input:where(.svelte-d3lnpc),form.svelte-d3lnpc select:where(.svelte-d3lnpc),form.svelte-d3lnpc textarea:where(.svelte-d3lnpc){width:100%}form.svelte-d3lnpc button:where(.svelte-d3lnpc){margin-top:1rem}.row.svelte-d3lnpc{align-items:center;gap:.5rem;display:flex}.share-text.svelte-d3lnpc{word-break:break-all;background:var(--plate-raised);border-radius:6px;flex:1;padding:.4em .6em}.confirm-action.svelte-d3lnpc{align-items:center;gap:.5rem;display:flex}.confirm-action.svelte-d3lnpc input:where(.svelte-d3lnpc){width:auto}@media (width<=640px){.row.svelte-d3lnpc{flex-direction:column;align-items:stretch}}.app.svelte-1uha8ag{max-width:960px;margin:0 auto;padding:1.5rem}header.svelte-1uha8ag{border-bottom:1px solid var(--hairline);flex-wrap:wrap;align-items:center;gap:1.5rem;margin-bottom:2rem;padding-bottom:1rem;display:flex}.brand.svelte-1uha8ag{font-weight:700;font-size:var(--step-2);align-items:center;gap:.5rem;display:flex}.mark.svelte-1uha8ag{color:var(--bronze)}nav.svelte-1uha8ag{flex-wrap:wrap;flex:1;gap:.25rem;display:flex}nav.svelte-1uha8ag a:where(.svelte-1uha8ag){color:var(--muted);border-radius:var(--radius);transition:color var(--transition), background var(--transition);padding:.4em .8em;text-decoration:none}nav.svelte-1uha8ag a:where(.svelte-1uha8ag):hover,nav.svelte-1uha8ag a:where(.svelte-1uha8ag):focus-visible{color:var(--bone);background:var(--plate)}nav.svelte-1uha8ag a.active:where(.svelte-1uha8ag){color:var(--bronze);background:var(--plate);font-weight:600}.status-and-theme.svelte-1uha8ag{align-items:center;gap:.75rem;display:flex}.live-dot.svelte-1uha8ag{background:var(--coral);border-radius:50%;width:10px;height:10px;display:inline-block}.live-dot.live.svelte-1uha8ag{background:var(--verdigris)}.connection-text.svelte-1uha8ag{font-size:var(--step--1);color:var(--muted)}@media (width<=640px){.app.svelte-1uha8ag{padding:1rem}header.svelte-1uha8ag{gap:.75rem}nav.svelte-1uha8ag{flex-basis:100%;order:3}nav.svelte-1uha8ag a:where(.svelte-1uha8ag){padding:.55em .65em}.status-and-theme.svelte-1uha8ag{margin-left:auto}} diff --git a/crates/carapace-api/static/_app/immutable/assets/2.Ccp3aMT0.css b/crates/carapace-api/static/_app/immutable/assets/2.Ccp3aMT0.css deleted file mode 100644 index cada163..0000000 --- a/crates/carapace-api/static/_app/immutable/assets/2.Ccp3aMT0.css +++ /dev/null @@ -1 +0,0 @@ -.banner.svelte-vde8u4{background:var(--plate);border:1px solid var(--coral);color:var(--coral);border-radius:var(--radius);justify-content:space-between;align-items:center;gap:1rem;margin-bottom:1rem;padding:.75rem 1rem;display:flex}.banner.svelte-vde8u4 button:where(.svelte-vde8u4){border-color:var(--coral);color:var(--coral);background:0 0;flex-shrink:0}.shell.svelte-v2pnom{grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1.25rem;display:grid}.plate-group.svelte-v2pnom{background:var(--plate);border:1px solid var(--hairline);border-radius:var(--radius);animation:.42s backwards svelte-v2pnom-reveal;animation-delay:calc(var(--i) * 80ms);padding:1.25rem}@keyframes svelte-v2pnom-reveal{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.segments.svelte-v2pnom{margin-bottom:.9rem;display:flex}.segment.svelte-v2pnom{border:2px solid var(--hairline);clip-path:polygon(14% 0,100% 0,86% 100%,0 100%);background:0 0;flex:1;height:34px;margin-left:-10px}.segment.svelte-v2pnom:first-child{margin-left:0}.state-healthy.svelte-v2pnom .segment.filled:where(.svelte-v2pnom){background:var(--verdigris);border-color:var(--verdigris)}.state-at-risk.svelte-v2pnom .segment.filled:where(.svelte-v2pnom){background:var(--coral);border-color:var(--coral)}.state-at-risk.svelte-v2pnom .segment:where(.svelte-v2pnom):not(.filled){border-color:var(--coral);border-style:dashed}.state-empty.svelte-v2pnom .segment:where(.svelte-v2pnom){border-style:dashed}.label.svelte-v2pnom{font-size:var(--step--1);text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}.value.svelte-v2pnom{font-size:var(--step-3);margin:.15em 0;font-weight:600}.state-healthy.svelte-v2pnom .value:where(.svelte-v2pnom){color:var(--verdigris)}.state-at-risk.svelte-v2pnom .value:where(.svelte-v2pnom){color:var(--coral)}.note.svelte-v2pnom{font-size:var(--step--1)}.hex.svelte-3sdcoe{cursor:pointer;color:inherit;background:0 0;border:none;align-items:baseline;gap:.5em;padding:0;display:inline-flex}.hex.svelte-3sdcoe:hover .hint:where(.svelte-3sdcoe),.hex.svelte-3sdcoe:focus-visible .hint:where(.svelte-3sdcoe){color:var(--bronze)}.hint.svelte-3sdcoe{font-size:var(--step--1);font-family:var(--font-sans)}.node.svelte-156cahi{flex-wrap:wrap;gap:2rem;margin:1.5rem 0;display:flex}.label.svelte-156cahi{font-size:var(--step--1);text-transform:uppercase;letter-spacing:.06em;margin-bottom:.3em}.actions.svelte-156cahi{flex-wrap:wrap;gap:.75rem;display:flex}.button-link.svelte-156cahi{background:var(--plate-raised);border:1px solid var(--hairline);color:var(--bone);border-radius:var(--radius);transition:border-color var(--transition);padding:.55em 1em;text-decoration:none}.button-link.svelte-156cahi:hover,.button-link.svelte-156cahi:focus-visible{border-color:var(--bronze)}.publish-form.svelte-1t2bjk0 label:where(.svelte-1t2bjk0){font-size:var(--step--1);color:var(--muted);margin-bottom:.4rem;display:block}.row.svelte-1t2bjk0{flex-wrap:wrap;align-items:center;gap:.6rem;display:flex}.row.svelte-1t2bjk0 input:where(.svelte-1t2bjk0){flex:1;min-width:180px}.list.svelte-1t2bjk0{flex-direction:column;gap:.75rem;margin-top:1.5rem;display:flex}.vault-row.svelte-1t2bjk0{grid-template-columns:1fr 2fr auto;align-items:start;gap:1.5rem;display:grid}.member-list.svelte-1t2bjk0{flex-direction:column;gap:.2rem;margin:.3rem 0 0;padding:0;list-style:none;display:flex}.label.svelte-1t2bjk0{font-size:var(--step--1);text-transform:uppercase;letter-spacing:.06em}.peer-row.svelte-1t2bjk0{margin-bottom:.5rem}.grid.svelte-1owrzkc{grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem;margin:1rem 0;display:grid}form.svelte-1owrzkc label:where(.svelte-1owrzkc),.ticket.svelte-1owrzkc label:where(.svelte-1owrzkc){font-size:var(--step--1);margin:.6rem 0 .3rem;display:block}form.svelte-1owrzkc input:where(.svelte-1owrzkc){width:100%}form.svelte-1owrzkc button:where(.svelte-1owrzkc){margin-top:1rem}.row.svelte-1owrzkc{gap:.5rem;display:flex}.row.svelte-1owrzkc input:where(.svelte-1owrzkc){flex:1}.list.svelte-1owrzkc{flex-direction:column;gap:.6rem;display:flex}.friend-row.svelte-1owrzkc{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.5rem;display:flex}.confirm.svelte-1owrzkc{flex-wrap:wrap;align-items:center;gap:.5rem;display:flex}button.danger.svelte-1owrzkc{border-color:var(--coral);color:var(--coral)}button.danger.svelte-1owrzkc:hover{background:var(--coral);color:var(--ink)}.row.svelte-imw02{flex-wrap:wrap;align-items:center;gap:1rem;margin-bottom:.75rem;display:flex}.row.svelte-imw02 label:where(.svelte-imw02){font-size:var(--step--1);color:var(--muted);align-items:center;gap:.4rem;display:flex}.list.svelte-imw02{flex-direction:column;gap:.5rem;display:flex}.set-row.svelte-imw02{gap:1.5rem;display:flex}.share-row.svelte-imw02{align-items:center;gap:.6rem;margin-bottom:.5rem;display:flex}.share-text.svelte-imw02{word-break:break-all;background:var(--plate-raised);border-radius:6px;flex:1;padding:.4em .6em}.grid.svelte-imw02{grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:1rem;display:grid}.grid.svelte-imw02 label:where(.svelte-imw02){font-size:var(--step--1);margin:.5rem 0 .3rem;display:block}.grid.svelte-imw02 input:where(.svelte-imw02){width:100%;margin-bottom:.5rem}.resplit-head.svelte-imw02{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:1rem;display:flex}.phase.svelte-imw02{font-size:var(--step--1);border:1px solid var(--hairline);border-radius:999px;padding:.2em .6em}.phase.complete.svelte-imw02{color:var(--verdigris);border-color:var(--verdigris)}.phase.ready_to_destroy.svelte-imw02{color:var(--bronze-strong);border-color:var(--bronze)}.phase.required.svelte-imw02{color:var(--coral);border-color:var(--coral)}.gauges.svelte-imw02{grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1rem;margin-top:.75rem;display:grid}.reach.svelte-imw02{flex-direction:column;gap:.4rem;margin-top:.4rem;display:flex}.reach-row.svelte-imw02{flex-wrap:wrap;align-items:center;gap:.6rem;display:flex}.dot.svelte-imw02{background:var(--muted);border-radius:50%;flex:none;width:.6rem;height:.6rem}.dot.done.svelte-imw02{background:var(--verdigris)}.dot.online.svelte-imw02{background:var(--bronze)}.dot.will_queue.svelte-imw02,.dot.offline.svelte-imw02{background:var(--muted)}.role.svelte-imw02{font-size:var(--step--1);color:var(--muted)}form.svelte-d3lnpc label:where(.svelte-d3lnpc){font-size:var(--step--1);margin:.6rem 0 .3rem;display:block}form.svelte-d3lnpc input:where(.svelte-d3lnpc),form.svelte-d3lnpc select:where(.svelte-d3lnpc),form.svelte-d3lnpc textarea:where(.svelte-d3lnpc){width:100%}form.svelte-d3lnpc button:where(.svelte-d3lnpc){margin-top:1rem}.row.svelte-d3lnpc{align-items:center;gap:.5rem;display:flex}.share-text.svelte-d3lnpc{word-break:break-all;background:var(--plate-raised);border-radius:6px;flex:1;padding:.4em .6em}.app.svelte-1uha8ag{max-width:960px;margin:0 auto;padding:1.5rem}header.svelte-1uha8ag{border-bottom:1px solid var(--hairline);flex-wrap:wrap;align-items:center;gap:1.5rem;margin-bottom:2rem;padding-bottom:1rem;display:flex}.brand.svelte-1uha8ag{font-weight:700;font-size:var(--step-2);align-items:center;gap:.5rem;display:flex}.mark.svelte-1uha8ag{color:var(--bronze)}nav.svelte-1uha8ag{flex-wrap:wrap;flex:1;gap:.25rem;display:flex}nav.svelte-1uha8ag a:where(.svelte-1uha8ag){color:var(--muted);border-radius:var(--radius);transition:color var(--transition), background var(--transition);padding:.4em .8em;text-decoration:none}nav.svelte-1uha8ag a:where(.svelte-1uha8ag):hover,nav.svelte-1uha8ag a:where(.svelte-1uha8ag):focus-visible{color:var(--bone);background:var(--plate)}nav.svelte-1uha8ag a.active:where(.svelte-1uha8ag){color:var(--bronze);background:var(--plate);font-weight:600}.status-and-theme.svelte-1uha8ag{align-items:center;gap:.75rem;display:flex}.live-dot.svelte-1uha8ag{background:var(--coral);border-radius:50%;width:10px;height:10px;display:inline-block}.live-dot.live.svelte-1uha8ag{background:var(--verdigris)} diff --git a/crates/carapace-api/static/_app/immutable/chunks/BGevx8A_.js b/crates/carapace-api/static/_app/immutable/chunks/BGevx8A_.js new file mode 100644 index 0000000..c2df053 --- /dev/null +++ b/crates/carapace-api/static/_app/immutable/chunks/BGevx8A_.js @@ -0,0 +1 @@ +import{G as e,K as t,M as n,N as r,X as i,j as a,r as o,t as s}from"./D7CCBGlz.js";import"./CCeg2KC3.js";var c=class{constructor(e,t){this.status=e,typeof t==`string`?this.body={message:t}:t?this.body=t:this.body={message:`Error: ${e}`}}toString(){return JSON.stringify(this.body)}},l=class{constructor(e,t){try{new Headers({location:t})}catch{throw Error(`Invalid redirect location ${JSON.stringify(t)}: this string contains characters that cannot be used in HTTP headers`)}this.status=e,this.location=t}},u=class extends Error{constructor(e,t,n){super(n),this.status=e,this.text=t}};new URL(`sveltekit-internal://`);function d(e,t){return e===`/`||t===`ignore`?e:t===`never`?e.endsWith(`/`)?e.slice(0,-1):e:t===`always`&&!e.endsWith(`/`)?e+`/`:e}function f(e){return e.split(`%25`).map(decodeURI).join(`%25`)}function p(e){for(let t in e)e[t]=decodeURIComponent(e[t]);return e}function m({href:e}){return e.split(`#`)[0]}function h(){}function g(...e){let t=5381;for(let n of e)if(typeof n==`string`){let e=n.length;for(;e;)t=t*33^n.charCodeAt(--e)}else if(ArrayBuffer.isView(n)){let e=new Uint8Array(n.buffer,n.byteOffset,n.byteLength),r=e.length;for(;r;)t=t*33^e[--r]}else throw TypeError(`value must be a string or TypedArray`);return(t>>>0).toString(36)}new TextEncoder;function _(e){let t=atob(e),n=new Uint8Array(t.length);for(let e=0;e((e instanceof Request?e.method:t?.method||`GET`)!==`GET`&&y.delete(x(e)),v(e,t));var y=new Map;function ee(e,t){let n=x(e,t),r=document.querySelector(n);if(r?.textContent){r.remove();let{body:e,...t}=JSON.parse(r.textContent);r.getAttribute(`data-b64`)!==null&&(e=_(e));let i=r.getAttribute(`data-ttl`);return i&&y.set(n,{body:e,init:t,ttl:1e3*Number(i)}),Promise.resolve(new Response(e,t))}return window.fetch(e,t)}function b(e,t,n){if(y.size>0){let t=x(e,n),r=y.get(t);if(r){if(performance.now(){let n=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(e);if(n)return t.push({name:n[1],matcher:n[2],optional:!1,rest:!0,chained:!0}),`(?:/([^]*))?`;let r=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(e);if(r)return t.push({name:r[1],matcher:r[2],optional:!0,rest:!1,chained:!0}),`(?:/([^/]+))?`;if(!e)return;let i=e.split(/\[(.+?)\](?!\])/);return`/`+i.map((e,n)=>{if(n%2){if(e.startsWith(`x+`))return se(String.fromCharCode(parseInt(e.slice(2),16)));if(e.startsWith(`u+`))return se(String.fromCharCode(...e.slice(2).split(`-`).map(e=>parseInt(e,16))));let[,r,a,o,s]=te.exec(e);return t.push({name:o,matcher:s,optional:!!r,rest:!!a,chained:a?n===1&&i[0]===``:!1}),a?`([^]*?)`:r?`([^/]*)?`:`([^/]+?)`}return se(e)}).join(``)}).join(``)}/?$`),params:t}}function ie(e){return e!==``&&!/^\([^)]+\)$/.test(e)}function ae(e){return e.slice(1).split(`/`).filter(ie)}function oe(e,t,n){let r={},i=e.slice(1),a=i.filter(e=>e!==void 0),o=0;for(let e=0;ee).join(`/`),o=0),c===void 0)if(s.rest)c=``;else continue;if(!s.matcher||n[s.matcher](c)){r[s.name]=c;let n=t[e+1],l=i[e+1];n&&!n.rest&&n.optional&&l&&s.chained&&(o=0),!n&&!l&&Object.keys(r).length===a.length&&(o=0);continue}if(s.optional&&s.chained){o++;continue}return}if(!o)return r}function se(e){return e.normalize().replace(/[[\]]/g,`\\$&`).replace(/%/g,`%25`).replace(/\//g,`%2[Ff]`).replace(/\?/g,`%3[Ff]`).replace(/#/g,`%23`).replace(/[.*+?^${}()|\\]/g,`\\$&`)}function ce({nodes:e,server_loads:t,dictionary:n,matchers:r}){let i=new Set(t);return Object.entries(n).map(([t,[n,i,s]])=>{let{pattern:c,params:l}=re(t),u={id:t,exec:e=>{let t=c.exec(e);if(t)return oe(t,l,r)},errors:[1,...s||[]].map(t=>e[t]),layouts:[0,...i||[]].map(o),leaf:a(n)};return u.errors.length=u.layouts.length=Math.max(u.errors.length,u.layouts.length),u});function a(t){let n=t<0;return n&&(t=~t),[n,e[t]]}function o(t){return t===void 0?t:[i.has(t),e[t]]}}function le(e,t=JSON.parse){try{return t(sessionStorage[e])}catch{}}function ue(e,t,n=JSON.stringify){let r=n(t);try{sessionStorage[e]=r}catch{}}var S=globalThis.__sveltekit_1wnehg?.base??``,de=globalThis.__sveltekit_1wnehg?.assets??S??``,fe=`0.0.1`,pe=`sveltekit:snapshot`,me=`sveltekit:scroll`,he=`sveltekit:states`,C=`sveltekit:history`,w=`sveltekit:navigation`,T={tap:1,hover:2,viewport:3,eager:4,off:-1,false:-1},ge=location.origin;function _e(e){if(e instanceof URL)return e;let t=document.baseURI;if(!t){let e=document.getElementsByTagName(`base`);t=e.length?e[0].href:document.URL}return new URL(e,t)}function E(){return{x:pageXOffset,y:pageYOffset}}function D(e,t){return e.getAttribute(`data-sveltekit-${t}`)}var ve={...T,"":T.hover};function ye(e){let t=e.assignedSlot??e.parentNode;return t?.nodeType===11&&(t=t.host),t}function be(e,t){for(;e&&e!==t;){if(e.nodeName.toUpperCase()===`A`&&e.hasAttribute(`href`))return e;e=ye(e)}}function xe(e,t,n){let r;try{if(r=new URL(e instanceof SVGAElement?e.href.baseVal:e.href,document.baseURI),n&&r.hash.match(/^#[^/]/)){let e=location.hash.split(`#`)[1]||`/`;r.hash=`#${e}${r.hash}`}}catch{}let i=e instanceof SVGAElement?e.target.baseVal:e.target,a=!r||!!i||k(r,t,n)||(e.getAttribute(`rel`)||``).split(/\s+/).includes(`external`),o=r?.origin===ge&&e.hasAttribute(`download`);return{url:r,external:a,target:i,download:o}}function O(e){let t=null,n=null,r=null,i=null,a=null,o=null,s=e;for(;s&&s!==document.documentElement;)r===null&&(r=D(s,`preload-code`)),i===null&&(i=D(s,`preload-data`)),t===null&&(t=D(s,`keepfocus`)),n===null&&(n=D(s,`noscroll`)),a===null&&(a=D(s,`reload`)),o===null&&(o=D(s,`replacestate`)),s=ye(s);function c(e){switch(e){case``:case`true`:return!0;case`off`:case`false`:return!1;default:return}}return{preload_code:ve[r??`off`],preload_data:ve[i??`off`],keepfocus:c(t),noscroll:c(n),reload:c(a),replace_state:c(o)}}function Se(e){let t=i(e),n=!0;function r(){n=!0,t.update(e=>e)}function a(e){n=!1,t.set(e)}function o(e){let r;return t.subscribe(t=>{(r===void 0||n&&t!==r)&&e(r=t)})}return{notify:r,set:a,subscribe:o}}var Ce={v:h};function we(){let{set:e,subscribe:t}=i(!1);async function n(){clearTimeout(void 0);try{let t=await fetch(`${de}/_app/version.json`,{headers:{pragma:`no-cache`,"cache-control":`no-cache`}});if(!t.ok)return!1;let n=(await t.json()).version!==fe;return n&&(e(!0),Ce.v(),clearTimeout(void 0)),n}catch{return!1}}return{subscribe:t,check:n}}function k(e,t,n){return e.origin!==ge||!e.pathname.startsWith(t)?!0:n?e.pathname!==location.pathname:!1}function Te(e){}var Ee=new Set([`load`,`prerender`,`csr`,`ssr`,`trailingSlash`,`config`]);[...Ee],[...new Set([...Ee])];function De(e){return e.filter(e=>e!=null)}function A(e,t){return e+`/`+t}function Oe(e){return e instanceof c||e instanceof u?e.status:500}function ke(e){return e instanceof u?e.text:`Internal Error`}var j,M,N,Ae=o.toString().includes(`$$`)||/function \w+\(\) \{\}/.test(o.toString()),je=`a:`;Ae?(j={data:{},form:null,error:null,params:{},route:{id:null},state:{},status:-1,url:new URL(je)},M={current:null},N={current:!1}):(j=new class{#e=t({});get data(){return a(this.#e)}set data(t){e(this.#e,t)}#t=t(null);get form(){return a(this.#t)}set form(t){e(this.#t,t)}#n=t(null);get error(){return a(this.#n)}set error(t){e(this.#n,t)}#r=t({});get params(){return a(this.#r)}set params(t){e(this.#r,t)}#i=t({id:null});get route(){return a(this.#i)}set route(t){e(this.#i,t)}#a=t({});get state(){return a(this.#a)}set state(t){e(this.#a,t)}#o=t(-1);get status(){return a(this.#o)}set status(t){e(this.#o,t)}#s=t(new URL(je));get url(){return a(this.#s)}set url(t){e(this.#s,t)}},M=new class{#e=t(null);get current(){return a(this.#e)}set current(t){e(this.#e,t)}},N=new class{#e=t(!1);get current(){return a(this.#e)}set current(t){e(this.#e,t)}},Ce.v=()=>N.current=!0);function Me(e){Object.assign(j,e)}var{onMount:Ne,tick:Pe}=s,Fe=new Set([`icon`,`shortcut icon`,`apple-touch-icon`]),P=null,F=le(`sveltekit:scroll`)??{},I=le(`sveltekit:snapshot`)??{},L={url:Se({}),page:Se({}),navigating:i(null),updated:we()};function Ie(e){F[e]=E()}function Le(e,t){let n=e+1;for(;F[n];)delete F[n],n+=1;for(n=t+1;I[n];)delete I[n],n+=1}function R(e,t=!1){return t?location.replace(e.href):location.href=e.href,new Promise(h)}async function Re(){if(`serviceWorker`in navigator){let e=await navigator.serviceWorker.getRegistration(S||`/`);e&&await e.update()}}var ze,Be,z,B,Ve,V,He={},Ue={},H=[],We=[],U=null;function Ge(){U?.fork?.then(e=>e?.discard()),U=null,Q={element:void 0,href:void 0}}var Ke=new Map,qe=new Set,Je=new Set,W=new Set,G={branch:[],error:null,url:null,nav:null},Ye=!1,Xe=!1,Ze=!0,K=!1,q=!1,Qe=!1,$e=!1,et,J,Y,X,tt=new Set,nt=new Map,rt=new Map;async function it(e,t,n){if(globalThis.__sveltekit_1wnehg.data){let{q:e={},p:t={},l:n={},f:r={}}=globalThis.__sveltekit_1wnehg.data;for(let t in e)He[t]=e[t];for(let e in n)He[e]=n[e];for(let e in r)He[e]=r[e];for(let e in t)Ue[e]=t[e]}document.URL!==location.href&&(location.href=location.href),V=e,await e.hooks.init?.(),ze=ce(e),B=document.documentElement,Ve=t,Be=e.nodes[0],z=e.nodes[1],Be(),z(),J=history.state?.[C],Y=history.state?.[w],J||(J=Y=Date.now(),history.replaceState({...history.state,[C]:J,[w]:Y},``));let r=F[J];function i(){r&&(history.scrollRestoration=`manual`,scrollTo(r.x,r.y))}n?(i(),await Mt(Ve,n)):(await Z({type:`enter`,url:_e(V.hash?Rt(new URL(location.href)):location.href),replace_state:!0}),i()),jt()}function at(){H.length=0,$e=!1}function ot(e){We.some(e=>e?.snapshot)&&(I[e]=We.map(e=>e?.snapshot?.capture()))}function st(e){I[e]?.forEach((e,t)=>{We[t]?.snapshot?.restore(e)})}function ct(){Ie(J),ue(me,F),ot(Y),ue(pe,I)}async function lt(e,t,n,i){let a,o;t.invalidateAll&&Ge(),await Z({type:`goto`,url:_e(e),keepfocus:t.keepFocus,noscroll:t.noScroll,replace_state:t.replaceState,state:t.state,redirect_count:n,nav_token:i,accept:()=>{if(t.invalidateAll){$e=!0,a=new Set;for(let[e,t]of nt)for(let[n,r]of t)r.resource?.reset(),a.add(A(e,n));o=new Set;for(let[e,t]of rt)for(let n of t.keys())o.add(A(e,n))}t.invalidate&&t.invalidate.forEach(At)}}),t.invalidateAll&&r().then(r).then(()=>{for(let[e,t]of nt)for(let[n,{resource:r}]of t)a?.has(A(e,n))&&r.start();for(let[e,t]of rt)for(let[n,{resource:r}]of t)o?.has(A(e,n))&&r.reconnect()})}async function ut(e){if(e.id!==U?.id){Ge();let t={};tt.add(t),U={id:e.id,token:t,promise:bt({...e,preload:t}).then(e=>(tt.delete(t),e.type===`loaded`&&e.state.error&&Ge(),e)),fork:null}}return U.promise}async function dt(e){let t=(await wt(e,!1))?.route;t&&await Promise.all([...t.layouts,t.leaf].filter(Boolean).map(e=>e[1]()))}async function ft(e,t,n){let r={params:G.params,route:{id:G.route?.id??null},url:new URL(location.href)};if(G={...e.state,nav:r},Me(e.props.page),et=new V.root({target:t,props:{...e.props,stores:L,components:We},hydrate:n,sync:!1,transformError:void 0}),await Promise.resolve(),n){let e={from:null,to:{...r,scroll:F[J]??E()},willUnload:!1,type:`enter`,complete:Promise.resolve()};W.forEach(t=>t(e))}st(Y),Xe=!0}async function pt({url:e,params:t,branch:n,errors:r,status:i,error:a,route:o,form:s}){let c=`never`;if(S&&(e.pathname===S||e.pathname===S+`/`))c=`always`;else for(let e of n)e?.slash!==void 0&&(c=e.slash);e.pathname=d(e.pathname,c),e.search=e.search;let l={type:`loaded`,state:{url:e,params:t,branch:n,error:a,route:o},props:{constructors:De(n).map(e=>e.node.component),page:Lt(j)}};s!==void 0&&(l.props.form=s);let u={},f=!j,p=0;for(let e=0;et(new URL(e))))return!0;return!1}function _t(e,t){return e?.type===`data`?e:e?.type===`skip`?t??null:null}function vt(e,t){if(!e)return new Set(t.searchParams.keys());let n=new Set([...e.searchParams.keys(),...t.searchParams.keys()]);for(let r of n){let i=e.searchParams.getAll(r),a=t.searchParams.getAll(r);i.every(e=>a.includes(e))&&a.every(e=>i.includes(e))&&n.delete(r)}return n}function yt({error:e,url:t,route:n,params:r}){return{type:`loaded`,state:{error:e,url:t,route:n,params:r,branch:[]},props:{page:Lt(j),constructors:[]}}}async function bt({id:e,invalidating:t,url:n,params:r,route:i,preload:a}){if(U?.id===e)return tt.delete(U.token),U.promise;let{errors:o,layouts:s,leaf:u}=i,d=[...s,u];o.forEach(e=>e?.().catch(h)),d.forEach(e=>e?.[1]().catch(h));let f=G.url?e!==Et(G.url):!1,p=G.route?i.id!==G.route.id:!1,m=vt(G.url,n),g=!1,_=d.map(async(e,t)=>{if(!e)return;let a=G.branch[t];return e[1]===a?.loader&&!gt(g,p,f,m,a.universal?.uses,r)?a:(g=!0,mt({loader:e[1],url:n,params:r,route:i,parent:async()=>{let e={};for(let n=0;nPromise.resolve({}),server_data_node:_t(null)}),{node:await z(),loader:z,universal:null,server:null,data:null}],status:e,error:t,errors:[],route:null})}catch(t){if(t instanceof l){await lt(new URL(t.location,location.href),{},0);return}let a=await V.get_error_template(),o=await $(t,{url:n,params:i,route:r}),s=a({status:e,message:String(o?.message??``).replace(/&/g,`&`).replace(//g,`>`)}),c=new DOMParser().parseFromString(s,`text/html`);throw document.documentElement.replaceChild(document.adoptNode(c.head),document.head),document.documentElement.replaceChild(document.adoptNode(c.body),document.body),t}}async function Ct(e){let t=e.href;if(Ke.has(t))return Ke.get(t);let n;try{let r=(async()=>{let t=await V.hooks.reroute({url:new URL(e),fetch:async(t,n)=>ht(t,n,e).promise})??e;if(typeof t==`string`){let n=new URL(e);V.hash?n.hash=t:n.pathname=t,t=n}return t})();Ke.set(t,r),n=await r}catch{Ke.delete(t);return}return n}async function wt(e,t){if(e&&!k(e,S,V.hash)){let n=await Ct(e);if(!n)return;let r=Tt(n);for(let n of ze){let i=n.exec(r);if(i)return{id:Et(e),invalidating:t,route:n,params:p(i),url:e}}}}function Tt(e){return f(V.hash?e.hash.replace(/^#/,``).replace(/[?#].+/,``):e.pathname.slice(S.length))||`/`}function Et(e){return(V.hash?e.hash.replace(/^#/,``):e.pathname)+e.search}function Dt({url:e,type:t,intent:n,delta:r,event:i,scroll:a}){let o=!1,s=It(G,n,e,t,a??null);r!==void 0&&(s.navigation.delta=r),i!==void 0&&(s.navigation.event=i);let c={...s.navigation,cancel:()=>{o=!0,s.reject(Error(`navigation cancelled`))}};return K||qe.forEach(e=>e(c)),o?null:s}async function Z({type:e,url:t,popped:i,keepfocus:a,noscroll:o,replace_state:s,state:c={},redirect_count:l=0,nav_token:d={},accept:f=h,block:p=h,event:m}){let g=X;X=d;let _=await wt(t,!1),v=e===`enter`?It(G,_,t,e):Dt({url:t,type:e,delta:i?.delta,intent:_,scroll:i?.scroll,event:m});if(!v){p(),X===d&&(X=g);return}let y=J,ee=Y;f(),K=!0,Xe&&v.navigation.type!==`enter`&&L.navigating.set(M.current=v.navigation);let b=_&&await bt(_);if(!b){if(k(t,S,V.hash))return await R(t,s);b=await Ot(t,{id:null},await $(new u(404,`Not Found`,`Not found: ${t.pathname}`),{url:t,params:{},route:{id:null}}),404,s)}if(t=_?.url||t,X!==d){v.reject(Error(`navigation aborted`));return}if(!b)return;if(b.type===`redirect`){if(l<20){await Z({type:e,url:new URL(b.location,t),popped:i,keepfocus:a,noscroll:o,replace_state:s,state:c,redirect_count:l+1,nav_token:d}),v.fulfil(void 0);return}if(b=await St({status:500,error:await $(Error(`Redirect loop`),{url:t,params:{},route:{id:null}}),url:t,route:{id:null}}),!b)return}else if(b.props.page.status>=400&&await L.updated.check())return await Re(),await R(t,s);if(at(),Ie(y),ot(ee),b.props.page.url.pathname!==t.pathname&&(t.pathname=b.props.page.url.pathname),c=i?i.state:c,!i){let e=+!s,n={[C]:J+=e,[w]:Y+=e,[he]:c};(s?history.replaceState:history.pushState).call(history,n,``,t),s||Le(J,Y)}let x=_&&U?.id===_.id?U.fork:null;U?.fork&&!x?Ge():(U=null,Q={element:void 0,href:void 0}),b.props.page.state=c;let te;if(Xe){let e=(await Promise.all(Array.from(Je,e=>e(v.navigation)))).filter(e=>typeof e==`function`);if(e.length>0){function t(){e.forEach(e=>{W.delete(e)})}e.push(t),e.forEach(e=>{W.add(e)})}let r=v.navigation.to;G={...b.state,nav:{params:r.params,route:r.route,url:r.url}},b.props.page&&(b.props.page.url=t),!a&&document.activeElement instanceof HTMLElement&&document.activeElement!==document.body&&document.activeElement.blur();let i=x&&await x;i?te=i.commit():(P=null,et.$set(b.props),P&&Object.assign(b.props.page,P),Me(b.props.page),te=n?.()),Qe=!0}else await ft(b,Ve,!1);let{activeElement:ne}=document;if(await te,await r(),await r(),X!==d){v.reject(Error(`navigation aborted`));return}b.props.page&&P&&Object.assign(b.props.page,P);let re=null;if(Ze){let e=i?i.scroll:o?E():null;e?scrollTo(e.x,e.y):(re=t.hash&&document.getElementById(zt(t)))?re.scrollIntoView():scrollTo(0,0)}let ie=document.activeElement!==ne&&document.activeElement!==document.body;!a&&!ie&&Ft(t,!re),Ze=!0,K=!1,v.fulfil(void 0),v.navigation.to&&(v.navigation.to.scroll=E()),W.forEach(e=>e(v.navigation)),e===`popstate`&&st(Y),L.navigating.set(M.current=null)}async function Ot(e,t,n,r,i){return e.origin===ge&&e.pathname===location.pathname&&!Ye?await St({status:r,error:n,url:e,route:t}):await R(e,i)}var Q={element:void 0,href:void 0};function kt(){let e,t;B.addEventListener(`mousemove`,t=>{let n=t.target;clearTimeout(e),e=setTimeout(()=>{i(n,T.hover)},20)});function n(e){e.defaultPrevented||i(e.composedPath()[0],T.tap)}B.addEventListener(`mousedown`,n),B.addEventListener(`touchstart`,n,{passive:!0});let r=new IntersectionObserver(e=>{for(let t of e)t.isIntersecting&&(dt(new URL(t.target.href)),r.unobserve(t.target))},{threshold:0});async function i(e,n){let r=be(e,B),i=r===Q.element&&r?.href===Q.href&&n>=t;if(!r||i)return;let{url:a,external:o,download:s}=xe(r,S,V.hash);if(o||s)return;let c=O(r),l=a&&Et(G.url)===Et(a);if(!(c.reload||l))if(n<=c.preload_data){Q={element:r,href:r.href},t=T.tap;let e=await wt(a,!1);if(!e)return;ut(e)}else n<=c.preload_code&&(Q={element:r,href:r.href},t=n,dt(a))}function a(){r.disconnect();for(let e of B.querySelectorAll(`a`)){let{url:t,external:n,download:i}=xe(e,S,V.hash);if(n||i)continue;let a=O(e);a.reload||(a.preload_code===T.viewport&&r.observe(e),a.preload_code===T.eager&&dt(t))}}W.add(a),a()}function $(e,t){if(e instanceof c)return e.body;let n=Oe(e),r=ke(e);return V.hooks.handleError({error:e,event:t,status:n,message:r})??{message:r}}function At(e){if(typeof e==`function`)H.push(e);else{let{href:t}=new URL(e,location.href);H.push(e=>e.href===t)}}function jt(){history.scrollRestoration=`manual`,addEventListener(`beforeunload`,e=>{let t=!1;if(ct(),!K){let e=It(G,void 0,null,`leave`),n={...e.navigation,cancel:()=>{t=!0,e.reject(Error(`navigation cancelled`))}};qe.forEach(e=>e(n))}t?(e.preventDefault(),e.returnValue=``):history.scrollRestoration=`auto`}),addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&ct()}),navigator.connection?.saveData||kt(),B.addEventListener(`click`,async t=>{if(t.button||t.which!==1||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey||t.defaultPrevented)return;let n=be(t.composedPath()[0],B);if(!n)return;let{url:r,external:i,target:a,download:o}=xe(n,S,V.hash);if(!r)return;if(a===`_parent`||a===`_top`){if(window.parent!==window)return}else if(a&&a!==`_self`)return;let s=O(n);if(!(n instanceof SVGAElement)&&r.protocol!==location.protocol&&!(r.protocol===`https:`||r.protocol===`http:`)||o)return;let[c,l]=(V.hash?r.hash.replace(/^#/,``):r.href).split(`#`),u=c===m(location);if(i||s.reload&&(!u||!l)){Dt({url:r,type:`link`,event:t})?K=!0:t.preventDefault();return}if(l!==void 0&&u){let[,i]=G.url.href.split(`#`);if(i===l){if(t.preventDefault(),l===``||l===`top`&&n.ownerDocument.getElementById(`top`)===null)scrollTo({top:0});else{let e=n.ownerDocument.getElementById(decodeURIComponent(l));e&&(e.scrollIntoView(),e.focus())}return}if(q=!0,Ie(J),e(r),!s.replace_state)return;q=!1}t.preventDefault(),await new Promise(e=>{requestAnimationFrame(()=>{setTimeout(e,0)}),setTimeout(e,100)}),await Z({type:`link`,url:r,keepfocus:s.keepfocus,noscroll:s.noscroll,replace_state:s.replace_state??r.href===location.href,event:t})}),B.addEventListener(`submit`,e=>{if(e.defaultPrevented)return;let t=HTMLFormElement.prototype.cloneNode.call(e.target),n=e.submitter;if((n?.formTarget||t.target)===`_blank`||(n?.formMethod||t.method)!==`get`)return;let r=new URL(n?.hasAttribute(`formaction`)&&n?.formAction||t.action);if(k(r,S,!1))return;let i=e.target,a=O(i);if(a.reload)return;e.preventDefault(),e.stopPropagation();let o=new FormData(i,n);r.search=new URLSearchParams(o).toString(),Z({type:`form`,url:r,keepfocus:a.keepfocus,noscroll:a.noscroll,replace_state:a.replace_state??r.href===location.href,event:e})}),addEventListener(`popstate`,async t=>{if(!Pt)if(t.state?.[`sveltekit:history`]){let n=t.state[C];if(X={},n===J)return;let r=F[n],i=t.state[`sveltekit:states`]??{},a=new URL(t.state[`sveltekit:pageurl`]??location.href),o=t.state[w],s=G.url?m(location)===m(G.url):!1;if(o===Y&&(Qe||s)){i!==j.state&&(j.state=i),e(a),F[J]=E(),r&&scrollTo(r.x,r.y),J=n;return}let c=n-J;await Z({type:`popstate`,url:a,popped:{state:i,scroll:r,delta:c},accept:()=>{J=n,Y=o},block:()=>{history.go(-c)},nav_token:X,event:t})}else q||(e(new URL(location.href)),V.hash&&location.reload())}),addEventListener(`hashchange`,()=>{q&&(q=!1,history.replaceState({...history.state,[C]:++J,[w]:Y},``,location.href))});for(let e of document.querySelectorAll(`link`))Fe.has(e.rel)&&(e.href=e.href);addEventListener(`pageshow`,e=>{e.persisted&&L.navigating.set(M.current=null)});function e(e){G.url=j.url=e,L.page.set(Lt(j)),L.page.notify()}}async function Mt(e,{status:t=200,error:n,node_ids:r,params:i,route:a,server_route:o,data:s,form:c}){Ye=!0;let u=new URL(location.href),d;({params:i={},route:a={id:null}}=await wt(u,!1)||{}),d=ze.find(({id:e})=>e===a.id);let f,p=!0;try{let e=r.map(async(t,n)=>{let r=s[n];return r?.uses&&(r.uses=Nt(r.uses)),mt({loader:V.nodes[t],url:u,params:i,route:a,parent:async()=>{let t={};for(let r=0;r{let a=history.state;Pt=!0,location.replace(new URL(`#${n}`,location.href)),history.replaceState(a,``,e),t&&scrollTo(r,i),Pt=!1})}else{let e=document.body,t=e.getAttribute(`tabindex`);e.tabIndex=-1,e.focus({preventScroll:!0,focusVisible:!1}),t===null?e.removeAttribute(`tabindex`):e.setAttribute(`tabindex`,t)}let r=getSelection();if(r&&r.type!==`None`){let e=[];for(let t=0;t{if(r.rangeCount===e.length){for(let t=0;t{a=e,o=t});return s.catch(h),{navigation:{from:{params:e.params,route:{id:e.route?.id??null},url:e.url,scroll:E()},to:n&&{params:t?.params??null,route:{id:t?.route?.id??null},url:n,scroll:i},willUnload:!t,type:r,complete:s},fulfil:a,reject:o}}function Lt(e){return{data:e.data,error:e.error,form:e.form,params:e.params,route:e.route,state:e.state,status:e.status,url:e.url}}function Rt(e){let t=new URL(e);return t.hash=decodeURIComponent(e.hash),t}function zt(e){let t;if(V.hash){let[,,n]=e.hash.split(`#`,3);t=n??``}else t=e.hash.slice(1);return decodeURIComponent(t)}export{N as a,j as i,L as n,Te as o,M as r,it as t}; \ No newline at end of file diff --git a/crates/carapace-api/static/_app/immutable/chunks/Cfkx4BKF.js b/crates/carapace-api/static/_app/immutable/chunks/Cfkx4BKF.js deleted file mode 100644 index e1b2d58..0000000 --- a/crates/carapace-api/static/_app/immutable/chunks/Cfkx4BKF.js +++ /dev/null @@ -1 +0,0 @@ -import{G as e,K as t,M as n,N as r,X as i,j as a,r as o,t as s}from"./D7CCBGlz.js";import"./CCeg2KC3.js";var c=class{constructor(e,t){this.status=e,typeof t==`string`?this.body={message:t}:t?this.body=t:this.body={message:`Error: ${e}`}}toString(){return JSON.stringify(this.body)}},l=class{constructor(e,t){try{new Headers({location:t})}catch{throw Error(`Invalid redirect location ${JSON.stringify(t)}: this string contains characters that cannot be used in HTTP headers`)}this.status=e,this.location=t}},u=class extends Error{constructor(e,t,n){super(n),this.status=e,this.text=t}};new URL(`sveltekit-internal://`);function d(e,t){return e===`/`||t===`ignore`?e:t===`never`?e.endsWith(`/`)?e.slice(0,-1):e:t===`always`&&!e.endsWith(`/`)?e+`/`:e}function f(e){return e.split(`%25`).map(decodeURI).join(`%25`)}function p(e){for(let t in e)e[t]=decodeURIComponent(e[t]);return e}function m({href:e}){return e.split(`#`)[0]}function h(){}function g(...e){let t=5381;for(let n of e)if(typeof n==`string`){let e=n.length;for(;e;)t=t*33^n.charCodeAt(--e)}else if(ArrayBuffer.isView(n)){let e=new Uint8Array(n.buffer,n.byteOffset,n.byteLength),r=e.length;for(;r;)t=t*33^e[--r]}else throw TypeError(`value must be a string or TypedArray`);return(t>>>0).toString(36)}new TextEncoder;function _(e){let t=atob(e),n=new Uint8Array(t.length);for(let e=0;e((e instanceof Request?e.method:t?.method||`GET`)!==`GET`&&y.delete(x(e)),v(e,t));var y=new Map;function ee(e,t){let n=x(e,t),r=document.querySelector(n);if(r?.textContent){r.remove();let{body:e,...t}=JSON.parse(r.textContent);r.getAttribute(`data-b64`)!==null&&(e=_(e));let i=r.getAttribute(`data-ttl`);return i&&y.set(n,{body:e,init:t,ttl:1e3*Number(i)}),Promise.resolve(new Response(e,t))}return window.fetch(e,t)}function b(e,t,n){if(y.size>0){let t=x(e,n),r=y.get(t);if(r){if(performance.now(){let n=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(e);if(n)return t.push({name:n[1],matcher:n[2],optional:!1,rest:!0,chained:!0}),`(?:/([^]*))?`;let r=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(e);if(r)return t.push({name:r[1],matcher:r[2],optional:!0,rest:!1,chained:!0}),`(?:/([^/]+))?`;if(!e)return;let i=e.split(/\[(.+?)\](?!\])/);return`/`+i.map((e,n)=>{if(n%2){if(e.startsWith(`x+`))return se(String.fromCharCode(parseInt(e.slice(2),16)));if(e.startsWith(`u+`))return se(String.fromCharCode(...e.slice(2).split(`-`).map(e=>parseInt(e,16))));let[,r,a,o,s]=te.exec(e);return t.push({name:o,matcher:s,optional:!!r,rest:!!a,chained:a?n===1&&i[0]===``:!1}),a?`([^]*?)`:r?`([^/]*)?`:`([^/]+?)`}return se(e)}).join(``)}).join(``)}/?$`),params:t}}function ie(e){return e!==``&&!/^\([^)]+\)$/.test(e)}function ae(e){return e.slice(1).split(`/`).filter(ie)}function oe(e,t,n){let r={},i=e.slice(1),a=i.filter(e=>e!==void 0),o=0;for(let e=0;ee).join(`/`),o=0),c===void 0)if(s.rest)c=``;else continue;if(!s.matcher||n[s.matcher](c)){r[s.name]=c;let n=t[e+1],l=i[e+1];n&&!n.rest&&n.optional&&l&&s.chained&&(o=0),!n&&!l&&Object.keys(r).length===a.length&&(o=0);continue}if(s.optional&&s.chained){o++;continue}return}if(!o)return r}function se(e){return e.normalize().replace(/[[\]]/g,`\\$&`).replace(/%/g,`%25`).replace(/\//g,`%2[Ff]`).replace(/\?/g,`%3[Ff]`).replace(/#/g,`%23`).replace(/[.*+?^${}()|\\]/g,`\\$&`)}function ce({nodes:e,server_loads:t,dictionary:n,matchers:r}){let i=new Set(t);return Object.entries(n).map(([t,[n,i,s]])=>{let{pattern:c,params:l}=re(t),u={id:t,exec:e=>{let t=c.exec(e);if(t)return oe(t,l,r)},errors:[1,...s||[]].map(t=>e[t]),layouts:[0,...i||[]].map(o),leaf:a(n)};return u.errors.length=u.layouts.length=Math.max(u.errors.length,u.layouts.length),u});function a(t){let n=t<0;return n&&(t=~t),[n,e[t]]}function o(t){return t===void 0?t:[i.has(t),e[t]]}}function le(e,t=JSON.parse){try{return t(sessionStorage[e])}catch{}}function ue(e,t,n=JSON.stringify){let r=n(t);try{sessionStorage[e]=r}catch{}}var S=globalThis.__sveltekit_18z83oy?.base??``,de=globalThis.__sveltekit_18z83oy?.assets??S??``,fe=`1784071871063`,pe=`sveltekit:snapshot`,me=`sveltekit:scroll`,he=`sveltekit:states`,C=`sveltekit:history`,w=`sveltekit:navigation`,T={tap:1,hover:2,viewport:3,eager:4,off:-1,false:-1},ge=location.origin;function _e(e){if(e instanceof URL)return e;let t=document.baseURI;if(!t){let e=document.getElementsByTagName(`base`);t=e.length?e[0].href:document.URL}return new URL(e,t)}function E(){return{x:pageXOffset,y:pageYOffset}}function D(e,t){return e.getAttribute(`data-sveltekit-${t}`)}var ve={...T,"":T.hover};function ye(e){let t=e.assignedSlot??e.parentNode;return t?.nodeType===11&&(t=t.host),t}function be(e,t){for(;e&&e!==t;){if(e.nodeName.toUpperCase()===`A`&&e.hasAttribute(`href`))return e;e=ye(e)}}function xe(e,t,n){let r;try{if(r=new URL(e instanceof SVGAElement?e.href.baseVal:e.href,document.baseURI),n&&r.hash.match(/^#[^/]/)){let e=location.hash.split(`#`)[1]||`/`;r.hash=`#${e}${r.hash}`}}catch{}let i=e instanceof SVGAElement?e.target.baseVal:e.target,a=!r||!!i||k(r,t,n)||(e.getAttribute(`rel`)||``).split(/\s+/).includes(`external`),o=r?.origin===ge&&e.hasAttribute(`download`);return{url:r,external:a,target:i,download:o}}function O(e){let t=null,n=null,r=null,i=null,a=null,o=null,s=e;for(;s&&s!==document.documentElement;)r===null&&(r=D(s,`preload-code`)),i===null&&(i=D(s,`preload-data`)),t===null&&(t=D(s,`keepfocus`)),n===null&&(n=D(s,`noscroll`)),a===null&&(a=D(s,`reload`)),o===null&&(o=D(s,`replacestate`)),s=ye(s);function c(e){switch(e){case``:case`true`:return!0;case`off`:case`false`:return!1;default:return}}return{preload_code:ve[r??`off`],preload_data:ve[i??`off`],keepfocus:c(t),noscroll:c(n),reload:c(a),replace_state:c(o)}}function Se(e){let t=i(e),n=!0;function r(){n=!0,t.update(e=>e)}function a(e){n=!1,t.set(e)}function o(e){let r;return t.subscribe(t=>{(r===void 0||n&&t!==r)&&e(r=t)})}return{notify:r,set:a,subscribe:o}}var Ce={v:h};function we(){let{set:e,subscribe:t}=i(!1);async function n(){clearTimeout(void 0);try{let t=await fetch(`${de}/_app/version.json`,{headers:{pragma:`no-cache`,"cache-control":`no-cache`}});if(!t.ok)return!1;let n=(await t.json()).version!==fe;return n&&(e(!0),Ce.v(),clearTimeout(void 0)),n}catch{return!1}}return{subscribe:t,check:n}}function k(e,t,n){return e.origin!==ge||!e.pathname.startsWith(t)?!0:n?e.pathname!==location.pathname:!1}function Te(e){}var Ee=new Set([`load`,`prerender`,`csr`,`ssr`,`trailingSlash`,`config`]);[...Ee],[...new Set([...Ee])];function De(e){return e.filter(e=>e!=null)}function A(e,t){return e+`/`+t}function Oe(e){return e instanceof c||e instanceof u?e.status:500}function ke(e){return e instanceof u?e.text:`Internal Error`}var j,M,N,Ae=o.toString().includes(`$$`)||/function \w+\(\) \{\}/.test(o.toString()),je=`a:`;Ae?(j={data:{},form:null,error:null,params:{},route:{id:null},state:{},status:-1,url:new URL(je)},M={current:null},N={current:!1}):(j=new class{#e=t({});get data(){return a(this.#e)}set data(t){e(this.#e,t)}#t=t(null);get form(){return a(this.#t)}set form(t){e(this.#t,t)}#n=t(null);get error(){return a(this.#n)}set error(t){e(this.#n,t)}#r=t({});get params(){return a(this.#r)}set params(t){e(this.#r,t)}#i=t({id:null});get route(){return a(this.#i)}set route(t){e(this.#i,t)}#a=t({});get state(){return a(this.#a)}set state(t){e(this.#a,t)}#o=t(-1);get status(){return a(this.#o)}set status(t){e(this.#o,t)}#s=t(new URL(je));get url(){return a(this.#s)}set url(t){e(this.#s,t)}},M=new class{#e=t(null);get current(){return a(this.#e)}set current(t){e(this.#e,t)}},N=new class{#e=t(!1);get current(){return a(this.#e)}set current(t){e(this.#e,t)}},Ce.v=()=>N.current=!0);function Me(e){Object.assign(j,e)}var{onMount:Ne,tick:Pe}=s,Fe=new Set([`icon`,`shortcut icon`,`apple-touch-icon`]),P=null,F=le(`sveltekit:scroll`)??{},I=le(`sveltekit:snapshot`)??{},L={url:Se({}),page:Se({}),navigating:i(null),updated:we()};function Ie(e){F[e]=E()}function Le(e,t){let n=e+1;for(;F[n];)delete F[n],n+=1;for(n=t+1;I[n];)delete I[n],n+=1}function R(e,t=!1){return t?location.replace(e.href):location.href=e.href,new Promise(h)}async function Re(){if(`serviceWorker`in navigator){let e=await navigator.serviceWorker.getRegistration(S||`/`);e&&await e.update()}}var ze,Be,z,B,Ve,V,He={},Ue={},H=[],We=[],U=null;function Ge(){U?.fork?.then(e=>e?.discard()),U=null,Q={element:void 0,href:void 0}}var Ke=new Map,qe=new Set,Je=new Set,W=new Set,G={branch:[],error:null,url:null,nav:null},Ye=!1,Xe=!1,Ze=!0,K=!1,q=!1,Qe=!1,$e=!1,et,J,Y,X,tt=new Set,nt=new Map,rt=new Map;async function it(e,t,n){if(globalThis.__sveltekit_18z83oy.data){let{q:e={},p:t={},l:n={},f:r={}}=globalThis.__sveltekit_18z83oy.data;for(let t in e)He[t]=e[t];for(let e in n)He[e]=n[e];for(let e in r)He[e]=r[e];for(let e in t)Ue[e]=t[e]}document.URL!==location.href&&(location.href=location.href),V=e,await e.hooks.init?.(),ze=ce(e),B=document.documentElement,Ve=t,Be=e.nodes[0],z=e.nodes[1],Be(),z(),J=history.state?.[C],Y=history.state?.[w],J||(J=Y=Date.now(),history.replaceState({...history.state,[C]:J,[w]:Y},``));let r=F[J];function i(){r&&(history.scrollRestoration=`manual`,scrollTo(r.x,r.y))}n?(i(),await Mt(Ve,n)):(await Z({type:`enter`,url:_e(V.hash?Rt(new URL(location.href)):location.href),replace_state:!0}),i()),jt()}function at(){H.length=0,$e=!1}function ot(e){We.some(e=>e?.snapshot)&&(I[e]=We.map(e=>e?.snapshot?.capture()))}function st(e){I[e]?.forEach((e,t)=>{We[t]?.snapshot?.restore(e)})}function ct(){Ie(J),ue(me,F),ot(Y),ue(pe,I)}async function lt(e,t,n,i){let a,o;t.invalidateAll&&Ge(),await Z({type:`goto`,url:_e(e),keepfocus:t.keepFocus,noscroll:t.noScroll,replace_state:t.replaceState,state:t.state,redirect_count:n,nav_token:i,accept:()=>{if(t.invalidateAll){$e=!0,a=new Set;for(let[e,t]of nt)for(let[n,r]of t)r.resource?.reset(),a.add(A(e,n));o=new Set;for(let[e,t]of rt)for(let n of t.keys())o.add(A(e,n))}t.invalidate&&t.invalidate.forEach(At)}}),t.invalidateAll&&r().then(r).then(()=>{for(let[e,t]of nt)for(let[n,{resource:r}]of t)a?.has(A(e,n))&&r.start();for(let[e,t]of rt)for(let[n,{resource:r}]of t)o?.has(A(e,n))&&r.reconnect()})}async function ut(e){if(e.id!==U?.id){Ge();let t={};tt.add(t),U={id:e.id,token:t,promise:bt({...e,preload:t}).then(e=>(tt.delete(t),e.type===`loaded`&&e.state.error&&Ge(),e)),fork:null}}return U.promise}async function dt(e){let t=(await wt(e,!1))?.route;t&&await Promise.all([...t.layouts,t.leaf].filter(Boolean).map(e=>e[1]()))}async function ft(e,t,n){let r={params:G.params,route:{id:G.route?.id??null},url:new URL(location.href)};if(G={...e.state,nav:r},Me(e.props.page),et=new V.root({target:t,props:{...e.props,stores:L,components:We},hydrate:n,sync:!1,transformError:void 0}),await Promise.resolve(),n){let e={from:null,to:{...r,scroll:F[J]??E()},willUnload:!1,type:`enter`,complete:Promise.resolve()};W.forEach(t=>t(e))}st(Y),Xe=!0}async function pt({url:e,params:t,branch:n,errors:r,status:i,error:a,route:o,form:s}){let c=`never`;if(S&&(e.pathname===S||e.pathname===S+`/`))c=`always`;else for(let e of n)e?.slash!==void 0&&(c=e.slash);e.pathname=d(e.pathname,c),e.search=e.search;let l={type:`loaded`,state:{url:e,params:t,branch:n,error:a,route:o},props:{constructors:De(n).map(e=>e.node.component),page:Lt(j)}};s!==void 0&&(l.props.form=s);let u={},f=!j,p=0;for(let e=0;et(new URL(e))))return!0;return!1}function _t(e,t){return e?.type===`data`?e:e?.type===`skip`?t??null:null}function vt(e,t){if(!e)return new Set(t.searchParams.keys());let n=new Set([...e.searchParams.keys(),...t.searchParams.keys()]);for(let r of n){let i=e.searchParams.getAll(r),a=t.searchParams.getAll(r);i.every(e=>a.includes(e))&&a.every(e=>i.includes(e))&&n.delete(r)}return n}function yt({error:e,url:t,route:n,params:r}){return{type:`loaded`,state:{error:e,url:t,route:n,params:r,branch:[]},props:{page:Lt(j),constructors:[]}}}async function bt({id:e,invalidating:t,url:n,params:r,route:i,preload:a}){if(U?.id===e)return tt.delete(U.token),U.promise;let{errors:o,layouts:s,leaf:u}=i,d=[...s,u];o.forEach(e=>e?.().catch(h)),d.forEach(e=>e?.[1]().catch(h));let f=G.url?e!==Et(G.url):!1,p=G.route?i.id!==G.route.id:!1,m=vt(G.url,n),g=!1,_=d.map(async(e,t)=>{if(!e)return;let a=G.branch[t];return e[1]===a?.loader&&!gt(g,p,f,m,a.universal?.uses,r)?a:(g=!0,mt({loader:e[1],url:n,params:r,route:i,parent:async()=>{let e={};for(let n=0;nPromise.resolve({}),server_data_node:_t(null)}),{node:await z(),loader:z,universal:null,server:null,data:null}],status:e,error:t,errors:[],route:null})}catch(t){if(t instanceof l){await lt(new URL(t.location,location.href),{},0);return}let a=await V.get_error_template(),o=await $(t,{url:n,params:i,route:r}),s=a({status:e,message:String(o?.message??``).replace(/&/g,`&`).replace(//g,`>`)}),c=new DOMParser().parseFromString(s,`text/html`);throw document.documentElement.replaceChild(document.adoptNode(c.head),document.head),document.documentElement.replaceChild(document.adoptNode(c.body),document.body),t}}async function Ct(e){let t=e.href;if(Ke.has(t))return Ke.get(t);let n;try{let r=(async()=>{let t=await V.hooks.reroute({url:new URL(e),fetch:async(t,n)=>ht(t,n,e).promise})??e;if(typeof t==`string`){let n=new URL(e);V.hash?n.hash=t:n.pathname=t,t=n}return t})();Ke.set(t,r),n=await r}catch{Ke.delete(t);return}return n}async function wt(e,t){if(e&&!k(e,S,V.hash)){let n=await Ct(e);if(!n)return;let r=Tt(n);for(let n of ze){let i=n.exec(r);if(i)return{id:Et(e),invalidating:t,route:n,params:p(i),url:e}}}}function Tt(e){return f(V.hash?e.hash.replace(/^#/,``).replace(/[?#].+/,``):e.pathname.slice(S.length))||`/`}function Et(e){return(V.hash?e.hash.replace(/^#/,``):e.pathname)+e.search}function Dt({url:e,type:t,intent:n,delta:r,event:i,scroll:a}){let o=!1,s=It(G,n,e,t,a??null);r!==void 0&&(s.navigation.delta=r),i!==void 0&&(s.navigation.event=i);let c={...s.navigation,cancel:()=>{o=!0,s.reject(Error(`navigation cancelled`))}};return K||qe.forEach(e=>e(c)),o?null:s}async function Z({type:e,url:t,popped:i,keepfocus:a,noscroll:o,replace_state:s,state:c={},redirect_count:l=0,nav_token:d={},accept:f=h,block:p=h,event:m}){let g=X;X=d;let _=await wt(t,!1),v=e===`enter`?It(G,_,t,e):Dt({url:t,type:e,delta:i?.delta,intent:_,scroll:i?.scroll,event:m});if(!v){p(),X===d&&(X=g);return}let y=J,ee=Y;f(),K=!0,Xe&&v.navigation.type!==`enter`&&L.navigating.set(M.current=v.navigation);let b=_&&await bt(_);if(!b){if(k(t,S,V.hash))return await R(t,s);b=await Ot(t,{id:null},await $(new u(404,`Not Found`,`Not found: ${t.pathname}`),{url:t,params:{},route:{id:null}}),404,s)}if(t=_?.url||t,X!==d){v.reject(Error(`navigation aborted`));return}if(!b)return;if(b.type===`redirect`){if(l<20){await Z({type:e,url:new URL(b.location,t),popped:i,keepfocus:a,noscroll:o,replace_state:s,state:c,redirect_count:l+1,nav_token:d}),v.fulfil(void 0);return}if(b=await St({status:500,error:await $(Error(`Redirect loop`),{url:t,params:{},route:{id:null}}),url:t,route:{id:null}}),!b)return}else if(b.props.page.status>=400&&await L.updated.check())return await Re(),await R(t,s);if(at(),Ie(y),ot(ee),b.props.page.url.pathname!==t.pathname&&(t.pathname=b.props.page.url.pathname),c=i?i.state:c,!i){let e=+!s,n={[C]:J+=e,[w]:Y+=e,[he]:c};(s?history.replaceState:history.pushState).call(history,n,``,t),s||Le(J,Y)}let x=_&&U?.id===_.id?U.fork:null;U?.fork&&!x?Ge():(U=null,Q={element:void 0,href:void 0}),b.props.page.state=c;let te;if(Xe){let e=(await Promise.all(Array.from(Je,e=>e(v.navigation)))).filter(e=>typeof e==`function`);if(e.length>0){function t(){e.forEach(e=>{W.delete(e)})}e.push(t),e.forEach(e=>{W.add(e)})}let r=v.navigation.to;G={...b.state,nav:{params:r.params,route:r.route,url:r.url}},b.props.page&&(b.props.page.url=t),!a&&document.activeElement instanceof HTMLElement&&document.activeElement!==document.body&&document.activeElement.blur();let i=x&&await x;i?te=i.commit():(P=null,et.$set(b.props),P&&Object.assign(b.props.page,P),Me(b.props.page),te=n?.()),Qe=!0}else await ft(b,Ve,!1);let{activeElement:ne}=document;if(await te,await r(),await r(),X!==d){v.reject(Error(`navigation aborted`));return}b.props.page&&P&&Object.assign(b.props.page,P);let re=null;if(Ze){let e=i?i.scroll:o?E():null;e?scrollTo(e.x,e.y):(re=t.hash&&document.getElementById(zt(t)))?re.scrollIntoView():scrollTo(0,0)}let ie=document.activeElement!==ne&&document.activeElement!==document.body;!a&&!ie&&Ft(t,!re),Ze=!0,K=!1,v.fulfil(void 0),v.navigation.to&&(v.navigation.to.scroll=E()),W.forEach(e=>e(v.navigation)),e===`popstate`&&st(Y),L.navigating.set(M.current=null)}async function Ot(e,t,n,r,i){return e.origin===ge&&e.pathname===location.pathname&&!Ye?await St({status:r,error:n,url:e,route:t}):await R(e,i)}var Q={element:void 0,href:void 0};function kt(){let e,t;B.addEventListener(`mousemove`,t=>{let n=t.target;clearTimeout(e),e=setTimeout(()=>{i(n,T.hover)},20)});function n(e){e.defaultPrevented||i(e.composedPath()[0],T.tap)}B.addEventListener(`mousedown`,n),B.addEventListener(`touchstart`,n,{passive:!0});let r=new IntersectionObserver(e=>{for(let t of e)t.isIntersecting&&(dt(new URL(t.target.href)),r.unobserve(t.target))},{threshold:0});async function i(e,n){let r=be(e,B),i=r===Q.element&&r?.href===Q.href&&n>=t;if(!r||i)return;let{url:a,external:o,download:s}=xe(r,S,V.hash);if(o||s)return;let c=O(r),l=a&&Et(G.url)===Et(a);if(!(c.reload||l))if(n<=c.preload_data){Q={element:r,href:r.href},t=T.tap;let e=await wt(a,!1);if(!e)return;ut(e)}else n<=c.preload_code&&(Q={element:r,href:r.href},t=n,dt(a))}function a(){r.disconnect();for(let e of B.querySelectorAll(`a`)){let{url:t,external:n,download:i}=xe(e,S,V.hash);if(n||i)continue;let a=O(e);a.reload||(a.preload_code===T.viewport&&r.observe(e),a.preload_code===T.eager&&dt(t))}}W.add(a),a()}function $(e,t){if(e instanceof c)return e.body;let n=Oe(e),r=ke(e);return V.hooks.handleError({error:e,event:t,status:n,message:r})??{message:r}}function At(e){if(typeof e==`function`)H.push(e);else{let{href:t}=new URL(e,location.href);H.push(e=>e.href===t)}}function jt(){history.scrollRestoration=`manual`,addEventListener(`beforeunload`,e=>{let t=!1;if(ct(),!K){let e=It(G,void 0,null,`leave`),n={...e.navigation,cancel:()=>{t=!0,e.reject(Error(`navigation cancelled`))}};qe.forEach(e=>e(n))}t?(e.preventDefault(),e.returnValue=``):history.scrollRestoration=`auto`}),addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&ct()}),navigator.connection?.saveData||kt(),B.addEventListener(`click`,async t=>{if(t.button||t.which!==1||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey||t.defaultPrevented)return;let n=be(t.composedPath()[0],B);if(!n)return;let{url:r,external:i,target:a,download:o}=xe(n,S,V.hash);if(!r)return;if(a===`_parent`||a===`_top`){if(window.parent!==window)return}else if(a&&a!==`_self`)return;let s=O(n);if(!(n instanceof SVGAElement)&&r.protocol!==location.protocol&&!(r.protocol===`https:`||r.protocol===`http:`)||o)return;let[c,l]=(V.hash?r.hash.replace(/^#/,``):r.href).split(`#`),u=c===m(location);if(i||s.reload&&(!u||!l)){Dt({url:r,type:`link`,event:t})?K=!0:t.preventDefault();return}if(l!==void 0&&u){let[,i]=G.url.href.split(`#`);if(i===l){if(t.preventDefault(),l===``||l===`top`&&n.ownerDocument.getElementById(`top`)===null)scrollTo({top:0});else{let e=n.ownerDocument.getElementById(decodeURIComponent(l));e&&(e.scrollIntoView(),e.focus())}return}if(q=!0,Ie(J),e(r),!s.replace_state)return;q=!1}t.preventDefault(),await new Promise(e=>{requestAnimationFrame(()=>{setTimeout(e,0)}),setTimeout(e,100)}),await Z({type:`link`,url:r,keepfocus:s.keepfocus,noscroll:s.noscroll,replace_state:s.replace_state??r.href===location.href,event:t})}),B.addEventListener(`submit`,e=>{if(e.defaultPrevented)return;let t=HTMLFormElement.prototype.cloneNode.call(e.target),n=e.submitter;if((n?.formTarget||t.target)===`_blank`||(n?.formMethod||t.method)!==`get`)return;let r=new URL(n?.hasAttribute(`formaction`)&&n?.formAction||t.action);if(k(r,S,!1))return;let i=e.target,a=O(i);if(a.reload)return;e.preventDefault(),e.stopPropagation();let o=new FormData(i,n);r.search=new URLSearchParams(o).toString(),Z({type:`form`,url:r,keepfocus:a.keepfocus,noscroll:a.noscroll,replace_state:a.replace_state??r.href===location.href,event:e})}),addEventListener(`popstate`,async t=>{if(!Pt)if(t.state?.[`sveltekit:history`]){let n=t.state[C];if(X={},n===J)return;let r=F[n],i=t.state[`sveltekit:states`]??{},a=new URL(t.state[`sveltekit:pageurl`]??location.href),o=t.state[w],s=G.url?m(location)===m(G.url):!1;if(o===Y&&(Qe||s)){i!==j.state&&(j.state=i),e(a),F[J]=E(),r&&scrollTo(r.x,r.y),J=n;return}let c=n-J;await Z({type:`popstate`,url:a,popped:{state:i,scroll:r,delta:c},accept:()=>{J=n,Y=o},block:()=>{history.go(-c)},nav_token:X,event:t})}else q||(e(new URL(location.href)),V.hash&&location.reload())}),addEventListener(`hashchange`,()=>{q&&(q=!1,history.replaceState({...history.state,[C]:++J,[w]:Y},``,location.href))});for(let e of document.querySelectorAll(`link`))Fe.has(e.rel)&&(e.href=e.href);addEventListener(`pageshow`,e=>{e.persisted&&L.navigating.set(M.current=null)});function e(e){G.url=j.url=e,L.page.set(Lt(j)),L.page.notify()}}async function Mt(e,{status:t=200,error:n,node_ids:r,params:i,route:a,server_route:o,data:s,form:c}){Ye=!0;let u=new URL(location.href),d;({params:i={},route:a={id:null}}=await wt(u,!1)||{}),d=ze.find(({id:e})=>e===a.id);let f,p=!0;try{let e=r.map(async(t,n)=>{let r=s[n];return r?.uses&&(r.uses=Nt(r.uses)),mt({loader:V.nodes[t],url:u,params:i,route:a,parent:async()=>{let t={};for(let r=0;r{let a=history.state;Pt=!0,location.replace(new URL(`#${n}`,location.href)),history.replaceState(a,``,e),t&&scrollTo(r,i),Pt=!1})}else{let e=document.body,t=e.getAttribute(`tabindex`);e.tabIndex=-1,e.focus({preventScroll:!0,focusVisible:!1}),t===null?e.removeAttribute(`tabindex`):e.setAttribute(`tabindex`,t)}let r=getSelection();if(r&&r.type!==`None`){let e=[];for(let t=0;t{if(r.rangeCount===e.length){for(let t=0;t{a=e,o=t});return s.catch(h),{navigation:{from:{params:e.params,route:{id:e.route?.id??null},url:e.url,scroll:E()},to:n&&{params:t?.params??null,route:{id:t?.route?.id??null},url:n,scroll:i},willUnload:!t,type:r,complete:s},fulfil:a,reject:o}}function Lt(e){return{data:e.data,error:e.error,form:e.form,params:e.params,route:e.route,state:e.state,status:e.status,url:e.url}}function Rt(e){let t=new URL(e);return t.hash=decodeURIComponent(e.hash),t}function zt(e){let t;if(V.hash){let[,,n]=e.hash.split(`#`,3);t=n??``}else t=e.hash.slice(1);return decodeURIComponent(t)}export{N as a,j as i,L as n,Te as o,M as r,it as t}; \ No newline at end of file diff --git a/crates/carapace-api/static/_app/immutable/entry/app.BJNxxL8q.js b/crates/carapace-api/static/_app/immutable/entry/app.6O9TqAkM.js similarity index 87% rename from crates/carapace-api/static/_app/immutable/entry/app.BJNxxL8q.js rename to crates/carapace-api/static/_app/immutable/entry/app.6O9TqAkM.js index 144b1b8..6b4636a 100644 --- a/crates/carapace-api/static/_app/immutable/entry/app.BJNxxL8q.js +++ b/crates/carapace-api/static/_app/immutable/entry/app.6O9TqAkM.js @@ -1,2 +1,2 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.DSqSDKgT.js","../chunks/D7CCBGlz.js","../chunks/xihTtKlq.js","../assets/0.BIBOqY7u.css","../nodes/1.X1iBfBii.js","../chunks/Cfkx4BKF.js","../chunks/CCeg2KC3.js","../nodes/2.CaWSwQlO.js","../assets/2.Ccp3aMT0.css"])))=>i.map(i=>d[i]); -import{$ as e,C as t,D as n,E as r,G as i,H as a,I as o,K as s,L as c,N as l,Q as u,R as d,S as f,T as p,U as m,V as h,a as g,i as _,j as v,nt as y,o as b,q as x,r as S,v as C,w}from"../chunks/D7CCBGlz.js";import"../chunks/xihTtKlq.js";var T=`modulepreload`,E=function(e,t){return new URL(e,t).href},D={},O=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=E(t,n),t=s(t),t in D)return;D[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:T,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},k={},A=r(`
`),j=r(` `,1);function M(r,_){e(_,!0);let T=g(_,`components`,23,()=>[]),E=g(_,`data_0`,3,null),D=g(_,`data_1`,3,null);d(()=>_.stores.page.set(_.page)),c(()=>{_.stores,_.page,_.constructors,T(),_.form,E(),D(),_.stores.page.notify()});let O=s(!1),k=s(!1),M=s(null);S(()=>{let e=_.stores.page.subscribe(()=>{v(O)&&(i(k,!0),l().then(()=>{i(M,document.title||`untitled page`,!0)}))});return i(O,!0),e});let N=x(()=>_.constructors[1]);var P=j(),F=a(P),I=e=>{let t=x(()=>_.constructors[0]);var n=p();C(a(n),()=>v(t),(e,t)=>{b(t(e,{get data(){return E()},get form(){return _.form},get params(){return _.page.params},children:(e,t)=>{var n=p();C(a(n),()=>v(N),(e,t)=>{b(t(e,{get data(){return D()},get form(){return _.form},get params(){return _.page.params}}),e=>T()[1]=e,()=>T()?.[1])}),w(e,n)},$$slots:{default:!0}}),e=>T()[0]=e,()=>T()?.[0])}),w(e,n)},L=e=>{let t=x(()=>_.constructors[0]);var n=p();C(a(n),()=>v(t),(e,t)=>{b(t(e,{get data(){return E()},get form(){return _.form},get params(){return _.page.params}}),e=>T()[0]=e,()=>T()?.[0])}),w(e,n)};f(F,e=>{_.constructors[1]?e(I):e(L,-1)});var R=m(F,2),z=e=>{var r=A(),i=h(r),a=e=>{var r=n();o(()=>t(r,v(M))),w(e,r)};f(i,e=>{v(k)&&e(a)}),y(r),w(e,r)};f(R,e=>{v(O)&&e(z)}),w(r,P),u()}var N=_(M),P=[()=>O(()=>import(`../nodes/0.DSqSDKgT.js`),__vite__mapDeps([0,1,2,3]),import.meta.url),()=>O(()=>import(`../nodes/1.X1iBfBii.js`),__vite__mapDeps([4,1,5,6,2]),import.meta.url),()=>O(()=>import(`../nodes/2.CaWSwQlO.js`),__vite__mapDeps([7,1,6,2,8]),import.meta.url)],F=[],I={"/":[2]},L={handleError:(({error:e})=>{console.error(e)}),reroute:(()=>{}),transport:{}},R=Object.fromEntries(Object.entries(L.transport).map(([e,t])=>[e,t.decode])),z=Object.fromEntries(Object.entries(L.transport).map(([e,t])=>[e,t.encode])),B=!1,V=(e,t)=>R[e](t),H=()=>O(()=>import(`../chunks/Bjy-W4x2.js`).then(e=>e.default),[],import.meta.url);export{V as decode,R as decoders,I as dictionary,z as encoders,H as get_error_template,B as hash,L as hooks,k as matchers,P as nodes,N as root,F as server_loads}; \ No newline at end of file +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.CCSq_TiJ.js","../chunks/D7CCBGlz.js","../chunks/xihTtKlq.js","../assets/0.BNChGHTQ.css","../nodes/1.DTHmk4CM.js","../chunks/BGevx8A_.js","../chunks/CCeg2KC3.js","../nodes/2.BZ_ZwSYn.js","../assets/2.BO-zofLV.css"])))=>i.map(i=>d[i]); +import{$ as e,C as t,D as n,E as r,G as i,H as a,I as o,K as s,L as c,N as l,Q as u,R as d,S as f,T as p,U as m,V as h,a as g,i as _,j as v,nt as y,o as b,q as x,r as S,v as C,w}from"../chunks/D7CCBGlz.js";import"../chunks/xihTtKlq.js";var T=`modulepreload`,E=function(e,t){return new URL(e,t).href},D={},O=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=E(t,n),t=s(t),t in D)return;D[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:T,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},k={},A=r(`
`),j=r(` `,1);function M(r,_){e(_,!0);let T=g(_,`components`,23,()=>[]),E=g(_,`data_0`,3,null),D=g(_,`data_1`,3,null);d(()=>_.stores.page.set(_.page)),c(()=>{_.stores,_.page,_.constructors,T(),_.form,E(),D(),_.stores.page.notify()});let O=s(!1),k=s(!1),M=s(null);S(()=>{let e=_.stores.page.subscribe(()=>{v(O)&&(i(k,!0),l().then(()=>{i(M,document.title||`untitled page`,!0)}))});return i(O,!0),e});let N=x(()=>_.constructors[1]);var P=j(),F=a(P),I=e=>{let t=x(()=>_.constructors[0]);var n=p();C(a(n),()=>v(t),(e,t)=>{b(t(e,{get data(){return E()},get form(){return _.form},get params(){return _.page.params},children:(e,t)=>{var n=p();C(a(n),()=>v(N),(e,t)=>{b(t(e,{get data(){return D()},get form(){return _.form},get params(){return _.page.params}}),e=>T()[1]=e,()=>T()?.[1])}),w(e,n)},$$slots:{default:!0}}),e=>T()[0]=e,()=>T()?.[0])}),w(e,n)},L=e=>{let t=x(()=>_.constructors[0]);var n=p();C(a(n),()=>v(t),(e,t)=>{b(t(e,{get data(){return E()},get form(){return _.form},get params(){return _.page.params}}),e=>T()[0]=e,()=>T()?.[0])}),w(e,n)};f(F,e=>{_.constructors[1]?e(I):e(L,-1)});var R=m(F,2),z=e=>{var r=A(),i=h(r),a=e=>{var r=n();o(()=>t(r,v(M))),w(e,r)};f(i,e=>{v(k)&&e(a)}),y(r),w(e,r)};f(R,e=>{v(O)&&e(z)}),w(r,P),u()}var N=_(M),P=[()=>O(()=>import(`../nodes/0.CCSq_TiJ.js`),__vite__mapDeps([0,1,2,3]),import.meta.url),()=>O(()=>import(`../nodes/1.DTHmk4CM.js`),__vite__mapDeps([4,1,5,6,2]),import.meta.url),()=>O(()=>import(`../nodes/2.BZ_ZwSYn.js`),__vite__mapDeps([7,1,6,2,8]),import.meta.url)],F=[],I={"/":[2]},L={handleError:(({error:e})=>{console.error(e)}),reroute:(()=>{}),transport:{}},R=Object.fromEntries(Object.entries(L.transport).map(([e,t])=>[e,t.decode])),z=Object.fromEntries(Object.entries(L.transport).map(([e,t])=>[e,t.encode])),B=!1,V=(e,t)=>R[e](t),H=()=>O(()=>import(`../chunks/Bjy-W4x2.js`).then(e=>e.default),[],import.meta.url);export{V as decode,R as decoders,I as dictionary,z as encoders,H as get_error_template,B as hash,L as hooks,k as matchers,P as nodes,N as root,F as server_loads}; \ No newline at end of file diff --git a/crates/carapace-api/static/_app/immutable/entry/start.CbdnOVWP.js b/crates/carapace-api/static/_app/immutable/entry/start.CbdnOVWP.js deleted file mode 100644 index d197d45..0000000 --- a/crates/carapace-api/static/_app/immutable/entry/start.CbdnOVWP.js +++ /dev/null @@ -1 +0,0 @@ -import{o as e,t}from"../chunks/Cfkx4BKF.js";export{e as load_css,t as start}; \ No newline at end of file diff --git a/crates/carapace-api/static/_app/immutable/entry/start.DYdfW4aU.js b/crates/carapace-api/static/_app/immutable/entry/start.DYdfW4aU.js new file mode 100644 index 0000000..88b11c5 --- /dev/null +++ b/crates/carapace-api/static/_app/immutable/entry/start.DYdfW4aU.js @@ -0,0 +1 @@ +import{o as e,t}from"../chunks/BGevx8A_.js";export{e as load_css,t as start}; \ No newline at end of file diff --git a/crates/carapace-api/static/_app/immutable/nodes/0.DSqSDKgT.js b/crates/carapace-api/static/_app/immutable/nodes/0.CCSq_TiJ.js similarity index 100% rename from crates/carapace-api/static/_app/immutable/nodes/0.DSqSDKgT.js rename to crates/carapace-api/static/_app/immutable/nodes/0.CCSq_TiJ.js diff --git a/crates/carapace-api/static/_app/immutable/nodes/1.X1iBfBii.js b/crates/carapace-api/static/_app/immutable/nodes/1.DTHmk4CM.js similarity index 89% rename from crates/carapace-api/static/_app/immutable/nodes/1.X1iBfBii.js rename to crates/carapace-api/static/_app/immutable/nodes/1.DTHmk4CM.js index f81df96..617d8fe 100644 --- a/crates/carapace-api/static/_app/immutable/nodes/1.X1iBfBii.js +++ b/crates/carapace-api/static/_app/immutable/nodes/1.DTHmk4CM.js @@ -1 +1 @@ -import{$ as e,C as t,E as n,H as r,I as i,Q as a,U as o,V as s,nt as c,w as l}from"../chunks/D7CCBGlz.js";import{i as u,n as d}from"../chunks/Cfkx4BKF.js";import"../chunks/xihTtKlq.js";var f={get data(){return u.data},get error(){return u.error},get form(){return u.form},get params(){return u.params},get route(){return u.route},get state(){return u.state},get status(){return u.status},get url(){return u.url}};d.updated.check;var p=f,m=n(`

`,1);function h(n,u){e(u,!0);var d=m(),f=r(d),h=s(f,!0);c(f);var g=o(f,2),_=s(g,!0);c(g),i(()=>{t(h,p.status),t(_,p.error?.message)}),l(n,d),a()}export{h as component}; \ No newline at end of file +import{$ as e,C as t,E as n,H as r,I as i,Q as a,U as o,V as s,nt as c,w as l}from"../chunks/D7CCBGlz.js";import{i as u,n as d}from"../chunks/BGevx8A_.js";import"../chunks/xihTtKlq.js";var f={get data(){return u.data},get error(){return u.error},get form(){return u.form},get params(){return u.params},get route(){return u.route},get state(){return u.state},get status(){return u.status},get url(){return u.url}};d.updated.check;var p=f,m=n(`

`,1);function h(n,u){e(u,!0);var d=m(),f=r(d),h=s(f,!0);c(f);var g=o(f,2),_=s(g,!0);c(g),i(()=>{t(h,p.status),t(_,p.error?.message)}),l(n,d),a()}export{h as component}; \ No newline at end of file diff --git a/crates/carapace-api/static/_app/immutable/nodes/2.BZ_ZwSYn.js b/crates/carapace-api/static/_app/immutable/nodes/2.BZ_ZwSYn.js new file mode 100644 index 0000000..81adb53 --- /dev/null +++ b/crates/carapace-api/static/_app/immutable/nodes/2.BZ_ZwSYn.js @@ -0,0 +1,26 @@ +import{$ as e,A as t,B as n,C as r,D as i,E as a,F as o,G as s,H as c,I as l,J as u,K as d,L as f,O as p,Q as m,S as h,T as g,U as _,V as v,W as y,X as b,Y as ee,_ as x,a as S,b as C,c as w,d as T,f as E,g as D,h as O,j as k,k as A,l as j,m as M,n as N,nt as P,p as te,q as ne,r as F,rt as I,s as L,tt as R,u as z,w as B,x as V,z as re}from"../chunks/D7CCBGlz.js";import"../chunks/CCeg2KC3.js";import"../chunks/xihTtKlq.js";function H(){return typeof window<`u`&&window.__CARAPACE_TOKEN__?window.__CARAPACE_TOKEN__:``}var U=b(null);function W(e){U.set(e)}function G(){U.set(null)}var ie=``,K=class extends Error{status;constructor(e,t){super(e),this.status=t}};async function ae(e,t){let n;try{n=await fetch(ie+e,{...t,headers:{...t?.body?{"Content-Type":`application/json`}:{},Authorization:`Bearer ${H()}`,...t?.headers}})}catch(t){let n=`Could not reach the daemon (${e}). Is it running? (${t.message})`;throw W(n),new K(n)}if(n.status===401||n.status===403){let e=n.status===401?`The daemon rejected this session (bad or missing token). Reload the page.`:`The daemon refused this request (Host/Origin guard). Reload the page.`;throw W(e),new K(e,n.status)}if(!n.ok){let t=n.statusText;try{let e=await n.json();typeof e?.error==`string`&&(t=e.error)}catch{}throw W(`${e} failed: ${t}`),new K(t,n.status)}return await n.json()}async function q(e,t){let n;try{n=await fetch(ie+e,{...t,headers:{Authorization:`Bearer ${H()}`,...t?.headers}})}catch(t){let n=`Could not reach the daemon (${e}). Is it running? (${t.message})`;throw W(n),new K(n)}if(n.status===401||n.status===403){let e=n.status===401?`The daemon rejected this session (bad or missing token). Reload the page.`:`The daemon refused this request (Host/Origin guard). Reload the page.`;throw W(e),new K(e,n.status)}if(!n.ok){let t=n.statusText;try{let e=await n.json();typeof e?.error==`string`&&(t=e.error)}catch{}throw W(`${e} failed: ${t}`),new K(t,n.status)}return n.text()}var J=e=>ae(e),Y=(e,t)=>ae(e,{method:`POST`,body:t===void 0?void 0:JSON.stringify(t)}),X={health:()=>J(`/api/health`),status:()=>J(`/api/status`),syncOwned:(e,t)=>Y(`/api/sync`,{peer:e,out_dir:t}),listVaults:()=>J(`/api/vaults`),publishVault:(e,t)=>Y(`/api/vaults`,{dir:e,vid:t}),listReplicas:e=>J(`/api/vaults/${e}/replicas`),placeReplicas:(e,t,n)=>Y(`/api/vaults/${e}/replicas`,{peers:t,r:n}),discloseFiles:(e,t,n)=>Y(`/api/vaults/${e}/grants`,{paths:t,audience:n}),fetchGrant:(e,t,n)=>Y(`/api/grants/fetch`,{grant_hex:e,owner:t,out_dir:n}),listFriends:()=>J(`/api/friends`),issueTicket:()=>Y(`/api/friends/ticket`),addFriend:(e,t,n)=>Y(`/api/friends`,{ticket_hex:e,addrs:t,grant_bytes:n}),unfriend:e=>Y(`/api/friends/${e}/unfriend`),resplitStatus:e=>J(`/api/recovery/${e}/resplit-status`),ceremonyStatus:()=>J(`/api/recovery/ceremony`),paperCards:e=>q(`/api/recovery/${e}/paper`),resplitStart:(e,t)=>Y(`/api/recovery/${e}/resplit-start`,t?{trustees:t}:void 0),recoverySplit:(e,t,n,r,i)=>Y(`/api/recovery/split`,{rsid:e,scope:t,m:n,n:r,allow_over_cap:i}),recoverySplitToTrustees:(e,t,n,r,i)=>Y(`/api/recovery/split`,{rsid:e,scope:t,m:n,trustees:r,allow_over_cap:i}),recoveryResplit:(e,t,n,r,i)=>Y(`/api/recovery/resplit`,{rsid:e,scope:t,m:n,n:r,allow_over_cap:i}),recoveryResplitToTrustees:(e,t,n,r,i)=>Y(`/api/recovery/resplit`,{rsid:e,scope:t,m:n,trustees:r,allow_over_cap:i}),recoveryExtend:(e,t,n)=>Y(`/api/recovery/extend`,{rsid:e,count:t,allow_over_cap:n}),restartRestore:e=>Y(`/api/recovery/restart-restore`,{out_dir:e}),ceremonyOpen:e=>Y(`/api/recovery/ceremony/open`,e),ceremonyApprove:e=>Y(`/api/recovery/ceremony/approve`,{ceremony_id:e}),ceremonyAbort:e=>Y(`/api/recovery/ceremony/abort`,{ceremony_id:e})},oe=b(null),se=b(!1),Z=null,ce=1e3,Q=null,le=!0;function ue(){if(typeof window>`u`||le)return;let e=`${location.protocol===`https:`?`wss:`:`ws:`}//${location.host}/api/events`;Z=new WebSocket(e),Z.onopen=()=>{se.set(!0),ce=1e3},Z.onmessage=e=>{try{oe.set(JSON.parse(e.data))}catch{}},Z.onclose=()=>{se.set(!1),!le&&(Q=setTimeout(ue,ce),ce=Math.min(ce*2,15e3))},Z.onerror=()=>{Z?.close()}}function de(){fe(),le=!1,X.status().then(e=>oe.set(e)).catch(()=>{}),ue()}function fe(){le=!0,Q!==null&&(clearTimeout(Q),Q=null);let e=Z;Z=null,e?.close(),se.set(!1)}var pe=a(``);function me(e){let t=()=>ee(U,`$lastError`,n),[n,i]=u();var a=g(),o=c(a),s=e=>{var n=pe(),i=v(n),a=v(i,!0);P(i);var o=_(i,2);P(n),l(()=>r(a,t())),A(`click`,o,function(...e){G?.apply(this,e)}),B(e,n)};h(o,e=>{t()&&e(s)}),B(e,a),i()}p([`click`]);var he=a(``),ge=a(`
`),_e=a(`
`);function $(e,t){function n(e){let t=Math.max(e.target,e.achieved,1);return Array.from({length:t},(t,n)=>({filled:nt.plates,e=>e.key,(e,t,i)=>{var a=ge(),o=v(a);C(o,21,()=>n(k(t)),V,(e,t)=>{var n=he();let r;l(()=>r=O(n,1,`segment svelte-v2pnom`,null,r,{filled:k(t).filled})),B(e,n)}),P(o);var s=_(o,2),c=v(s,!0);P(s);var u=_(s,2),d=v(u,!0);P(u);var f=_(u,2),p=v(f,!0);P(f),P(a),l(()=>{O(a,1,`plate-group state-${k(t).state??``}`,`svelte-v2pnom`),M(a,`--i: ${k(i)??``}`),r(c,k(t).label),r(d,k(t).valueLabel),r(p,k(t).note)}),B(e,a)}),P(i),B(e,i)}function ve(e,t=8,n=6){return e.length<=t+n+1?e:`${e.slice(0,t)}…${e.slice(-n)}`}async function ye(e){try{return await navigator.clipboard.writeText(e),!0}catch{return W(`Could not copy to the clipboard. Select and copy the value manually.`),!1}}var be=[`B`,`KiB`,`MiB`,`GiB`,`TiB`];function xe(e){if(!Number.isFinite(e)||e<0)return`—`;if(e===0)return`0 B`;let t=Math.min(Math.floor(Math.log2(e)/10),be.length-1),n=e/2**(10*t);return`${n>=10||t===0?Math.round(n):n.toFixed(1)} ${be[t]}`}var Se=a(``);function Ce(t,n){e(n,!0);let i=S(n,`head`,3,8),a=S(n,`tail`,3,6),o=d(!1);async function c(){await ye(n.value)&&(s(o,!0),setTimeout(()=>s(o,!1),1200))}var u=Se(),f=v(u),p=_(f),h=v(p,!0);P(p),P(u),l(e=>{T(u,`title`,n.value),T(u,`aria-label`,`Copy ${n.value??``}`),r(f,`${e??``} `),r(h,k(o)?`copied`:`copy`)},[()=>ve(n.value,i(),a())]),A(`click`,u,c),B(t,u),m()}p([`click`]);var we=a(`
This node
Friends storing your vaults
Vaults published
`,1),Te=a(`

Waiting for the daemon…

`),Ee=a(`

Shell integrity

`);function De(t,n){e(n,!0);let i=()=>ee(oe,`$status`,a),[a,o]=u(),p=d(null);f(()=>{let e=i()?.vaults.published??[];if(e.length===0){s(p,null);return}Promise.all(e.map(e=>X.listReplicas(e.vid).catch(()=>({members:[]})))).then(e=>{s(p,Math.min(...e.map(e=>e.members.length)),!0)}).catch(()=>{s(p,null)})});let g=ne(()=>{let e=i();if(!e)return[];let t=e.vaults.published.length,n=k(p)??0,r={key:`replicas`,label:`Replicas`,achieved:t===0?0:Math.min(n,3),target:3,valueLabel:t===0?`—`:`${n}/3`,state:t===0?`empty`:n>=3?`healthy`:`at-risk`,note:t===0?`No vaults published yet`:`Weakest vault held by ${n} friend${n===1?``:`s`}`},a=e.share_health.recovery_sets_owned,o=e.share_health.sets.find(e=>e.scope.kind===`root`),s={key:`shares`,label:`Recovery shares`,achieved:+(a>0),target:1,valueLabel:o?`${o.threshold}-of-${o.issued}`:a>0?`split`:`—`,state:a>0?`healthy`:`empty`,note:a>0?`${e.share_health.shares_held} share${e.share_health.shares_held===1?``:`s`} held here in trust for others`:`Your key has no trustees yet - nobody could rebuild it`},c=e.addr.length,l=e.relay_networks,u=e.relay_diversity_warning||l<2;return[r,s,{key:`relays`,label:`Reachability`,achieved:Math.min(l,2),target:2,valueLabel:`${l}`,state:c===0?`empty`:u?`at-risk`:`healthy`,note:u?`Only ${l} relay network${l===1?``:`s`} - add a friend's relay so you can still be reached if one drops`:`${e.reachability} · ${l} relay networks, ${c} dialable address${c===1?``:`es`}`}]});var y=Ee(),b=_(v(y),2),x=e=>{var t=we(),n=c(t);$(n,{get plates(){return k(g)}});var a=_(n,2),o=v(a);Ce(_(v(o),2),{get value(){return i().node_id},head:12,tail:8}),P(o);var s=_(o,2),u=_(v(s),2),d=v(u,!0);P(u),P(s);var f=_(s,2),p=_(v(f),2),m=v(p,!0);P(p),P(f),P(a),R(2),l(()=>{r(d,i().friends.count),r(m,i().vaults.published.length)}),B(e,t)},S=e=>{B(e,Te())};h(b,e=>{i()?e(x):e(S,-1)}),P(y),B(t,y),m(),o()}var Oe=a(``),ke=a(``),Ae=a(`

Sync is in progress.

`),je=a(`

The sync completed. There were no new vault versions to restore.

`),Me=a(`
  • `),Ne=a(`

      `,1),Pe=a(`

      Loading vaults…

      `),Fe=a(`

      No vaults published yet. Publish a directory above to start protecting it.

      `),Ie=a(`
    • `),Le=a(`
        `),Re=a(`no replicas placed - vault exists only here`),ze=a(`

        Replica members
        `),Be=a(`
        `),Ve=a(``),He=a(``),Ue=a(`
        `),We=a(`

        `),Ge=a(`

        Place replicas for

        Peer-dependent operation · safe to retry if a peer is offline

        Select verified friends
        Advanced manual peers
        `),Ke=a(`

        Vaults

        A vault is a directory you've published for friends to hold replicas of. Publishing ingests + and encrypts it locally; placing replicas is what actually copies it out.

        Local operation

        Sync from your other device

        This peer-dependent action pulls authorized vault updates and restores them below the selected directory.

        Advanced manual peer
        `);function qe(n,i){e(i,!0);let a=()=>ee(oe,`$status`,o),[o,p]=u(),b=[],x=d(y([])),S=d(y({})),E=d(!0),D=d(``),O=d(!1),M=d(``),N=d(``),ne=d(``),F=d(``),I=d(!1),L=d(null),R=d(``),re=d(null),H=d(3),U=d(y([{node:``,addrs:``}])),W=d(!1),G=d(null),ie=d(y([]));async function K(){s(E,!0);let e=await X.listVaults().catch(()=>({published:[]}));s(x,e.published,!0);let t=await Promise.all(k(x).map(async e=>[e.vid,(await X.listReplicas(e.vid).catch(()=>({members:[]}))).members]));s(S,Object.fromEntries(t),!0),s(E,!1)}K(),f(()=>{a()?.vaults.published.length,K()});async function ae(){if(k(D).trim()){s(O,!0),s(R,``);try{await X.publishVault(k(D).trim()),s(D,``),await K()}catch(e){s(R,e.message,!0)}finally{s(O,!1)}}}async function q(){let e=a()?.peers.find(e=>e.node===k(ne));if(e&&(s(M,e.node,!0),s(N,e.addrs.join(`,`),!0)),!(!k(M).trim()||!k(F).trim())){s(I,!0),s(L,null),s(R,``);try{let e=k(N).split(`,`).map(e=>e.trim()).filter(Boolean),t=await X.syncOwned({node:k(M).trim(),addrs:e},k(F).trim());s(L,t.restored,!0),await K()}catch(e){s(R,e.message,!0)}finally{s(I,!1)}}}function J(e){s(re,e,!0),s(G,null),s(U,[{node:``,addrs:``}],!0),s(ie,[],!0)}function Y(){s(U,[...k(U),{node:``,addrs:``}],!0)}function se(e){s(U,k(U).filter((t,n)=>n!==e),!0)}async function Z(){if(k(re)){s(W,!0);try{let e=(a()?.peers??[]).filter(e=>k(ie).includes(e.node)),t=k(U).filter(e=>e.node.trim()).map(e=>({node:e.node.trim(),addrs:e.addrs.split(`,`).map(e=>e.trim()).filter(Boolean)})),n=[...e.map(e=>({node:e.node,addrs:e.addrs})),...t],r=await X.placeReplicas(k(re),n,k(H));s(G,r.placed,!0),await K()}catch(e){s(R,e.message,!0)}finally{s(W,!1)}}}var ce=Ke(),Q=_(v(ce),4),le=e=>{var t=Oe(),n=v(t,!0);P(t),l(()=>r(n,k(R))),B(e,t)};h(Q,e=>{k(R)&&e(le)});var ue=_(Q,2),de=_(v(ue),4),fe=v(de);z(fe);var pe=_(fe,2),me=v(pe,!0);P(pe),P(de),P(ue);var he=_(ue,2),ge=_(v(he),6),_e=v(ge);_e.value=_e.__value=``,C(_(_e),1,()=>a()?.peers??[],e=>e.node,(e,t)=>{var n=ke(),i=v(n);P(n);var a={};l(e=>{r(i,`${(k(t).display||`Friend`)??``} · ${e??``}…`),a!==(a=k(t).node)&&(n.value=(n.__value=k(t).node)??``)},[()=>k(t).node.slice(0,10)]),B(e,n)}),P(ge);var $=_(ge,2),ve=_(v($),3);z(ve);var ye=_(ve,3);z(ye),P($);var be=_($,4);z(be);var xe=_(be,2),Se=_(xe,2),we=v(Se),Te=e=>{B(e,Ae())},Ee=e=>{var t=g(),n=c(t),i=e=>{B(e,je())},a=e=>{var t=Ne(),n=c(t),i=v(n);P(n);var a=_(n,2);C(a,21,()=>k(L),e=>e.vid,(e,t)=>{var n=Me(),i=v(n),a=v(i);P(i);var o=_(i),s=_(o),c=v(s,!0);P(s),P(n),l(e=>{r(a,`${e??``}…`),r(o,`, epoch ${k(t).epoch??``}, in `),r(c,k(t).out_dir)},[()=>k(t).vid.slice(0,12)]),B(e,n)}),P(a),l(()=>r(i,`Restored ${k(L).length??``} vault(s).`)),B(e,t)};h(n,e=>{k(L).length===0?e(i):e(a,-1)}),B(e,t)};h(we,e=>{k(I)?e(Te):k(L)&&e(Ee,1)}),P(Se),P(he);var De=_(he,2),qe=e=>{B(e,Pe())},Je=e=>{B(e,Fe())},Ye=e=>{var t=Be();C(t,21,()=>k(x),e=>e.vid,(e,t)=>{var n=ze(),i=v(n),a=v(i),o=v(a,!0);P(a);var s=_(a,2);Ce(s,{get value(){return k(t).vid}});var c=_(s,2),u=v(c);P(c),P(i);var d=_(i,2),f=_(v(d),2),p=e=>{var n=Le();C(n,20,()=>k(S)[k(t).vid],e=>e,(e,t)=>{var n=Ie();Ce(v(n),{get value(){return t}}),P(n),B(e,n)}),P(n),B(e,n)},m=e=>{B(e,Re())};h(f,e=>{k(S)[k(t).vid]?.length?e(p):e(m,-1)}),P(d);var g=_(d,2);P(n),l(()=>{r(o,k(t).name),r(u,`epoch ${k(t).epoch??``}`)}),A(`click`,g,()=>J(k(t).vid)),B(e,n)}),P(t),B(e,t)};h(De,e=>{k(E)?e(qe):k(x).length===0?e(Je,1):e(Ye,-1)});var Xe=_(De,2),Ze=e=>{var t=Ge(),n=v(t),i=_(v(n)),o=v(i);P(i),P(n);var c=_(n,4);C(_(v(c),2),1,()=>a()?.peers??[],e=>e.node,(e,t)=>{var n=Ve(),i=v(n);z(i);var a,o=_(i);P(n),l(e=>{a!==(a=k(t).node)&&(i.value=(i.__value=k(t).node)??``),r(o,` ${(k(t).display||`Friend`)??``} · ${e??``}…`)},[()=>k(t).node.slice(0,10)]),w(b,[],i,()=>(k(t).node,k(ie)),e=>s(ie,e)),B(e,n)}),P(c);var u=_(c,2),d=_(v(u));C(d,17,()=>k(U),V,(e,t,n)=>{var r=Ue(),i=v(r);z(i);var a=_(i,2);z(a);var o=_(a,2),s=e=>{var t=He();A(`click`,t,()=>se(n)),B(e,t)};h(o,e=>{k(U).length>1&&e(s)}),P(r),j(i,()=>k(t).node,e=>k(t).node=e),j(a,()=>k(t).addrs,e=>k(t).addrs=e),B(e,r)});var f=_(d);P(u);var p=_(u,2),m=_(v(p),2);z(m);var g=_(m,2),y=v(g,!0);P(g);var ee=_(g,2);P(p);var x=_(p,2),S=e=>{var t=We(),n=v(t);P(t),l(()=>r(n,`Placed on ${k(G).length??``} peer${k(G).length===1?``:`s`}.`)),B(e,t)};h(x,e=>{k(G)&&e(S)}),P(t),l(e=>{r(o,`${e??``}…`),g.disabled=k(W),r(y,k(W)?`Placing…`:`Place`)},[()=>k(re).slice(0,12)]),A(`click`,f,Y),j(m,()=>k(H),e=>s(H,e)),A(`click`,g,Z),A(`click`,ee,()=>s(re,null)),B(e,t)};h(Xe,e=>{k(re)&&e(Ze)}),P(ce),l((e,t)=>{pe.disabled=e,r(me,k(O)?`Publishing…`:`Publish vault`),T(he,`aria-busy`,k(I)),xe.disabled=t},[()=>k(O)||!k(D).trim(),()=>k(I)||!k(ne)&&!k(M).trim()||!k(F).trim()]),t(`submit`,ue,e=>(e.preventDefault(),ae())),j(fe,()=>k(D),e=>s(D,e)),t(`submit`,he,e=>(e.preventDefault(),q())),te(ge,()=>k(ne),e=>s(ne,e)),j(ve,()=>k(M),e=>s(M,e)),j(ye,()=>k(N),e=>s(N,e)),j(be,()=>k(F),e=>s(F,e)),B(n,ce),m(),p()}p([`click`]);var Je=a(`
        `),Ye=a(`

        `),Xe=a(`

        Re-split required

        Recovery & trustees.

        `,1),Ze=a(`

        `),Qe=a(`
        `),$e=a(`

        Loading…

        `),et=a(`

        No friends yet. Issue an invite ticket to add your first one.

        `),tt=a(`Remove this friend? `),nt=a(``),rt=a(`
        `),it=a(`
        `),at=a(`

        Friends

        Friends hold encrypted replicas of your vaults and, if you make them trustees, pieces of your + recovery key.

        Invite a friend

        Local operation · the ticket becomes peer-dependent when your friend accepts it

        Add a friend from a ticket

        Peer-dependent operation · an unused ticket is safe to retry

        Your friends

        Storage grants above come from durable daemon state. Trustee and relay roles appear in their + own recovery and replica status views.

        `);function ot(n,a){e(a,!0);let o=()=>ee(oe,`$status`,p),[p,g]=u(),b=d(y([])),x=d(!0),S=d(null),w=d(!1),T=d(``),D=d(``),M=d(1),N=d(!1),te=d(null),F=d(!1),I=d(null),L=d(null),V=d(null);function re(e){return o()?.friends.grants.find(t=>t.user===e)?.grant_bytes??null}async function H(){s(x,!0);let e=await X.listFriends().catch(()=>({count:0,list:[]}));s(b,e.list,!0),s(x,!1)}H(),f(()=>{o()?.friends.count,H()});async function U(){s(w,!0);try{let e=await X.issueTicket();s(S,e,!0)}finally{s(w,!1)}}async function W(){k(S)&&await ye(k(S).uri)&&(s(F,!0),setTimeout(()=>s(F,!1),1200))}async function G(){if(k(T).trim()){s(N,!0),s(te,null);try{let e=k(D).split(`,`).map(e=>e.trim()).filter(Boolean),t=Math.round(k(M)*1024**3),n=await X.addFriend(k(T).trim(),e.length?e:void 0,t);s(te,`Friend added (${n.friend.slice(0,12)}…).`),s(T,``),s(D,``),await H()}finally{s(N,!1)}}}async function ie(e){s(L,e,!0);try{let t=await X.unfriend(e);s(I,null),t.was_friend&&s(V,{friend:e,resplit:t.resplit_triggered,rsids:t.recovery_set_ids},!0),await H()}finally{s(L,null)}}var K=at(),ae=_(v(K),4),q=v(ae),J=_(v(q),4),Y=v(J,!0);P(J);var se=_(J,2),Z=e=>{var t=Je(),n=_(v(t),2),i=v(n);z(i);var a=_(i,2),o=v(a,!0);P(a),P(n),P(t),l(()=>{E(i,k(S).uri),r(o,k(F)?`Copied`:`Copy`)}),A(`click`,a,W),B(e,t)};h(se,e=>{k(S)&&e(Z)}),P(q);var ce=_(q,2),Q=_(v(ce),4),le=_(v(Q),2);z(le);var ue=_(le,4);z(ue);var de=_(ue,4);z(de);var fe=_(de,2),pe=v(fe,!0);P(fe),P(Q);var me=_(Q,2),he=e=>{var t=Ye(),n=v(t,!0);P(t),l(()=>r(n,k(te))),B(e,t)};h(me,e=>{k(te)&&e(he)}),P(ce),P(ae);var ge=_(ae,2),_e=e=>{var t=Qe(),n=v(t),i=e=>{var t=Xe(),n=_(c(t),2),i=v(n);R(2),P(n),l((e,t)=>r(i,`${e??``}… was a trustee. A trustee re-split is now running for + recovery set${k(V).rsids.length>1?`s`:``} + ${t??``}. Both the old and new sets stay usable until the new set is + live and the old shares are destroyed - track it under `),[()=>k(V).friend.slice(0,12),()=>k(V).rsids.join(`, `)]),B(e,t)},a=e=>{var t=Ze(),n=v(t);P(t),l(e=>r(n,`${e??``}… removed. They held no recovery shares, so no re-split + was needed.`),[()=>k(V).friend.slice(0,12)]),B(e,t)};h(n,e=>{k(V).resplit?e(i):e(a,-1)});var o=_(n,2);P(t),l(()=>O(t,1,`card ${k(V).resplit?`at-risk`:`healthy`}`)),A(`click`,o,()=>s(V,null)),B(e,t)};h(ge,e=>{k(V)&&e(_e)});var $=_(ge,4),ve=e=>{B(e,$e())},be=e=>{B(e,et())},Se=e=>{var t=it();C(t,20,()=>k(b),e=>e,(e,t)=>{var n=rt(),a=v(n);Ce(a,{get value(){return t}});var o=_(a,2),c=v(o),u=e=>{var n=i();l(e=>r(n,`${e??``} storage grant`),[()=>xe(re(t))]),B(e,n)},d=ne(()=>re(t)!==null),f=e=>{B(e,i(`storage grant unavailable`))};h(c,e=>{k(d)?e(u):e(f,-1)}),P(o);var p=_(o,2),m=e=>{var n=tt(),i=_(v(n),2),a=v(i,!0);P(i);var o=_(i,2);P(n),l(()=>{i.disabled=k(L)===t,r(a,k(L)===t?`Removing…`:`Confirm unfriend`),o.disabled=k(L)===t}),A(`click`,i,()=>ie(t)),A(`click`,o,()=>s(I,null)),B(e,n)},g=e=>{var n=nt();A(`click`,n,()=>s(I,t,!0)),B(e,n)};h(p,e=>{k(I)===t?e(m):e(g,-1)}),P(n),B(e,n)}),P(t),B(e,t)};h($,e=>{k(x)?e(ve):k(b).length===0?e(be,1):e(Se,-1)}),R(2),P(K),l(e=>{J.disabled=k(w),r(Y,k(w)?`Issuing…`:`Create invite ticket`),fe.disabled=e,r(pe,k(N)?`Adding…`:`Add friend`)},[()=>k(N)||!k(T).trim()]),A(`click`,J,U),t(`submit`,Q,e=>(e.preventDefault(),G())),j(le,()=>k(T),e=>s(T,e)),j(ue,()=>k(D),e=>s(D,e)),j(de,()=>k(M),e=>s(M,e)),B(n,K),m(),g()}p([`click`]);var st=a(`
        Live on this node

        `),ct=a(`

        Restore is in progress.

        `),lt=a(`

        `),ut=a(``),dt=a(``),ft=a(``),pt=a(`
        Reason
        Approvals
        Sponsor
        `),mt=a(`

        Active recovery ceremonies

        Review the claimant and reason out of band before you approve a ceremony. Abort any ceremony + against your identity that you did not start.

        `,1),ht=a(`
        `),gt=a(`

        Paper cards (offline backstop)

        Print a paper card for each recovery set (§8, §10.2). A card recovers from its words + alone - offline, with no Carapace software - so it is the backstop that never goes + offline. The card shows a share's secret words; print it, then keep or destroy the copy.

        `,1),_t=a(`
        `),vt=a(`
        re-split required

        was a trustee of this recovery set and + was unfriended. Their retained share must be neutralized by re-splitting to a fresh set.

        Suggested new trustee set - live reachability

        `),yt=a(`

        Re-split required

        An unfriended trustee still held a share of the recovery set below (§9.3.4). Start the + re-split to hand a fresh share to a new trustee set - the old shares are only destroyed + once that new set is live.

        `,1),bt=a(`· live`),xt=a(`· not live yet`),St=a(`· destroy refused until new set is live`),Ct=a(`
        `),wt=a(`

        New set attested (destroy gate: M + slack)

        Old shares destroyed (ack)

        Remaining friends - live reachability
        `),Tt=a(`

        Trustee re-splits in progress

        An unfriended trustee's share is being neutralized (§9.3 step 4). Both the old and new + recovery sets stay usable until the new set is live and the old shares are destroyed - + neither door closes early.

        `,1),Et=a(` `),Dt=a(`
        `),Ot=a(`

        Authoritative recovery sets

        `,1),kt=a(``),At=a(`

        Advanced recovery-set id
        `,1),jt=a(``),Mt=a(` `,1),Nt=a(``),Pt=a(`

        Add friends before you create recovery protection.

        `),Ft=a(``),It=a(`
        `),Lt=a(`
        `),Rt=a(` `,1),zt=a(` `),Bt=a(`
      • `),Vt=a(`

          `,1),Ht=a(`

          All selected trustees received their shares.

          `),Ut=a(`

          Verified share delivery

          `),Wt=a(`

          `),Gt=a(`
          `),Kt=a(``),qt=a(`

          Shares - send one to each trustee

          `),Jt=a(``),Yt=a(`
          `),Xt=a(`

          This device does not hold a recovery grant. It cannot sponsor a ceremony.

          `),Zt=a(``),Qt=a(`

          Verified sponsor ceremony package - hand to the claimant
          `,1),$t=a(`

          `,1),en=a(`

          Send this abort to your trustees.

          `,1),tn=a(`

          Recovery & trustees

          Split your key into pieces so a group of trustees can rebuild it if you lose access. + The normal flow sends a signed recovery grant to each selected friend and tracks delivery. + The advanced manual flow shows bearer shares that you must protect and deliver yourself.

          Restore retained vaults after identity recovery

          Discovery-dependent operation · it is safe to retry after a network failure

          Use the public restart handoff saved by claimant activation. Carapace contacts the verified trustee hints and accepts only the maximum announced epoch for each vault.

          Split or re-split

          Share delivery

          Add a trustee (extend)

          Peer-dependent operation · offline delivery stays queued and is safe to retry

          Recovery ceremony

          A ceremony is opened by a signed request a trustee receives out of band (from the person + recovering, or the daemon that observed them). The recovery delay and required approvals are + enforced by the daemon per the grant that authorized the ceremony; paste the pieces below as + they arrive.

          Open (sponsor)

          Use the recovery request from the claimant. Confirm their identity and new device through + a separate trusted channel before you open the ceremony.

          Advanced manual values

          Approve

          Abort

          `);function nn(n,a){e(a,!0);let o=()=>ee(oe,`$status`,f),[f,p]=u(),g=[],b=[],x=[],S=[],E=d(`split`),M=d(1),N=d(!1),F=d(`root`),I=d(``),H=d(2),U=d(3),W=d(`friends`),G=d(y([])),ie=d(!1),K=d(!1),ae=d(null),q=d(y([])),J=d(null),Y=ne(()=>Math.max(0,...o()?.share_health.sets.map(e=>e.rsid)??[])+1),se=d(1),Z=d(1),ce=d(!1),Q=d(!1),le=d(null),ue=d(``),de=d(``),fe=d(``),pe=d(``),me=d(``),he=d(``),ge=d(``),_e=d(!1),$=d(null);function ve(){s(pe,``);try{let e=JSON.parse(k(fe));if(e.type!==`carapace.claimant-handoff`||e.version!==1||typeof e.ceremony_enc!=`string`||typeof e.new_node!=`string`||!/^[0-9a-f]{64}$/i.test(e.ceremony_enc)||!/^[0-9a-f]{64}$/i.test(e.new_node))throw Error(`Package type, version, or public values are invalid.`);s(me,e.ceremony_enc,!0),s(he,e.new_node,!0)}catch(e){s(pe,e instanceof Error?e.message:`The handoff package is invalid.`,!0)}}let be=d(``),xe=d(!1),Se=d(null),Ce=d(``),we=d(!1),Te=d(!1),Ee=d(null),De=d(``),Oe=d(!1),ke=d(null);async function Ae(){s(Oe,!0);try{let e=await X.restartRestore(k(De).trim());s(ke,{restored:e.restored.length,refs:e.maximum_epoch_refs},!0)}finally{s(Oe,!1)}}function je(e){switch(e){case`awaiting_new_set`:return`Standing up new set`;case`ready_to_destroy`:return`New set live - destroying old shares`;case`complete`:return`Complete`;default:return e}}function Me(e){return o()?.peers.find(t=>t.user===e)?.display||`Friend ${e.slice(0,10)}…`}let Ne=d(null);function Pe(e){return e.filter(e=>e.online).length}async function Fe(e){s(Ne,e,!0);try{await X.resplitStart(e)}finally{s(Ne,null)}}let Ie=d(null);async function Le(e){s(Ie,e,!0);try{let t=await X.paperCards(e),n=URL.createObjectURL(new Blob([t],{type:`text/html`}));window.open(n,`_blank`,`noopener`),setTimeout(()=>URL.revokeObjectURL(n),6e4)}finally{s(Ie,null)}}async function Re(e,t){await ye(e)&&(t(!0),setTimeout(()=>t(!1),1200))}async function ze(){s(K,!0),s(ae,null),s(q,[],!0),s(J,null);try{let e=k(F)===`root`?{kind:`root`}:{kind:`vault`,vid:k(I).trim()};if(k(W)===`friends`){let t=await(k(E)===`split`?X.recoverySplitToTrustees:X.recoveryResplitToTrustees)(k(E)===`split`&&!k(N)?k(Y):k(M),e,k(H),k(G),k(ie));s(J,{delivered:t.delivered,undelivered:t.undelivered},!0),s(q,t.warnings,!0)}else{let t=await(k(E)===`split`?X.recoverySplit:X.recoveryResplit)(k(E)===`split`&&!k(N)?k(Y):k(M),e,k(H),k(U),k(ie));s(ae,t.shares,!0),s(q,t.warnings,!0)}}finally{s(K,!1)}}async function Be(){s(Q,!0),s(le,null);try{let e=await X.recoveryExtend(k(se),k(Z),k(ce));s(le,e.shares,!0)}finally{s(Q,!1)}}async function Ve(){s(_e,!0),s($,null);try{s($,await X.ceremonyOpen({subject:k(ue).trim(),claimant_display:k(de).trim(),ceremony_enc:k(me).trim(),new_node:k(he).trim(),reason:k(ge).trim()}),!0)}finally{s(_e,!1)}}async function He(){s(xe,!0),s(Se,null);try{s(Se,await X.ceremonyApprove(k(be).trim()),!0)}finally{s(xe,!1)}}async function Ue(){s(Te,!0),s(Ee,null);try{s(Ee,(await X.ceremonyAbort(k(Ce).trim())).abort_hex,!0)}finally{s(Te,!1)}}let We=d(y({})),Ge=d(!1),Ke=d(!1);function qe(e){s(be,e,!0),document.getElementById(`approve-id`)?.focus()}function Je(e){s(Ce,e,!0),s(we,!1),document.getElementById(`abort-id`)?.focus()}var Ye=tn(),Xe=_(v(Ye),4),Ze=e=>{var t=st(),n=_(v(t),2),i=v(n);P(n),P(t),l(()=>r(i,`${o().share_health.recovery_sets_owned??``} recovery set(s) split · + ${o().share_health.shares_held??``} share(s) held here in trust for others`)),B(e,t)};h(Xe,e=>{o()&&e(Ze)});var Qe=_(Xe,2),$e=_(v(Qe),6),et=_(v($e),2);z(et);var tt=_(et,2),nt=v(tt,!0);P(tt),P($e);var rt=_($e,2),it=v(rt),at=e=>{B(e,ct())},ot=e=>{var t=lt(),n=v(t);P(t),l(()=>r(n,`Restored ${k(ke).restored??``} vault(s) from ${k(ke).refs??``} maximum-epoch reference(s).`)),B(e,t)};h(it,e=>{k(Oe)?e(at):k(ke)&&e(ot,1)}),P(rt),P(Qe);var nn=_(Qe,2),rn=e=>{var t=mt(),n=_(c(t),4);C(n,5,()=>o().ceremonies,e=>e.ceremony_id,(e,t)=>{var n=pt();let i;var a=v(n),o=v(a),s=v(o),c=v(s,!0);P(s);var u=_(s,2),d=v(u,!0);P(u),P(o);var f=_(o,2),p=v(f,!0);P(f),P(a);var m=_(a,2),g=e=>{B(e,ut())};h(m,e=>{k(t).alarm&&e(g)});var y=_(m,2),b=v(y),ee=_(v(b)),x=v(ee,!0);P(ee),P(b);var S=_(b,2),C=_(v(S)),w=v(C);P(C),P(S);var T=_(S,2),E=_(v(T)),D=v(E,!0);P(E),P(T),P(y);var j=_(y,2),M=v(j),N=e=>{var n=dt();A(`click`,n,()=>qe(k(t).ceremony_id)),B(e,n)};h(M,e=>{k(t).trustee&&!k(t).approved&&e(N)});var te=_(M,2),ne=e=>{var n=ft();A(`click`,n,()=>Je(k(t).ceremony_id)),B(e,n)};h(te,e=>{k(t).is_self_subject&&!k(t).takeover&&e(ne)}),P(j),P(n),l(()=>{i=O(n,1,`card ceremony svelte-imw02`,null,i,{alarm:k(t).alarm}),r(c,k(t).claimant_display||`Unnamed claimant`),r(d,k(t).ceremony_id),r(p,k(t).phase),r(x,k(t).reason||`No reason supplied`),r(w,`${k(t).approvals??``} / ${k(t).threshold??``}`),r(D,k(t).sponsor)}),B(e,n)}),P(n),B(e,t)};h(nn,e=>{o()?.ceremonies?.length&&e(rn)});var an=_(nn,2),on=e=>{var t=gt(),n=_(c(t),4);C(n,5,()=>o().recovery_grants.minted,e=>e.rsid,(e,t)=>{var n=ht(),i=v(n),a=v(i);P(i);var o=_(i,2),s=v(o);P(o);var c=_(o,2),u=v(c,!0);P(c),P(n),l(()=>{r(a,`rsid ${k(t).rsid??``}`),r(s,`${k(t).trustees.length??``} share(s)`),c.disabled=k(Ie)===k(t).rsid,r(u,k(Ie)===k(t).rsid?`Opening…`:`Print / export paper cards`)}),A(`click`,c,()=>Le(k(t).rsid)),B(e,n)}),P(n),B(e,t)};h(an,e=>{o()?.recovery_grants?.minted?.length&&e(on)});var sn=_(an,2),cn=e=>{var t=yt();C(_(c(t),4),1,()=>o().pending_resplits,e=>e.old_rsid,(e,t)=>{var n=vt(),a=v(n),o=v(a),s=v(o);P(o),R(2),P(a);var c=_(a,2),u=v(c),d=v(u);P(u),R(),P(c);var f=_(c,4);C(f,21,()=>k(t).suggested,e=>e.user,(e,t)=>{var n=_t(),i=v(n),a=_(i,2),o=v(a);P(a);var s=_(a,2),c=v(s,!0);P(s),P(n),l(e=>{O(i,1,`dot ${k(t).online?`online`:`offline`}`,`svelte-imw02`),T(i,`title`,k(t).online?`online`:`offline`),r(o,`${e??``}…`),r(c,k(t).online?`online`:`offline`)},[()=>k(t).user.slice(0,12)]),B(e,n)}),P(f);var p=_(f,2),m=v(p),g=_(m),y=e=>{B(e,i(`will complete immediately once started.`))},b=ne(()=>Pe(k(t).suggested)===k(t).suggested.length&&k(t).suggested.length>0),ee=e=>{B(e,i(`will complete progressively as offline trustees come online.`))};h(g,e=>{k(b)?e(y):e(ee,-1)}),P(p);var x=_(p,2),S=v(x,!0);P(x),P(n),l((e,n,i)=>{r(s,`rsid ${k(t).old_rsid??``}`),r(d,`${e??``}…`),O(p,1,n),r(m,`${i??``} / ${k(t).suggested.length??``} suggested trustee(s) online - `),x.disabled=k(Ne)===k(t).old_rsid,r(S,k(Ne)===k(t).old_rsid?`Starting…`:`Start re-split (use suggested set)`)},[()=>k(t).ex_trustee.slice(0,12),()=>D(Pe(k(t).suggested)===k(t).suggested.length&&k(t).suggested.length>0?`healthy`:`muted`),()=>Pe(k(t).suggested)]),A(`click`,x,()=>Fe(k(t).old_rsid)),B(e,n)}),B(e,t)};h(sn,e=>{o()?.pending_resplits?.length&&e(cn)});var ln=_(sn,2),un=e=>{var t=Tt();C(_(c(t),4),1,()=>o().resplits,e=>e.old_rsid,(e,t)=>{var n=wt(),a=v(n),o=v(a),s=v(o);P(o);var c=_(o,2),u=v(c,!0);P(c),P(a);var d=_(a,2),f=v(d);P(d);var p=_(d,2),m=v(p),g=_(v(m),2),y=v(g),b=_(y),ee=e=>{B(e,bt())},x=e=>{B(e,xt())};h(b,e=>{k(t).new_set_live?e(ee):e(x,-1)}),P(g),P(m);var S=_(m,2),w=_(v(S),2),E=v(w),D=_(E),A=e=>{B(e,St())};h(D,e=>{k(t).new_set_live||e(A)}),P(w),P(S),P(p);var j=_(p,4);C(j,21,()=>k(t).remaining,e=>e.node,(e,t)=>{var n=Ct(),a=v(n),o=_(a,2),s=v(o);P(o);var c=_(o,2),u=v(c,!0);P(c);var d=_(c,2),f=v(d),p=e=>{B(e,i(`done`))},m=e=>{var n=i();l(()=>r(n,`online - ${k(t).role===`new`?`sending share`:`sending destroy`}`)),B(e,n)},g=e=>{B(e,i(`offline - queued`))};h(f,e=>{k(t).done?e(p):k(t).online?e(m,1):e(g,-1)}),P(d),P(n),l(e=>{O(a,1,`dot ${k(t).status??``}`,`svelte-imw02`),T(a,`title`,k(t).status),r(s,`${e??``}…`),O(c,1,`role ${k(t).role??``}`,`svelte-imw02`),r(u,k(t).role===`new`?`gets new share`:`gets destroy step`)},[()=>k(t).node.slice(0,12)]),B(e,n)}),P(j),P(n),l((e,n)=>{r(s,`rsid ${k(t).old_rsid??``} → ${k(t).new_rsid??``}`),O(c,1,`phase ${k(t).phase??``}`,`svelte-imw02`),r(u,e),r(f,`ex-trustee ${n??``}…`),r(y,`${k(t).new_attested??``} / ${k(t).new_total??``} `),r(E,`${k(t).old_destroyed??``} / ${k(t).old_total??``} `)},[()=>je(k(t).phase),()=>k(t).ex_trustee.slice(0,12)]),B(e,n)}),B(e,t)};h(ln,e=>{o()?.resplits?.length&&e(un)});var dn=_(ln,2),fn=e=>{var t=Ot(),n=_(c(t),2);C(n,5,()=>o().share_health.sets,e=>e.rsid,(e,t)=>{var n=Dt(),i=v(n),a=v(i);P(i);var o=_(i,2),s=v(o,!0);P(o);var c=_(o,2),u=v(c);P(c);var d=_(c,2),f=v(d);P(d);var p=_(d,2),m=e=>{var n=Et(),i=v(n,!0);P(n),l(e=>r(i,e),[()=>k(t).warnings.join(`, `)]),B(e,n)};h(p,e=>{k(t).warnings.length&&e(m)}),P(n),l((e,n)=>{r(a,`rsid ${k(t).rsid??``}`),r(s,e),r(u,`${k(t).threshold??``}-of-${k(t).issued??``}`),r(f,`${n??``}/${k(t).trustees.length??``} trustee grants delivered`)},[()=>k(t).scope.kind===`root`?`your root key`:`vault ${k(t).scope.vid.slice(0,10)}…`,()=>k(t).trustees.filter(e=>e.delivered).length]),B(e,n)}),P(n),B(e,t)};h(dn,e=>{o()?.share_health.sets.length&&e(fn)});var pn=_(dn,4),mn=v(pn),hn=v(mn),gn=v(hn);z(gn),gn.value=gn.__value=`split`,R(),P(hn);var _n=_(hn,2),vn=v(_n);z(vn),vn.value=vn.__value=`resplit`,R(),P(_n),P(mn);var yn=_(mn,2),bn=e=>{var t=At(),n=c(t),i=v(n);P(n);var a=_(n,2),o=_(v(a)),u=v(o);z(u),R(),P(o);var d=_(o),f=e=>{var t=kt();z(t),j(t,()=>k(M),e=>s(M,e)),B(e,t)};h(d,e=>{k(N)&&e(f)}),P(a),l(()=>r(i,`Carapace will create recovery set ${k(Y)??``}.`)),L(u,()=>k(N),e=>s(N,e)),B(e,t)},xn=e=>{var t=Mt(),n=_(c(t),2);C(n,5,()=>o()?.share_health.sets??[],e=>e.rsid,(e,t)=>{var n=jt(),i=v(n);P(n);var a={};l(()=>{r(i,`${k(t).scope.kind===`root`?`Identity recovery`:`Vault recovery`} · ${k(t).threshold??``}-of-${k(t).issued??``}`),a!==(a=k(t).rsid)&&(n.value=(n.__value=k(t).rsid)??``)}),B(e,n)}),P(n),te(n,()=>k(M),e=>s(M,e)),B(e,t)};h(yn,e=>{k(E)===`split`?e(bn):e(xn,-1)});var Sn=_(yn,2),Cn=v(Sn),wn=v(Cn);z(wn),wn.value=wn.__value=`root`,R(),P(Cn);var Tn=_(Cn,2),En=v(Tn);z(En),En.value=En.__value=`vault`,R(),P(Tn);var Dn=_(Tn,2),On=e=>{var t=Nt(),n=v(t);n.value=n.__value=``,C(_(n),1,()=>o()?.vaults.published??[],e=>e.vid,(e,t)=>{var n=jt(),i=v(n,!0);P(n);var a={};l(()=>{r(i,k(t).name),a!==(a=k(t).vid)&&(n.value=(n.__value=k(t).vid)??``)}),B(e,n)}),P(t),te(t,()=>k(I),e=>s(I,e)),B(e,t)};h(Dn,e=>{k(F)===`vault`&&e(On)}),P(Sn);var kn=_(Sn,2),An=_(v(kn),2),jn=v(An);z(jn),jn.value=jn.__value=`friends`,R(),P(An);var Mn=_(An,2),Nn=v(Mn);z(Nn),Nn.value=Nn.__value=`manual`,R(),P(Mn),P(kn);var Pn=_(kn,2),Fn=e=>{var t=Lt(),n=v(t),i=v(n);P(n);var a=_(n,2),c=e=>{B(e,Pt())},u=e=>{var t=It();C(t,5,()=>o().friends.list,e=>e,(e,t)=>{var n=Ft(),i=v(n);z(i);var a,o=_(i,2),c=v(o,!0);P(o),P(n),l(e=>{T(n,`title`,k(t)),a!==(a=k(t))&&(i.value=(i.__value=k(t))??``),r(c,e)},[()=>Me(k(t))]),w(S,[],i,()=>(k(t),k(G)),e=>s(G,e)),B(e,n)}),P(t),B(e,t)};h(a,e=>{o()?.friends.list.length?e(u,-1):e(c)}),P(t),l(()=>r(i,`Select trustees (${k(G).length??``} selected)`)),B(e,t)};h(Pn,e=>{k(W)===`friends`&&e(Fn)});var In=_(Pn,2),Ln=_(v(In),2);z(Ln);var Rn=_(Ln,2),zn=e=>{var t=Rt(),n=_(c(t),2);z(n),j(n,()=>k(U),e=>s(U,e)),B(e,t)},Bn=e=>{var t=zt(),n=v(t);P(t),l(()=>r(n,`of ${k(G).length??``} selected`)),B(e,t)};h(Rn,e=>{k(W)===`manual`?e(zn):e(Bn,-1)}),P(In);var Vn=_(In,2),Hn=v(Vn),Un=v(Hn);z(Un),R(),P(Hn),P(Vn);var Wn=_(Vn,2),Gn=v(Wn,!0);P(Wn),P(pn);var Kn=_(pn,2),qn=e=>{var t=Ut();let n;var i=_(v(t),2),a=v(i);P(i);var o=_(i,2),s=e=>{var t=Vt(),n=c(t),i=v(n);P(n);var a=_(n,2);C(a,20,()=>k(J).undelivered,e=>e,(e,t)=>{var n=Bt(),i=v(n,!0);P(n),l(()=>r(i,t)),B(e,n)}),P(a),l(()=>r(i,`Could not reach ${k(J).undelivered.length??``} trustee(s). Carapace will retry delivery during maintenance.`)),B(e,t)},u=e=>{B(e,Ht())};h(o,e=>{k(J).undelivered.length?e(s):e(u,-1)}),P(t),l(()=>{n=O(t,1,`card`,null,n,{"at-risk":k(J).undelivered.length>0}),r(a,`${k(J).delivered.length??``} trustee(s) confirmed that they stored a signed recovery grant.`)}),B(e,t)};h(Kn,e=>{k(J)&&e(qn)});var Jn=_(Kn,2),Yn=e=>{var t=Gt();C(t,20,()=>k(q),e=>e,(e,t)=>{var n=Wt(),i=v(n,!0);P(n),l(()=>r(i,t)),B(e,n)}),P(t),B(e,t)};h(Jn,e=>{k(q).length&&e(Yn)});var Xn=_(Jn,2),Zn=e=>{var t=qt();C(_(v(t),2),17,()=>k(ae),V,(e,t,n)=>{var i=Kt(),a=v(i),o=v(a,!0);P(a);var c=_(a,2),u=v(c,!0);P(c),P(i),l(()=>{r(o,k(t)),r(u,k(We)[n]?`Copied`:`Copy`)}),A(`click`,c,()=>Re(k(t),e=>s(We,{...k(We),[n]:e},!0))),B(e,i)}),P(t),B(e,t)};h(Xn,e=>{k(ae)&&e(Zn)});var Qn=_(Xn,6),$n=v(Qn),er=_(v($n),2);C(er,5,()=>o()?.share_health.sets??[],e=>e.rsid,(e,t)=>{var n=jt(),i=v(n);P(n);var a={};l(()=>{r(i,`${k(t).scope.kind===`root`?`Identity recovery`:`Vault recovery`} · ${k(t).threshold??``}-of-${k(t).issued??``}`),a!==(a=k(t).rsid)&&(n.value=(n.__value=k(t).rsid)??``)}),B(e,n)}),P(er);var tr=_(er,4);z(tr);var nr=_(tr,2),rr=v(nr);z(rr),R(),P(nr),P($n);var ir=_($n,2),ar=v(ir,!0);P(ir),P(Qn);var or=_(Qn,2),sr=e=>{var t=Yt();C(t,21,()=>k(le),V,(e,t)=>{var n=Jt(),i=v(n),a=v(i,!0);P(i),P(n),l(()=>r(a,k(t))),B(e,n)}),P(t),B(e,t)};h(or,e=>{k(le)&&e(sr)});var cr=_(or,6),lr=v(cr),ur=_(v(lr),6),dr=v(ur);dr.value=dr.__value=``,C(_(dr),1,()=>o()?.recovery_grants.held??[],e=>e,(e,t)=>{var n=jt(),i=v(n,!0);P(n);var a={};l(e=>{r(i,e),a!==(a=k(t))&&(n.value=(n.__value=k(t))??``)},[()=>Me(k(t))]),B(e,n)}),P(ur);var fr=_(ur,2),pr=e=>{B(e,Xt())};h(fr,e=>{o()?.recovery_grants.held.length||e(pr)});var mr=_(fr,4);z(mr);var hr=_(mr,4);re(hr);var gr=_(hr,2),_r=_(gr,2),vr=e=>{var t=Zt(),n=v(t,!0);P(t),l(()=>r(n,k(pe))),B(e,t)};h(_r,e=>{k(pe)&&e(vr)});var yr=_(_r,2),br=_(v(yr),4);z(br);var xr=_(br,4);z(xr),P(yr);var Sr=_(yr,4);z(Sr);var Cr=_(Sr,2),wr=v(Cr,!0);P(Cr);var Tr=_(Cr,2),Er=e=>{var t=Qt(),n=c(t),i=v(n);P(n);var a=_(n,4),o=v(a),u=v(o,!0);P(o);var d=_(o,2),f=v(d,!0);P(d),P(a),l(e=>{r(i,`id ${e??``}… · fanned out to ${k($).fanout_reached??``} peer(s)`),r(u,k($).sponsor_package),r(f,k(Ge)?`Copied`:`Copy`)},[()=>k($).ceremony_id.slice(0,12)]),A(`click`,d,()=>Re(k($).sponsor_package,e=>s(Ge,e,!0))),B(e,t)};h(Tr,e=>{k($)&&e(Er)}),P(lr);var Dr=_(lr,2),Or=_(v(Dr),4);z(Or);var kr=_(Or,2),Ar=v(kr,!0);P(kr);var jr=_(kr,2),Mr=e=>{var t=$t(),n=c(t),i=v(n);P(n);var a=_(n,2),o=v(a),u=v(o,!0);P(o);var d=_(o,2),f=v(d,!0);P(d),P(a),l(()=>{r(i,`Approval broadcast to ${k(Se).broadcast_reached??``} co-trustee(s).`),r(u,k(Se).approve_hex),r(f,k(Ke)?`Copied`:`Copy`)}),A(`click`,d,()=>Re(k(Se).approve_hex,e=>s(Ke,e,!0))),B(e,t)};h(jr,e=>{k(Se)&&e(Mr)}),P(Dr);var Nr=_(Dr,2),Pr=_(v(Nr),4);z(Pr);var Fr=_(Pr,2),Ir=v(Fr);z(Ir),R(),P(Fr);var Lr=_(Fr,2),Rr=v(Lr,!0);P(Lr);var zr=_(Lr,2),Br=e=>{var t=en(),n=c(t),i=v(n,!0);P(n),R(2),l(()=>r(i,k(Ee))),B(e,t)};h(zr,e=>{k(Ee)&&e(Br)}),P(Nr),P(cr),P(Ye),l((e,t,n)=>{tt.disabled=e,r(nt,k(Oe)?`Restoring…`:`Discover and restore retained vaults`),Wn.disabled=k(K)||k(W)===`friends`&&(k(G).length===0||k(H)>k(G).length),r(Gn,k(K)?`Splitting…`:k(E)===`split`?`Split key`:`Re-split key`),ir.disabled=k(Q),r(ar,k(Q)?`Issuing…`:`Issue new share(s)`),Cr.disabled=t,r(wr,k(_e)?`Opening…`:`Open ceremony`),kr.disabled=k(xe),r(Ar,k(xe)?`Recording…`:`Record approval`),Lr.disabled=n,r(Rr,k(Te)?`Signing…`:`Abort as subject`)},[()=>k(Oe)||!k(De).trim(),()=>k(_e)||!k(ue)||!k(de).trim()||!k(me).trim()||!k(he).trim()||!k(ge).trim(),()=>k(Te)||!k(Ce).trim()||!k(we)]),t(`submit`,$e,e=>(e.preventDefault(),Ae())),j(et,()=>k(De),e=>s(De,e)),t(`submit`,pn,e=>(e.preventDefault(),ze())),w(g,[],gn,()=>k(E),e=>s(E,e)),w(g,[],vn,()=>k(E),e=>s(E,e)),w(b,[],wn,()=>k(F),e=>s(F,e)),w(b,[],En,()=>k(F),e=>s(F,e)),w(x,[],jn,()=>k(W),e=>s(W,e)),w(x,[],Nn,()=>k(W),e=>s(W,e)),j(Ln,()=>k(H),e=>s(H,e)),L(Un,()=>k(ie),e=>s(ie,e)),t(`submit`,Qn,e=>(e.preventDefault(),Be())),te(er,()=>k(se),e=>s(se,e)),j(tr,()=>k(Z),e=>s(Z,e)),L(rr,()=>k(ce),e=>s(ce,e)),t(`submit`,lr,e=>(e.preventDefault(),Ve())),te(ur,()=>k(ue),e=>s(ue,e)),j(mr,()=>k(de),e=>s(de,e)),j(hr,()=>k(fe),e=>s(fe,e)),A(`click`,gr,ve),j(br,()=>k(me),e=>s(me,e)),j(xr,()=>k(he),e=>s(he,e)),j(Sr,()=>k(ge),e=>s(ge,e)),t(`submit`,Dr,e=>(e.preventDefault(),He())),j(Or,()=>k(be),e=>s(be,e)),t(`submit`,Nr,e=>(e.preventDefault(),Ue())),j(Pr,()=>k(Ce),e=>s(Ce,e)),L(Ir,()=>k(we),e=>s(we,e)),B(n,Ye),m(),p()}p([`click`]);var rn=a(``),an=a(``),on=a(`

          Send this to each person in the audience.

          `,1),sn=a(`
        • `),cn=a(`

            `,1),ln=a(`

            Shared files

            Share files from a vault

            Local operation · creates a non-recallable encrypted snapshot

            A share is a snapshot of these files at the vault's current epoch. It cannot + be recalled once handed over - editing the files afterward only affects future shares, not + this one.

            Recipients
            Advanced manual recipient identities

            Fetch a file someone shared with you

            Peer-dependent operation · no partial output is activated, so a failed fetch is safe to retry

            Advanced manual peer
            `);function un(n,i){e(i,!0);let a=()=>ee(oe,`$status`,o),[o,f]=u(),p=[],g=d(y([]));X.listVaults().then(e=>s(g,e.published,!0)).catch(()=>{});let b=d(``),x=d(``),S=d(``),T=d(y([])),E=d(!1),D=d(null),O=d(!1),M=d(!1);async function N(){if(!(!k(b)||!k(x).trim()||k(T).length===0&&!k(S).trim())){s(E,!0),s(D,null);try{let e=k(x).split(` +`).map(e=>e.trim()).filter(Boolean),t=k(S).split(`,`).map(e=>e.trim()).filter(Boolean),n=[...new Set([...k(T),...t])],r=await X.discloseFiles(k(b),e,n);s(D,r.grant_hex,!0)}finally{s(E,!1)}}}async function ne(){k(D)&&await ye(k(D))&&(s(O,!0),setTimeout(()=>s(O,!1),1200))}let F=d(``),I=d(``),V=d(``),H=d(``),U=d(``),W=d(!1),G=d(null);async function ie(){let e=a()?.peers.find(e=>e.node===k(H));if(e&&(s(I,e.node,!0),s(V,e.addrs.join(`,`),!0)),!(!k(F).trim()||!k(I).trim()||!k(U).trim())){s(W,!0),s(G,null);try{let e=k(V).split(`,`).map(e=>e.trim()).filter(Boolean),t=await X.fetchGrant(k(F).trim(),{node:k(I).trim(),addrs:e},k(U).trim());s(G,t.written,!0)}finally{s(W,!1)}}}var K=ln(),ae=_(v(K),2),q=_(v(ae),6),J=_(v(q),2),Y=v(J);Y.value=Y.__value=``,C(_(Y),17,()=>k(g),e=>e.vid,(e,t)=>{var n=rn(),i=v(n);P(n);var a={};l(()=>{r(i,`${k(t).name??``} (epoch ${k(t).epoch??``})`),a!==(a=k(t).vid)&&(n.value=(n.__value=k(t).vid)??``)}),B(e,n)}),P(J);var se=_(J,4);re(se);var Z=_(se,2);C(_(v(Z)),1,()=>a()?.peers??[],e=>e.node,(e,t)=>{var n=an(),i=v(n);z(i);var a,o=_(i);P(n),l(e=>{a!==(a=k(t).user)&&(i.value=(i.__value=k(t).user)??``),r(o,` ${e??``}`)},[()=>k(t).display||`Friend ${k(t).user.slice(0,10)}…`]),w(p,[],i,()=>(k(t).user,k(T)),e=>s(T,e)),B(e,n)}),P(Z);var ce=_(Z,2),Q=_(v(ce),2);z(Q),P(ce);var le=_(ce,2),ue=v(le);z(ue),R(),P(le);var de=_(le,2),fe=v(de,!0);P(de),P(q);var pe=_(q,2),me=e=>{var t=on(),n=c(t),i=v(n),a=v(i,!0);P(i);var o=_(i,2),s=v(o,!0);P(o),P(n),R(2),l(()=>{r(a,k(D)),r(s,k(O)?`Copied`:`Copy`)}),A(`click`,o,ne),B(e,t)};h(pe,e=>{k(D)&&e(me)}),P(ae);var he=_(ae,2),ge=_(v(he),4),_e=_(v(ge),2);z(_e);var $=_(_e,3),ve=v($);ve.value=ve.__value=``,C(_(ve),1,()=>a()?.peers??[],e=>e.node,(e,t)=>{var n=rn(),i=v(n,!0);P(n);var a={};l(()=>{r(i,k(t).display||`Friend`),a!==(a=k(t).node)&&(n.value=(n.__value=k(t).node)??``)}),B(e,n)}),P($);var be=_($,2),xe=_(v(be),2);z(xe);var Se=_(xe,2);z(Se),P(be);var Ce=_(be,4);z(Ce);var we=_(Ce,2),Te=v(we,!0);P(we),P(ge);var Ee=_(ge,2),De=e=>{var t=cn(),n=c(t),i=v(n);P(n);var a=_(n,2);C(a,20,()=>k(G),e=>e,(e,t)=>{var n=sn(),i=v(n,!0);P(n),l(()=>r(i,t)),B(e,n)}),P(a),l(()=>r(i,`Wrote ${k(G).length??``} file(s):`)),B(e,t)};h(Ee,e=>{k(G)&&e(De)}),P(he),P(K),l(e=>{de.disabled=e,r(fe,k(E)?`Sharing…`:`Share files`),we.disabled=k(W),r(Te,k(W)?`Fetching…`:`Fetch files`)},[()=>k(E)||!k(b)||!k(x).trim()||k(T).length===0&&!k(S).trim()||!k(M)]),t(`submit`,q,e=>(e.preventDefault(),N())),te(J,()=>k(b),e=>s(b,e)),j(se,()=>k(x),e=>s(x,e)),j(Q,()=>k(S),e=>s(S,e)),L(ue,()=>k(M),e=>s(M,e)),t(`submit`,ge,e=>(e.preventDefault(),ie())),j(_e,()=>k(F),e=>s(F,e)),te($,()=>k(H),e=>s(H,e)),j(xe,()=>k(I),e=>s(I,e)),j(Se,()=>k(V),e=>s(V,e)),j(Ce,()=>k(U),e=>s(U,e)),B(n,K),m(),f()}p([`click`]);var dn=a(` `),fn=a(`
            Carapace
            `);function pn(t,i){e(i,!0);let a=()=>ee(se,`$live`,c),[c,f]=u(),p={"":`Overview`,vaults:`Vaults`,friends:`Friends`,recovery:`Recovery`,shared:`Shared files`};function g(){let e=typeof location<`u`?location.hash.replace(/^#\/?/,``):``;return e in p?e:``}let b=d(y(g()));function S(){s(b,g(),!0)}let w=d(null);function E(e){e?document.documentElement.setAttribute(`data-theme`,e):document.documentElement.removeAttribute(`data-theme`)}function D(){let e=typeof matchMedia<`u`&&matchMedia(`(prefers-color-scheme: light)`).matches,t=k(w)?k(w)===`light`:e;s(w,t?`dark`:`light`,!0),localStorage.setItem(`carapace-theme`,k(w)),E(k(w))}F(()=>{window.addEventListener(`hashchange`,S);let e=localStorage.getItem(`carapace-theme`);return(e===`dark`||e===`light`)&&(s(w,e,!0),E(k(w))),de(),()=>window.removeEventListener(`hashchange`,S)}),N(()=>{fe()});var j=fn();x(`1uha8ag`,e=>{o(()=>{n.title=`Carapace`})});var M=v(j),te=_(v(M),2);C(te,21,()=>Object.entries(p),([e,t])=>e,(e,t)=>{var n=ne(()=>I(k(t),2));let i=()=>k(n)[0],a=()=>k(n)[1];var o=dn();let s;var c=v(o,!0);P(o),l(()=>{T(o,`href`,i()?`#/${i()}`:`#/`),s=O(o,1,`svelte-1uha8ag`,null,s,{active:k(b)===i()}),r(c,a())}),B(e,o)}),P(te);var L=_(te,2),R=v(L);let z;var V=_(R,2),re=v(V,!0);P(V);var H=_(V,2),U=v(H,!0);P(H),P(L),P(M);var W=_(M,2),G=v(W);me(G,{});var ie=_(G,2),K=e=>{qe(e,{})},ae=e=>{ot(e,{})},q=e=>{nn(e,{})},J=e=>{un(e,{})},Y=e=>{De(e,{})};h(ie,e=>{k(b)===`vaults`?e(K):k(b)===`friends`?e(ae,1):k(b)===`recovery`?e(q,2):k(b)===`shared`?e(J,3):e(Y,-1)}),P(W),P(j),l(()=>{z=O(R,1,`live-dot svelte-1uha8ag`,null,z,{live:a()}),T(R,`title`,a()?`Live updates connected`:`Reconnecting…`),r(re,a()?`Live`:`Reconnecting…`),r(U,k(w)===`light`?`Molt (light)`:`Dark`)}),A(`click`,H,D),B(t,j),m(),f()}p([`click`]);export{pn as component}; \ No newline at end of file diff --git a/crates/carapace-api/static/_app/immutable/nodes/2.CaWSwQlO.js b/crates/carapace-api/static/_app/immutable/nodes/2.CaWSwQlO.js deleted file mode 100644 index 10fcd26..0000000 --- a/crates/carapace-api/static/_app/immutable/nodes/2.CaWSwQlO.js +++ /dev/null @@ -1,25 +0,0 @@ -import{$ as e,A as t,B as n,C as r,D as i,E as a,F as o,G as s,H as c,I as l,J as u,K as d,L as f,O as p,Q as m,S as h,T as g,U as _,V as v,W as y,X as b,Y as ee,_ as x,a as S,b as C,c as w,d as T,f as E,g as D,h as O,j as k,k as A,l as j,m as M,n as te,nt as N,p as P,q as ne,r as F,rt as I,s as L,tt as R,u as z,w as B,x as V,z as re}from"../chunks/D7CCBGlz.js";import"../chunks/CCeg2KC3.js";import"../chunks/xihTtKlq.js";function H(){return typeof window<`u`&&window.__CARAPACE_TOKEN__?window.__CARAPACE_TOKEN__:``}var U=b(null);function W(e){U.set(e)}function ie(){U.set(null)}var ae=``,G=class extends Error{status;constructor(e,t){super(e),this.status=t}};async function K(e,t){let n;try{n=await fetch(ae+e,{...t,headers:{...t?.body?{"Content-Type":`application/json`}:{},Authorization:`Bearer ${H()}`,...t?.headers}})}catch(t){let n=`Could not reach the daemon (${e}). Is it running? (${t.message})`;throw W(n),new G(n)}if(n.status===401||n.status===403){let e=n.status===401?`The daemon rejected this session (bad or missing token). Reload the page.`:`The daemon refused this request (Host/Origin guard). Reload the page.`;throw W(e),new G(e,n.status)}if(!n.ok){let t=n.statusText;try{let e=await n.json();typeof e?.error==`string`&&(t=e.error)}catch{}throw W(`${e} failed: ${t}`),new G(t,n.status)}return await n.json()}async function q(e,t){let n;try{n=await fetch(ae+e,{...t,headers:{Authorization:`Bearer ${H()}`,...t?.headers}})}catch(t){let n=`Could not reach the daemon (${e}). Is it running? (${t.message})`;throw W(n),new G(n)}if(n.status===401||n.status===403){let e=n.status===401?`The daemon rejected this session (bad or missing token). Reload the page.`:`The daemon refused this request (Host/Origin guard). Reload the page.`;throw W(e),new G(e,n.status)}if(!n.ok){let t=n.statusText;try{let e=await n.json();typeof e?.error==`string`&&(t=e.error)}catch{}throw W(`${e} failed: ${t}`),new G(t,n.status)}return n.text()}var J=e=>K(e),Y=(e,t)=>K(e,{method:`POST`,body:t===void 0?void 0:JSON.stringify(t)}),X={health:()=>J(`/api/health`),status:()=>J(`/api/status`),listVaults:()=>J(`/api/vaults`),publishVault:(e,t)=>Y(`/api/vaults`,{dir:e,vid:t}),listReplicas:e=>J(`/api/vaults/${e}/replicas`),placeReplicas:(e,t,n)=>Y(`/api/vaults/${e}/replicas`,{peers:t,r:n}),discloseFiles:(e,t,n)=>Y(`/api/vaults/${e}/grants`,{paths:t,audience:n}),fetchGrant:(e,t,n)=>Y(`/api/grants/fetch`,{grant_hex:e,owner:t,out_dir:n}),listFriends:()=>J(`/api/friends`),issueTicket:()=>Y(`/api/friends/ticket`),addFriend:(e,t,n)=>Y(`/api/friends`,{ticket_hex:e,addrs:t,grant_bytes:n}),unfriend:e=>Y(`/api/friends/${e}/unfriend`),resplitStatus:e=>J(`/api/recovery/${e}/resplit-status`),paperCards:e=>q(`/api/recovery/${e}/paper`),resplitStart:(e,t)=>Y(`/api/recovery/${e}/resplit-start`,t?{trustees:t}:void 0),recoverySplit:(e,t,n,r,i)=>Y(`/api/recovery/split`,{rsid:e,scope:t,m:n,n:r,allow_over_cap:i}),recoveryResplit:(e,t,n,r,i)=>Y(`/api/recovery/resplit`,{rsid:e,scope:t,m:n,n:r,allow_over_cap:i}),recoveryExtend:(e,t,n)=>Y(`/api/recovery/extend`,{rsid:e,count:t,allow_over_cap:n}),ceremonyOpen:e=>Y(`/api/recovery/ceremony/open`,e),ceremonyApprove:e=>Y(`/api/recovery/ceremony/approve`,{ceremony_id:e}),ceremonyAbort:e=>Y(`/api/recovery/ceremony/abort`,{ceremony_id:e})},oe=b(null),se=b(!1),Z=null,Q=1e3;function $(){if(typeof window>`u`)return;let e=`${location.protocol===`https:`?`wss:`:`ws:`}//${location.host}/api/events?token=${encodeURIComponent(H())}`;Z=new WebSocket(e),Z.onopen=()=>{se.set(!0),Q=1e3},Z.onmessage=e=>{try{oe.set(JSON.parse(e.data))}catch{}},Z.onclose=()=>{se.set(!1),setTimeout($,Q),Q=Math.min(Q*2,15e3)},Z.onerror=()=>{Z?.close()}}function ce(){X.status().then(e=>oe.set(e)).catch(()=>{}),$()}function le(){Z?.close(),Z=null}var ue=a(``);function de(e){let t=()=>ee(U,`$lastError`,n),[n,i]=u();var a=g(),o=c(a),s=e=>{var n=ue(),i=v(n),a=v(i,!0);N(i);var o=_(i,2);N(n),l(()=>r(a,t())),A(`click`,o,function(...e){ie?.apply(this,e)}),B(e,n)};h(o,e=>{t()&&e(s)}),B(e,a),i()}p([`click`]);var fe=`carapace-gui-notes-v1`;function pe(){if(typeof localStorage>`u`)return{friendStorageGrants:{},recoverySets:{}};try{let e=localStorage.getItem(fe);return e?JSON.parse(e):{friendStorageGrants:{},recoverySets:{}}}catch{return{friendStorageGrants:{},recoverySets:{}}}}var me=b(pe());me.subscribe(e=>{typeof localStorage>`u`||localStorage.setItem(fe,JSON.stringify(e))});function he(e,t){me.update(n=>({...n,friendStorageGrants:{...n.friendStorageGrants,[e]:t}}))}function ge(e){me.update(t=>({...t,recoverySets:{...t.recoverySets,[String(e.rsid)]:e}}))}var _e=a(``),ve=a(`
            `),ye=a(`
            `);function be(e,t){function n(e){let t=Math.max(e.target,e.achieved,1);return Array.from({length:t},(t,n)=>({filled:nt.plates,e=>e.key,(e,t,i)=>{var a=ve(),o=v(a);C(o,21,()=>n(k(t)),V,(e,t)=>{var n=_e();let r;l(()=>r=O(n,1,`segment svelte-v2pnom`,null,r,{filled:k(t).filled})),B(e,n)}),N(o);var s=_(o,2),c=v(s,!0);N(s);var u=_(s,2),d=v(u,!0);N(u);var f=_(u,2),p=v(f,!0);N(f),N(a),l(()=>{O(a,1,`plate-group state-${k(t).state??``}`,`svelte-v2pnom`),M(a,`--i: ${k(i)??``}`),r(c,k(t).label),r(d,k(t).valueLabel),r(p,k(t).note)}),B(e,a)}),N(i),B(e,i)}function xe(e,t=8,n=6){return e.length<=t+n+1?e:`${e.slice(0,t)}…${e.slice(-n)}`}async function Se(e){try{return await navigator.clipboard.writeText(e),!0}catch{return!1}}var Ce=[`B`,`KiB`,`MiB`,`GiB`,`TiB`];function we(e){if(!Number.isFinite(e)||e<0)return`—`;if(e===0)return`0 B`;let t=Math.min(Math.floor(Math.log2(e)/10),Ce.length-1),n=e/2**(10*t);return`${n>=10||t===0?Math.round(n):n.toFixed(1)} ${Ce[t]}`}var Te=a(``);function Ee(t,n){e(n,!0);let i=S(n,`head`,3,8),a=S(n,`tail`,3,6),o=d(!1);async function c(){await Se(n.value)&&(s(o,!0),setTimeout(()=>s(o,!1),1200))}var u=Te(),f=v(u),p=_(f),h=v(p,!0);N(p),N(u),l(e=>{T(u,`title`,n.value),T(u,`aria-label`,`Copy ${n.value??``}`),r(f,`${e??``} `),r(h,k(o)?`copied`:`copy`)},[()=>xe(n.value,i(),a())]),A(`click`,u,c),B(t,u),m()}p([`click`]);var De=a(`
            This node
            Friends storing your vaults
            Vaults published
            `,1),Oe=a(`

            Waiting for the daemon…

            `),ke=a(`

            Shell integrity

            `);function Ae(t,n){e(n,!0);let i=()=>ee(oe,`$status`,o),a=()=>ee(me,`$notes`,o),[o,p]=u(),g=d(null);f(()=>{let e=i()?.vaults.published??[];if(e.length===0){s(g,null);return}Promise.all(e.map(e=>X.listReplicas(e.vid).catch(()=>({members:[]})))).then(e=>{s(g,Math.min(...e.map(e=>e.members.length)),!0)}).catch(()=>{s(g,null)})});let y=ne(()=>{let e=i();if(!e)return[];let t=e.vaults.published.length,n=k(g)??0,r={key:`replicas`,label:`Replicas`,achieved:t===0?0:Math.min(n,3),target:3,valueLabel:t===0?`—`:`${n}/3`,state:t===0?`empty`:n>=3?`healthy`:`at-risk`,note:t===0?`No vaults published yet`:`Weakest vault held by ${n} friend${n===1?``:`s`}`},o=e.share_health.recovery_sets_owned,s=Object.values(a().recoverySets).find(e=>e.scope.kind===`root`),c={key:`shares`,label:`Recovery shares`,achieved:+(o>0),target:1,valueLabel:s?`${s.m}-of-${s.n}`:o>0?`split`:`—`,state:o>0?`healthy`:`empty`,note:o>0?`${e.share_health.shares_held} share${e.share_health.shares_held===1?``:`s`} held here in trust for others`:`Your key has no trustees yet - nobody could rebuild it`},l=e.addr.length,u=e.relay_networks,d=e.relay_diversity_warning||u<2;return[r,c,{key:`relays`,label:`Reachability`,achieved:Math.min(u,2),target:2,valueLabel:`${u}`,state:l===0?`empty`:d?`at-risk`:`healthy`,note:d?`Only ${u} relay network${u===1?``:`s`} - add a friend's relay so you can still be reached if one drops`:`${e.reachability} · ${u} relay networks, ${l} dialable address${l===1?``:`es`}`}]});var b=ke(),x=_(v(b),2),S=e=>{var t=De(),n=c(t);be(n,{get plates(){return k(y)}});var a=_(n,2),o=v(a);Ee(_(v(o),2),{get value(){return i().node_id},head:12,tail:8}),N(o);var s=_(o,2),u=_(v(s),2),d=v(u,!0);N(u),N(s);var f=_(s,2),p=_(v(f),2),m=v(p,!0);N(p),N(f),N(a),R(2),l(()=>{r(d,i().friends.count),r(m,i().vaults.published.length)}),B(e,t)},C=e=>{B(e,Oe())};h(x,e=>{i()?e(S):e(C,-1)}),N(b),B(t,b),m(),p()}var je=a(`

            Loading vaults…

            `),Me=a(`

            No vaults published yet. Publish a directory above to start protecting it.

            `),Ne=a(`
          • `),Pe=a(`
              `),Fe=a(`no replicas placed - vault exists only here`),Ie=a(`
              Replica members
              `),Le=a(`
              `),Re=a(``),ze=a(`
              `),Be=a(`

              `),Ve=a(`

              Place replicas for

              The daemon doesn't remember a friend's network address for you - list each peer's node - id and dialable address(es) again here.

              `),He=a(`

              Vaults

              A vault is a directory you've published for friends to hold replicas of. Publishing ingests - and encrypts it locally; placing replicas is what actually copies it out.

              `);function Ue(n,i){e(i,!0);let a=()=>ee(oe,`$status`,o),[o,c]=u(),p=d(y([])),g=d(y({})),b=d(!0),x=d(``),S=d(!1),w=d(null),T=d(3),E=d(y([{node:``,addrs:``}])),D=d(!1),O=d(null);async function M(){s(b,!0);let e=await X.listVaults().catch(()=>({published:[]}));s(p,e.published,!0);let t=await Promise.all(k(p).map(async e=>[e.vid,(await X.listReplicas(e.vid).catch(()=>({members:[]}))).members]));s(g,Object.fromEntries(t),!0),s(b,!1)}M(),f(()=>{a()?.vaults.published.length,M()});async function te(){if(k(x).trim()){s(S,!0);try{await X.publishVault(k(x).trim()),s(x,``),await M()}finally{s(S,!1)}}}function P(e){s(w,e,!0),s(O,null),s(E,[{node:``,addrs:``}],!0)}function ne(){s(E,[...k(E),{node:``,addrs:``}],!0)}function F(e){s(E,k(E).filter((t,n)=>n!==e),!0)}async function I(){if(k(w)){s(D,!0);try{let e=k(E).filter(e=>e.node.trim()).map(e=>({node:e.node.trim(),addrs:e.addrs.split(`,`).map(e=>e.trim()).filter(Boolean)})),t=await X.placeReplicas(k(w),e,k(T));s(O,t.placed,!0),await M()}finally{s(D,!1)}}}var L=He(),R=_(v(L),4),re=_(v(R),2),H=v(re);z(H);var U=_(H,2),W=v(U,!0);N(U),N(re),N(R);var ie=_(R,2),ae=e=>{B(e,je())},G=e=>{B(e,Me())},K=e=>{var t=Le();C(t,21,()=>k(p),e=>e.vid,(e,t)=>{var n=Ie(),i=v(n),a=v(i);Ee(a,{get value(){return k(t).vid}});var o=_(a,2),s=v(o);N(o),N(i);var c=_(i,2),u=_(v(c),2),d=e=>{var n=Pe();C(n,20,()=>k(g)[k(t).vid],e=>e,(e,t)=>{var n=Ne();Ee(v(n),{get value(){return t}}),N(n),B(e,n)}),N(n),B(e,n)},f=e=>{B(e,Fe())};h(u,e=>{k(g)[k(t).vid]?.length?e(d):e(f,-1)}),N(c);var p=_(c,2);N(n),l(()=>r(s,`epoch ${k(t).epoch??``}`)),A(`click`,p,()=>P(k(t).vid)),B(e,n)}),N(t),B(e,t)};h(ie,e=>{k(b)?e(ae):k(p).length===0?e(G,1):e(K,-1)});var q=_(ie,2),J=e=>{var t=Ve(),n=v(t),i=_(v(n)),a=v(i);N(i),N(n);var o=_(n,4);C(o,17,()=>k(E),V,(e,t,n)=>{var r=ze(),i=v(r);z(i);var a=_(i,2);z(a);var o=_(a,2),s=e=>{var t=Re();A(`click`,t,()=>F(n)),B(e,t)};h(o,e=>{k(E).length>1&&e(s)}),N(r),j(i,()=>k(t).node,e=>k(t).node=e),j(a,()=>k(t).addrs,e=>k(t).addrs=e),B(e,r)});var c=_(o,2),u=_(c,2),d=_(v(u),2);z(d);var f=_(d,2),p=v(f,!0);N(f);var m=_(f,2);N(u);var g=_(u,2),y=e=>{var t=Be(),n=v(t);N(t),l(()=>r(n,`Placed on ${k(O).length??``} peer${k(O).length===1?``:`s`}.`)),B(e,t)};h(g,e=>{k(O)&&e(y)}),N(t),l(e=>{r(a,`${e??``}…`),f.disabled=k(D),r(p,k(D)?`Placing…`:`Place`)},[()=>k(w).slice(0,12)]),A(`click`,c,ne),j(d,()=>k(T),e=>s(T,e)),A(`click`,f,I),A(`click`,m,()=>s(w,null)),B(e,t)};h(q,e=>{k(w)&&e(J)}),N(L),l(e=>{U.disabled=e,r(W,k(S)?`Publishing…`:`Publish vault`)},[()=>k(S)||!k(x).trim()]),t(`submit`,R,e=>(e.preventDefault(),te())),j(H,()=>k(x),e=>s(x,e)),B(n,L),m(),c()}p([`click`]);var We=a(`
              `),Ge=a(`

              `),Ke=a(`

              Re-split required

              Recovery & trustees.

              `,1),qe=a(`

              `),Je=a(`
              `),Ye=a(`

              Loading…

              `),Xe=a(`

              No friends yet. Issue an invite ticket to add your first one.

              `),Ze=a(`Remove this friend? `),Qe=a(``),$e=a(`
              `),et=a(`
              `),tt=a(`

              Friends

              Friends hold encrypted replicas of your vaults and, if you make them trustees, pieces of your - recovery key.

              Invite a friend

              Add a friend from a ticket

              Your friends

              The daemon doesn't yet report a friend's storage/trustee/relay role or agreed limit back to the - GUI - the figures above are only what this browser set when adding the friend.

              `);function nt(n,a){e(a,!0);let o=()=>ee(oe,`$status`,g),p=()=>ee(me,`$notes`,g),[g,b]=u(),x=d(y([])),S=d(!0),w=d(null),T=d(!1),D=d(``),M=d(``),te=d(1),P=d(!1),ne=d(null),F=d(!1),I=d(null),L=d(null),V=d(null);async function re(){s(S,!0);let e=await X.listFriends().catch(()=>({count:0,list:[]}));s(x,e.list,!0),s(S,!1)}re(),f(()=>{o()?.friends.count,re()});async function H(){s(T,!0);try{let e=await X.issueTicket();s(w,e,!0)}finally{s(T,!1)}}async function U(){k(w)&&await Se(k(w).uri)&&(s(F,!0),setTimeout(()=>s(F,!1),1200))}async function W(){if(k(D).trim()){s(P,!0),s(ne,null);try{let e=k(M).split(`,`).map(e=>e.trim()).filter(Boolean),t=Math.round(k(te)*1024**3),n=await X.addFriend(k(D).trim(),e.length?e:void 0,t);he(n.friend,t),s(ne,`Friend added (${n.friend.slice(0,12)}…).`),s(D,``),s(M,``),await re()}finally{s(P,!1)}}}async function ie(e){s(L,e,!0);try{let t=await X.unfriend(e);s(I,null),t.was_friend&&s(V,{friend:e,resplit:t.resplit_triggered,rsids:t.recovery_set_ids},!0),await re()}finally{s(L,null)}}var ae=tt(),G=_(v(ae),4),K=v(G),q=_(v(K),2),J=v(q,!0);N(q);var Y=_(q,2),se=e=>{var t=We(),n=_(v(t),2),i=v(n);z(i);var a=_(i,2),o=v(a,!0);N(a),N(n),N(t),l(()=>{E(i,k(w).uri),r(o,k(F)?`Copied`:`Copy`)}),A(`click`,a,U),B(e,t)};h(Y,e=>{k(w)&&e(se)}),N(K);var Z=_(K,2),Q=_(v(Z),2),$=_(v(Q),2);z($);var ce=_($,4);z(ce);var le=_(ce,4);z(le);var ue=_(le,2),de=v(ue,!0);N(ue),N(Q);var fe=_(Q,2),pe=e=>{var t=Ge(),n=v(t,!0);N(t),l(()=>r(n,k(ne))),B(e,t)};h(fe,e=>{k(ne)&&e(pe)}),N(Z),N(G);var ge=_(G,2),_e=e=>{var t=Je(),n=v(t),i=e=>{var t=Ke(),n=_(c(t),2),i=v(n);R(2),N(n),l((e,t)=>r(i,`${e??``}… was a trustee. A trustee re-split is now running for - recovery set${k(V).rsids.length>1?`s`:``} - ${t??``}. Both the old and new sets stay usable until the new set is - live and the old shares are destroyed - track it under `),[()=>k(V).friend.slice(0,12),()=>k(V).rsids.join(`, `)]),B(e,t)},a=e=>{var t=qe(),n=v(t);N(t),l(e=>r(n,`${e??``}… removed. They held no recovery shares, so no re-split - was needed.`),[()=>k(V).friend.slice(0,12)]),B(e,t)};h(n,e=>{k(V).resplit?e(i):e(a,-1)});var o=_(n,2);N(t),l(()=>O(t,1,`card ${k(V).resplit?`at-risk`:`healthy`}`)),A(`click`,o,()=>s(V,null)),B(e,t)};h(ge,e=>{k(V)&&e(_e)});var ve=_(ge,4),ye=e=>{B(e,Ye())},be=e=>{B(e,Xe())},xe=e=>{var t=et();C(t,20,()=>k(x),e=>e,(e,t)=>{var n=$e(),a=v(n);Ee(a,{get value(){return t}});var o=_(a,2),c=v(o),u=e=>{var n=i();l(e=>r(n,`≈${e??``} agreed (recorded in this browser)`),[()=>we(p().friendStorageGrants[t])]),B(e,n)},d=e=>{B(e,i(`storage limit not recorded here`))};h(c,e=>{p().friendStorageGrants[t]===void 0?e(d,-1):e(u)}),N(o);var f=_(o,2),m=e=>{var n=Ze(),i=_(v(n),2),a=v(i,!0);N(i);var o=_(i,2);N(n),l(()=>{i.disabled=k(L)===t,r(a,k(L)===t?`Removing…`:`Confirm unfriend`),o.disabled=k(L)===t}),A(`click`,i,()=>ie(t)),A(`click`,o,()=>s(I,null)),B(e,n)},g=e=>{var n=Qe();A(`click`,n,()=>s(I,t,!0)),B(e,n)};h(f,e=>{k(I)===t?e(m):e(g,-1)}),N(n),B(e,n)}),N(t),B(e,t)};h(ve,e=>{k(S)?e(ye):k(x).length===0?e(be,1):e(xe,-1)}),R(2),N(ae),l(e=>{q.disabled=k(T),r(J,k(T)?`Issuing…`:`Create invite ticket`),ue.disabled=e,r(de,k(P)?`Adding…`:`Add friend`)},[()=>k(P)||!k(D).trim()]),A(`click`,q,H),t(`submit`,Q,e=>(e.preventDefault(),W())),j($,()=>k(D),e=>s(D,e)),j(ce,()=>k(M),e=>s(M,e)),j(le,()=>k(te),e=>s(te,e)),B(n,ae),m(),b()}p([`click`]);var rt=a(`
              Live on this node

              `),it=a(`
              `),at=a(`

              Paper cards (offline backstop)

              Print a paper card for each recovery set (§8, §10.2). A card recovers from its words - alone - offline, with no Carapace software - so it is the backstop that never goes - offline. The card shows a share's secret words; print it, then keep or destroy the copy.

              `,1),ot=a(`
              `),st=a(`
              re-split required

              was a trustee of this recovery set and - was unfriended. Their retained share must be neutralized by re-splitting to a fresh set.

              Suggested new trustee set - live reachability

              `),ct=a(`

              Re-split required

              An unfriended trustee still held a share of the recovery set below (§9.3.4). Start the - re-split to hand a fresh share to a new trustee set - the old shares are only destroyed - once that new set is live.

              `,1),lt=a(`· live`),ut=a(`· not live yet`),dt=a(`· destroy refused until new set is live`),ft=a(`
              `),pt=a(`

              New set attested (destroy gate: M + slack)

              Old shares destroyed (ack)

              Remaining friends - live reachability
              `),mt=a(`

              Trustee re-splits in progress

              An unfriended trustee's share is being neutralized (§9.3 step 4). Both the old and new - recovery sets stay usable until the new set is live and the old shares are destroyed - - neither door closes early.

              `,1),ht=a(`
              `),gt=a(`

              Recovery sets split from this browser

              `,1),_t=a(``),vt=a(`

              `),yt=a(`
              `),bt=a(``),xt=a(`

              Shares - send one to each trustee

              `),St=a(``),Ct=a(`
              `),wt=a(`

              Signed open - hand to the claimant
              `,1),Tt=a(`

              `,1),Et=a(`

              Send this abort to your trustees.

              `,1),Dt=a(`

              Recovery & trustees

              Split your key into pieces so a group of trustees can rebuild it if you lose access. - Each share below is a bearer secret: send one to each trustee yourself - the daemon - doesn't track who you gave it to.

              Split or re-split

              Add a trustee (extend)

              Recovery ceremony

              A ceremony is opened by a signed request a trustee receives out of band (from the person - recovering, or the daemon that observed them). The recovery delay and required approvals are - enforced by the daemon per the grant that authorized the ceremony; paste the pieces below as - they arrive.

              Open (sponsor)

              Approve

              Abort

              `);function Ot(n,a){e(a,!0);let o=()=>ee(me,`$notes`,p),f=()=>ee(oe,`$status`,p),[p,g]=u(),b=[],x=[],S=d(`split`),E=d(1),M=d(`root`),te=d(``),P=d(2),F=d(3),I=d(!1),re=d(!1),H=d(null),U=d(y([])),W=d(1),ie=d(1),ae=d(!1),G=d(!1),K=d(null),q=d(``),J=d(``),Y=d(``),se=d(``),Z=d(``),Q=d(!1),$=d(null),ce=d(``),le=d(!1),ue=d(null),de=d(``),fe=d(!1),pe=d(null);function he(e){switch(e){case`awaiting_new_set`:return`Standing up new set`;case`ready_to_destroy`:return`New set live - destroying old shares`;case`complete`:return`Complete`;default:return e}}let _e=d(null);function ve(e){return e.filter(e=>e.online).length}async function ye(e){s(_e,e,!0);try{await X.resplitStart(e)}finally{s(_e,null)}}let be=d(null);async function xe(e){s(be,e,!0);try{let t=await X.paperCards(e),n=URL.createObjectURL(new Blob([t],{type:`text/html`}));window.open(n,`_blank`,`noopener`),setTimeout(()=>URL.revokeObjectURL(n),6e4)}finally{s(be,null)}}async function Ce(e,t){await Se(e)&&(t(!0),setTimeout(()=>t(!1),1200))}async function we(){s(re,!0),s(H,null),s(U,[],!0);try{let e=k(M)===`root`?{kind:`root`}:{kind:`vault`,vid:k(te).trim()},t=await(k(S)===`split`?X.recoverySplit:X.recoveryResplit)(k(E),e,k(P),k(F),k(I));s(H,t.shares,!0),s(U,t.warnings,!0),ge({rsid:k(E),scope:e,m:k(P),n:k(F),createdAt:Date.now()})}finally{s(re,!1)}}async function Te(){s(G,!0),s(K,null);try{let e=await X.recoveryExtend(k(W),k(ie),k(ae));s(K,e.shares,!0);let t=o().recoverySets[String(k(W))];t&&ge({...t,n:t.n+k(ie)})}finally{s(G,!1)}}async function Ee(){s(Q,!0),s($,null);try{s($,await X.ceremonyOpen({subject:k(q).trim(),claimant_display:k(J).trim(),ceremony_enc:k(Y).trim(),new_node:k(se).trim(),reason:k(Z).trim()}),!0)}finally{s(Q,!1)}}async function De(){s(le,!0),s(ue,null);try{s(ue,await X.ceremonyApprove(k(ce).trim()),!0)}finally{s(le,!1)}}async function Oe(){s(fe,!0),s(pe,null);try{s(pe,(await X.ceremonyAbort(k(de).trim())).abort_hex,!0)}finally{s(fe,!1)}}let ke=d(y({})),Ae=d(!1),je=d(!1);var Me=Dt(),Ne=_(v(Me),4),Pe=e=>{var t=rt(),n=_(v(t),2),i=v(n);N(n),N(t),l(()=>r(i,`${f().share_health.recovery_sets_owned??``} recovery set(s) split · - ${f().share_health.shares_held??``} share(s) held here in trust for others`)),B(e,t)};h(Ne,e=>{f()&&e(Pe)});var Fe=_(Ne,2),Ie=e=>{var t=at(),n=_(c(t),4);C(n,5,()=>f().recovery_grants.minted,e=>e.rsid,(e,t)=>{var n=it(),i=v(n),a=v(i);N(i);var o=_(i,2),s=v(o);N(o);var c=_(o,2),u=v(c,!0);N(c),N(n),l(()=>{r(a,`rsid ${k(t).rsid??``}`),r(s,`${k(t).trustees.length??``} share(s)`),c.disabled=k(be)===k(t).rsid,r(u,k(be)===k(t).rsid?`Opening…`:`Print / export paper cards`)}),A(`click`,c,()=>xe(k(t).rsid)),B(e,n)}),N(n),B(e,t)};h(Fe,e=>{f()?.recovery_grants?.minted?.length&&e(Ie)});var Le=_(Fe,2),Re=e=>{var t=ct();C(_(c(t),4),1,()=>f().pending_resplits,e=>e.old_rsid,(e,t)=>{var n=st(),a=v(n),o=v(a),s=v(o);N(o),R(2),N(a);var c=_(a,2),u=v(c),d=v(u);N(u),R(),N(c);var f=_(c,4);C(f,21,()=>k(t).suggested,e=>e.user,(e,t)=>{var n=ot(),i=v(n),a=_(i,2),o=v(a);N(a);var s=_(a,2),c=v(s,!0);N(s),N(n),l(e=>{O(i,1,`dot ${k(t).online?`online`:`offline`}`,`svelte-imw02`),T(i,`title`,k(t).online?`online`:`offline`),r(o,`${e??``}…`),r(c,k(t).online?`online`:`offline`)},[()=>k(t).user.slice(0,12)]),B(e,n)}),N(f);var p=_(f,2),m=v(p),g=_(m),y=e=>{B(e,i(`will complete immediately once started.`))},b=ne(()=>ve(k(t).suggested)===k(t).suggested.length&&k(t).suggested.length>0),ee=e=>{B(e,i(`will complete progressively as offline trustees come online.`))};h(g,e=>{k(b)?e(y):e(ee,-1)}),N(p);var x=_(p,2),S=v(x,!0);N(x),N(n),l((e,n,i)=>{r(s,`rsid ${k(t).old_rsid??``}`),r(d,`${e??``}…`),O(p,1,n),r(m,`${i??``} / ${k(t).suggested.length??``} suggested trustee(s) online - `),x.disabled=k(_e)===k(t).old_rsid,r(S,k(_e)===k(t).old_rsid?`Starting…`:`Start re-split (use suggested set)`)},[()=>k(t).ex_trustee.slice(0,12),()=>D(ve(k(t).suggested)===k(t).suggested.length&&k(t).suggested.length>0?`healthy`:`muted`),()=>ve(k(t).suggested)]),A(`click`,x,()=>ye(k(t).old_rsid)),B(e,n)}),B(e,t)};h(Le,e=>{f()?.pending_resplits?.length&&e(Re)});var ze=_(Le,2),Be=e=>{var t=mt();C(_(c(t),4),1,()=>f().resplits,e=>e.old_rsid,(e,t)=>{var n=pt(),a=v(n),o=v(a),s=v(o);N(o);var c=_(o,2),u=v(c,!0);N(c),N(a);var d=_(a,2),f=v(d);N(d);var p=_(d,2),m=v(p),g=_(v(m),2),y=v(g),b=_(y),ee=e=>{B(e,lt())},x=e=>{B(e,ut())};h(b,e=>{k(t).new_set_live?e(ee):e(x,-1)}),N(g),N(m);var S=_(m,2),w=_(v(S),2),E=v(w),D=_(E),A=e=>{B(e,dt())};h(D,e=>{k(t).new_set_live||e(A)}),N(w),N(S),N(p);var j=_(p,4);C(j,21,()=>k(t).remaining,e=>e.node,(e,t)=>{var n=ft(),a=v(n),o=_(a,2),s=v(o);N(o);var c=_(o,2),u=v(c,!0);N(c);var d=_(c,2),f=v(d),p=e=>{B(e,i(`done`))},m=e=>{var n=i();l(()=>r(n,`online - ${k(t).role===`new`?`sending share`:`sending destroy`}`)),B(e,n)},g=e=>{B(e,i(`offline - queued`))};h(f,e=>{k(t).done?e(p):k(t).online?e(m,1):e(g,-1)}),N(d),N(n),l(e=>{O(a,1,`dot ${k(t).status??``}`,`svelte-imw02`),T(a,`title`,k(t).status),r(s,`${e??``}…`),O(c,1,`role ${k(t).role??``}`,`svelte-imw02`),r(u,k(t).role===`new`?`gets new share`:`gets destroy step`)},[()=>k(t).node.slice(0,12)]),B(e,n)}),N(j),N(n),l((e,n)=>{r(s,`rsid ${k(t).old_rsid??``} → ${k(t).new_rsid??``}`),O(c,1,`phase ${k(t).phase??``}`,`svelte-imw02`),r(u,e),r(f,`ex-trustee ${n??``}…`),r(y,`${k(t).new_attested??``} / ${k(t).new_total??``} `),r(E,`${k(t).old_destroyed??``} / ${k(t).old_total??``} `)},[()=>he(k(t).phase),()=>k(t).ex_trustee.slice(0,12)]),B(e,n)}),B(e,t)};h(ze,e=>{f()?.resplits?.length&&e(Be)});var Ve=_(ze,2),He=e=>{var t=gt(),n=_(c(t),2);C(n,5,()=>Object.values(o().recoverySets),e=>e.rsid,(e,t)=>{var n=ht(),i=v(n),a=v(i);N(i);var o=_(i,2),s=v(o,!0);N(o);var c=_(o,2),u=v(c);N(c),N(n),l(e=>{r(a,`rsid ${k(t).rsid??``}`),r(s,e),r(u,`${k(t).m??``}-of-${k(t).n??``}`)},[()=>k(t).scope.kind===`root`?`your root key`:`vault ${k(t).scope.vid.slice(0,10)}…`]),B(e,n)}),N(n),B(e,t)},Ue=ne(()=>Object.keys(o().recoverySets).length>0);h(Ve,e=>{k(Ue)&&e(He)});var We=_(Ve,4),Ge=v(We),Ke=v(Ge),qe=v(Ke);z(qe),qe.value=qe.__value=`split`,R(),N(Ke);var Je=_(Ke,2),Ye=v(Je);z(Ye),Ye.value=Ye.__value=`resplit`,R(),N(Je),N(Ge);var Xe=_(Ge,2),Ze=_(v(Xe),2);z(Ze),N(Xe);var Qe=_(Xe,2),$e=v(Qe),et=v($e);z(et),et.value=et.__value=`root`,R(),N($e);var tt=_($e,2),nt=v(tt);z(nt),nt.value=nt.__value=`vault`,R(),N(tt);var Ot=_(tt,2),kt=e=>{var t=_t();z(t),j(t,()=>k(te),e=>s(te,e)),B(e,t)};h(Ot,e=>{k(M)===`vault`&&e(kt)}),N(Qe);var At=_(Qe,2),jt=_(v(At),2);z(jt);var Mt=_(jt,4);z(Mt),N(At);var Nt=_(At,2),Pt=v(Nt),Ft=v(Pt);z(Ft),R(),N(Pt),N(Nt);var It=_(Nt,2),Lt=v(It,!0);N(It),N(We);var Rt=_(We,2),zt=e=>{var t=yt();C(t,20,()=>k(U),e=>e,(e,t)=>{var n=vt(),i=v(n,!0);N(n),l(()=>r(i,t)),B(e,n)}),N(t),B(e,t)};h(Rt,e=>{k(U).length&&e(zt)});var Bt=_(Rt,2),Vt=e=>{var t=xt();C(_(v(t),2),17,()=>k(H),V,(e,t,n)=>{var i=bt(),a=v(i),o=v(a,!0);N(a);var c=_(a,2),u=v(c,!0);N(c),N(i),l(()=>{r(o,k(t)),r(u,k(ke)[n]?`Copied`:`Copy`)}),A(`click`,c,()=>Ce(k(t),e=>s(ke,{...k(ke),[n]:e},!0))),B(e,i)}),N(t),B(e,t)};h(Bt,e=>{k(H)&&e(Vt)});var Ht=_(Bt,4),Ut=v(Ht),Wt=_(v(Ut),2);z(Wt);var Gt=_(Wt,4);z(Gt);var Kt=_(Gt,2),qt=v(Kt);z(qt),R(),N(Kt),N(Ut);var Jt=_(Ut,2),Yt=v(Jt,!0);N(Jt),N(Ht);var Xt=_(Ht,2),Zt=e=>{var t=Ct();C(t,21,()=>k(K),V,(e,t)=>{var n=St(),i=v(n),a=v(i,!0);N(i),N(n),l(()=>r(a,k(t))),B(e,n)}),N(t),B(e,t)};h(Xt,e=>{k(K)&&e(Zt)});var Qt=_(Xt,6),$t=v(Qt),en=_(v($t),4);z(en);var tn=_(en,4);z(tn);var nn=_(tn,4);z(nn);var rn=_(nn,4);z(rn);var an=_(rn,4);z(an);var on=_(an,2),sn=v(on,!0);N(on);var cn=_(on,2),ln=e=>{var t=wt(),n=c(t),i=v(n);N(n);var a=_(n,4),o=v(a),u=v(o,!0);N(o);var d=_(o,2),f=v(d,!0);N(d),N(a),l(e=>{r(i,`id ${e??``}… · fanned out to ${k($).fanout_reached??``} peer(s)`),r(u,k($).open_hex),r(f,k(Ae)?`Copied`:`Copy`)},[()=>k($).ceremony_id.slice(0,12)]),A(`click`,d,()=>Ce(k($).open_hex,e=>s(Ae,e,!0))),B(e,t)};h(cn,e=>{k($)&&e(ln)}),N($t);var un=_($t,2),dn=_(v(un),4);z(dn);var fn=_(dn,2),pn=v(fn,!0);N(fn);var mn=_(fn,2),hn=e=>{var t=Tt(),n=c(t),i=v(n);N(n);var a=_(n,2),o=v(a),u=v(o,!0);N(o);var d=_(o,2),f=v(d,!0);N(d),N(a),l(()=>{r(i,`Approval broadcast to ${k(ue).broadcast_reached??``} co-trustee(s).`),r(u,k(ue).approve_hex),r(f,k(je)?`Copied`:`Copy`)}),A(`click`,d,()=>Ce(k(ue).approve_hex,e=>s(je,e,!0))),B(e,t)};h(mn,e=>{k(ue)&&e(hn)}),N(un);var gn=_(un,2),_n=_(v(gn),4);z(_n);var vn=_(_n,2),yn=v(vn,!0);N(vn);var bn=_(vn,2),xn=e=>{var t=Et(),n=c(t),i=v(n,!0);N(n),R(2),l(()=>r(i,k(pe))),B(e,t)};h(bn,e=>{k(pe)&&e(xn)}),N(gn),N(Qt),N(Me),l(()=>{It.disabled=k(re),r(Lt,k(re)?`Splitting…`:k(S)===`split`?`Split key`:`Re-split key`),Jt.disabled=k(G),r(Yt,k(G)?`Issuing…`:`Issue new share(s)`),on.disabled=k(Q),r(sn,k(Q)?`Opening…`:`Open ceremony`),fn.disabled=k(le),r(pn,k(le)?`Recording…`:`Record approval`),vn.disabled=k(fe),r(yn,k(fe)?`Signing…`:`Abort as subject`)}),t(`submit`,We,e=>(e.preventDefault(),we())),w(b,[],qe,()=>k(S),e=>s(S,e)),w(b,[],Ye,()=>k(S),e=>s(S,e)),j(Ze,()=>k(E),e=>s(E,e)),w(x,[],et,()=>k(M),e=>s(M,e)),w(x,[],nt,()=>k(M),e=>s(M,e)),j(jt,()=>k(P),e=>s(P,e)),j(Mt,()=>k(F),e=>s(F,e)),L(Ft,()=>k(I),e=>s(I,e)),t(`submit`,Ht,e=>(e.preventDefault(),Te())),j(Wt,()=>k(W),e=>s(W,e)),j(Gt,()=>k(ie),e=>s(ie,e)),L(qt,()=>k(ae),e=>s(ae,e)),t(`submit`,$t,e=>(e.preventDefault(),Ee())),j(en,()=>k(q),e=>s(q,e)),j(tn,()=>k(J),e=>s(J,e)),j(nn,()=>k(Y),e=>s(Y,e)),j(rn,()=>k(se),e=>s(se,e)),j(an,()=>k(Z),e=>s(Z,e)),t(`submit`,un,e=>(e.preventDefault(),De())),j(dn,()=>k(ce),e=>s(ce,e)),t(`submit`,gn,e=>(e.preventDefault(),Oe())),j(_n,()=>k(de),e=>s(de,e)),B(n,Me),m(),g()}p([`click`]);var kt=a(``),At=a(`

              Send this to each person in the audience.

              `,1),jt=a(`
            • `),Mt=a(`

                `,1),Nt=a(`

                Shared files

                Share files from a vault

                A share is a snapshot of these files at the vault's current epoch. It cannot - be recalled once handed over - editing the files afterward only affects future shares, not - this one.

                Fetch a file someone shared with you

                `);function Pt(n,i){e(i,!0);let a=d(y([]));X.listVaults().then(e=>s(a,e.published,!0)).catch(()=>{});let o=d(``),u=d(``),f=d(``),p=d(!1),g=d(null),b=d(!1);async function ee(){if(!(!k(o)||!k(u).trim()||!k(f).trim())){s(p,!0),s(g,null);try{let e=k(u).split(` -`).map(e=>e.trim()).filter(Boolean),t=k(f).split(`,`).map(e=>e.trim()).filter(Boolean),n=await X.discloseFiles(k(o),e,t);s(g,n.grant_hex,!0)}finally{s(p,!1)}}}async function x(){k(g)&&await Se(k(g))&&(s(b,!0),setTimeout(()=>s(b,!1),1200))}let S=d(``),w=d(``),T=d(``),E=d(``),D=d(!1),O=d(null);async function M(){if(!(!k(S).trim()||!k(w).trim()||!k(E).trim())){s(D,!0),s(O,null);try{let e=k(T).split(`,`).map(e=>e.trim()).filter(Boolean),t=await X.fetchGrant(k(S).trim(),{node:k(w).trim(),addrs:e},k(E).trim());s(O,t.written,!0)}finally{s(D,!1)}}}var te=Nt(),ne=_(v(te),2),F=_(v(ne),4),I=_(v(F),2),L=v(I);L.value=L.__value=``,C(_(L),17,()=>k(a),e=>e.vid,(e,t)=>{var n=kt(),i=v(n);N(n);var a={};l(e=>{r(i,`${e??``}… (epoch ${k(t).epoch??``})`),a!==(a=k(t).vid)&&(n.value=(n.__value=k(t).vid)??``)},[()=>k(t).vid.slice(0,16)]),B(e,n)}),N(I);var V=_(I,4);re(V);var H=_(V,4);z(H);var U=_(H,2),W=v(U,!0);N(U),N(F);var ie=_(F,2),ae=e=>{var t=At(),n=c(t),i=v(n),a=v(i,!0);N(i);var o=_(i,2),s=v(o,!0);N(o),N(n),R(2),l(()=>{r(a,k(g)),r(s,k(b)?`Copied`:`Copy`)}),A(`click`,o,x),B(e,t)};h(ie,e=>{k(g)&&e(ae)}),N(ne);var G=_(ne,2),K=_(v(G),2),q=_(v(K),2);z(q);var J=_(q,4);z(J);var Y=_(J,4);z(Y);var oe=_(Y,4);z(oe);var se=_(oe,2),Z=v(se,!0);N(se),N(K);var Q=_(K,2),$=e=>{var t=Mt(),n=c(t),i=v(n);N(n);var a=_(n,2);C(a,20,()=>k(O),e=>e,(e,t)=>{var n=jt(),i=v(n,!0);N(n),l(()=>r(i,t)),B(e,n)}),N(a),l(()=>r(i,`Wrote ${k(O).length??``} file(s):`)),B(e,t)};h(Q,e=>{k(O)&&e($)}),N(G),N(te),l(()=>{U.disabled=k(p),r(W,k(p)?`Sharing…`:`Share files`),se.disabled=k(D),r(Z,k(D)?`Fetching…`:`Fetch files`)}),t(`submit`,F,e=>(e.preventDefault(),ee())),P(I,()=>k(o),e=>s(o,e)),j(V,()=>k(u),e=>s(u,e)),j(H,()=>k(f),e=>s(f,e)),t(`submit`,K,e=>(e.preventDefault(),M())),j(q,()=>k(S),e=>s(S,e)),j(J,()=>k(w),e=>s(w,e)),j(Y,()=>k(T),e=>s(T,e)),j(oe,()=>k(E),e=>s(E,e)),B(n,te),m()}p([`click`]);var Ft=a(` `),It=a(`
                Carapace
                `);function Lt(t,i){e(i,!0);let a=()=>ee(se,`$live`,c),[c,f]=u(),p={"":`Overview`,vaults:`Vaults`,friends:`Friends`,recovery:`Recovery`,shared:`Shared files`};function g(){let e=typeof location<`u`?location.hash.replace(/^#\/?/,``):``;return e in p?e:``}let b=d(y(g()));function S(){s(b,g(),!0)}let w=d(null);function E(e){e?document.documentElement.setAttribute(`data-theme`,e):document.documentElement.removeAttribute(`data-theme`)}function D(){let e=typeof matchMedia<`u`&&matchMedia(`(prefers-color-scheme: light)`).matches,t=k(w)?k(w)===`light`:e;s(w,t?`dark`:`light`,!0),localStorage.setItem(`carapace-theme`,k(w)),E(k(w))}F(()=>{window.addEventListener(`hashchange`,S);let e=localStorage.getItem(`carapace-theme`);return(e===`dark`||e===`light`)&&(s(w,e,!0),E(k(w))),ce(),()=>window.removeEventListener(`hashchange`,S)}),te(()=>{le()});var j=It();x(`1uha8ag`,e=>{o(()=>{n.title=`Carapace`})});var M=v(j),P=_(v(M),2);C(P,21,()=>Object.entries(p),([e,t])=>e,(e,t)=>{var n=ne(()=>I(k(t),2));let i=()=>k(n)[0],a=()=>k(n)[1];var o=Ft();let s;var c=v(o,!0);N(o),l(()=>{T(o,`href`,i()?`#/${i()}`:`#/`),s=O(o,1,`svelte-1uha8ag`,null,s,{active:k(b)===i()}),r(c,a())}),B(e,o)}),N(P);var L=_(P,2),R=v(L);let z;var V=_(R,2),re=v(V,!0);N(V),N(L),N(M);var H=_(M,2),U=v(H);de(U,{});var W=_(U,2),ie=e=>{Ue(e,{})},ae=e=>{nt(e,{})},G=e=>{Ot(e,{})},K=e=>{Pt(e,{})},q=e=>{Ae(e,{})};h(W,e=>{k(b)===`vaults`?e(ie):k(b)===`friends`?e(ae,1):k(b)===`recovery`?e(G,2):k(b)===`shared`?e(K,3):e(q,-1)}),N(H),N(j),l(()=>{z=O(R,1,`live-dot svelte-1uha8ag`,null,z,{live:a()}),T(R,`title`,a()?`Live updates connected`:`Reconnecting…`),r(re,k(w)===`light`?`Molt (light)`:`Dark`)}),A(`click`,V,D),B(t,j),m(),f()}p([`click`]);export{Lt as component}; \ No newline at end of file diff --git a/crates/carapace-api/static/_app/version.json b/crates/carapace-api/static/_app/version.json index 9b24b99..8309af1 100644 --- a/crates/carapace-api/static/_app/version.json +++ b/crates/carapace-api/static/_app/version.json @@ -1 +1 @@ -{"version":"1784071871063"} \ No newline at end of file +{"version":"0.0.1"} \ No newline at end of file diff --git a/crates/carapace-api/static/claimant.css b/crates/carapace-api/static/claimant.css new file mode 100644 index 0000000..c7b9a2c --- /dev/null +++ b/crates/carapace-api/static/claimant.css @@ -0,0 +1,14 @@ +:root { color-scheme: dark; font-family: system-ui, sans-serif; background: #151719; color: #f3eee5; } +body { margin: 0; } +main { max-width: 52rem; margin: auto; padding: 1.25rem; } +header, section { border: 1px solid #53504a; border-radius: .75rem; padding: 1.25rem; margin-bottom: 1rem; background: #202326; } +.eyebrow { color: #d9a86c; text-transform: uppercase; letter-spacing: .08em; } +label { display: block; margin: 1rem 0 .4rem; } +textarea { box-sizing: border-box; width: 100%; margin-top: .4rem; padding: .7rem; color: inherit; background: #111315; border: 1px solid #77736b; border-radius: .35rem; font: .9rem ui-monospace, monospace; } +button { margin-top: .35rem; padding: .65rem .9rem; color: #151719; background: #d9a86c; border: 0; border-radius: .35rem; font-weight: 700; cursor: pointer; } +button:focus-visible, textarea:focus-visible { outline: 3px solid #6fc7bd; outline-offset: 2px; } +button:disabled { opacity: .55; cursor: wait; } +#error { margin: 1rem 0; padding: 1rem; border: 2px solid #e68172; } +#notice, #progress { min-height: 1.4em; } +@media (max-width: 40rem) { main { padding: .65rem; } header, section { padding: .9rem; } } +@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; } } diff --git a/crates/carapace-api/static/claimant.html b/crates/carapace-api/static/claimant.html new file mode 100644 index 0000000..f5fdc8a --- /dev/null +++ b/crates/carapace-api/static/claimant.html @@ -0,0 +1,80 @@ + + + + + + + Carapace identity recovery + + + + +
                +
                +

                Claimant mode

                +

                Recover this Carapace identity

                +

                This isolated server cannot open vaults or use normal daemon actions.

                +
                + +
                + + +
                +

                1. Give the public session values to a sponsor

                +

                Verify the sponsor through a separate trusted channel. This package contains public values only.

                + + +
                + Advanced manual values + + +
                +
                + +
                +

                2. Import the signed open and trustee endpoints

                +

                The signed open must contain the exact public values above. Add one trustee user per line.

                +
                + + + +
                + Advanced manual values + + + +
                + + +
                +

                +
                + + +
                + + diff --git a/crates/carapace-api/static/claimant.js b/crates/carapace-api/static/claimant.js new file mode 100644 index 0000000..6fee8b1 --- /dev/null +++ b/crates/carapace-api/static/claimant.js @@ -0,0 +1,138 @@ +const token = document.querySelector('meta[name="carapace-claimant-token"]')?.content ?? ''; +const errorBox = document.querySelector('#error'); +const notice = document.querySelector('#notice'); +const progress = document.querySelector('#progress'); + +function showError(message) { + errorBox.textContent = message; + errorBox.hidden = false; +} + +async function request(path, init) { + const response = await fetch(path, { + ...init, + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...init?.headers } + }); + const body = await response.json(); + if (!response.ok) throw new Error(typeof body.error === 'string' ? body.error : 'The claimant request failed.'); + return body; +} + +async function copyPublicValue(id) { + try { + await navigator.clipboard.writeText(document.querySelector(`#${id}`).value); + notice.textContent = 'Copied.'; + } catch { + showError('Could not copy. Select and copy the public value manually.'); + } +} + +function lines(id) { + return document.querySelector(`#${id}`).value.split(/\r?\n/).map((value) => value.trim()).filter(Boolean); +} + +function trusteeRows() { + return lines('trustees').map((line) => { + const [node, rawAddresses = ''] = line.split('|', 2); + return { node: node.trim(), addrs: rawAddresses.split(',').map((value) => value.trim()).filter(Boolean) }; + }); +} + +let announceRefs = []; + +function importSponsorPackage() { + const value = JSON.parse(document.querySelector('#sponsor-package').value); + if (value.type !== 'carapace.sponsor-ceremony' || value.version !== 1 || + typeof value.open_hex !== 'string' || !Array.isArray(value.roster) || !Array.isArray(value.trustees)) { + throw new Error('The sponsor ceremony package type, version, or fields are invalid.'); + } + document.querySelector('#open-hex').value = value.open_hex; + document.querySelector('#roster').value = value.roster.join('\n'); + document.querySelector('#trustees').value = value.trustees.map((trustee) => + `${trustee.node}|${Array.isArray(trustee.addrs) ? trustee.addrs.join(',') : ''}`).join('\n'); + announceRefs = Array.isArray(value.announce_refs) ? value.announce_refs : []; +} + +let confirmedSubject = ''; + +async function previewOpen() { + errorBox.hidden = true; + importSponsorPackage(); + const preview = await request('/api/claimant/preview', { + method: 'POST', body: JSON.stringify({ open_hex: document.querySelector('#open-hex').value.trim() }) + }); + if (!preview.session_bound) throw new Error('The signed open is not bound to this session.'); + confirmedSubject = preview.subject; + document.querySelector('#subject-id').textContent = preview.subject; + document.querySelector('#subject-confirmation').hidden = false; + document.querySelector('#confirm-subject').checked = false; + document.querySelector('#recover').disabled = true; +} + +async function loadSession() { + const session = await request('/api/claimant/status'); + if (session.phase === 'activation_complete') { + document.querySelector('#restart').hidden = false; + document.querySelector('#complete-form').hidden = true; + return; + } + document.querySelector('#ceremony-enc').value = session.ceremony_enc; + document.querySelector('#new-node').value = session.new_node; + document.querySelector('#handoff').value = session.handoff; +} + +document.querySelectorAll('[data-copy]').forEach((button) => { + button.addEventListener('click', () => copyPublicValue(button.dataset.copy)); +}); + +document.querySelector('#preview-open').addEventListener('click', () => { + previewOpen().catch((error) => showError(error instanceof Error ? error.message : 'Could not verify the signed open.')); +}); + +document.querySelector('#sponsor-package').addEventListener('input', () => { + confirmedSubject = ''; + announceRefs = []; + document.querySelector('#subject-confirmation').hidden = true; + document.querySelector('#recover').disabled = true; +}); + +document.querySelector('#cancel').addEventListener('click', async () => { + errorBox.hidden = true; + await request('/api/claimant/cancel', { method: 'POST', body: '{}' }); + for (const id of ['sponsor-package', 'open-hex', 'roster', 'trustees']) document.querySelector(`#${id}`).value = ''; + confirmedSubject = ''; + announceRefs = []; + document.querySelector('#subject-confirmation').hidden = true; + document.querySelector('#recover').disabled = true; + progress.textContent = 'The old session keys were cleared. A fresh retry session is ready.'; + await loadSession(); +}); + +document.querySelector('#confirm-subject').addEventListener('change', (event) => { + document.querySelector('#recover').disabled = !(event.target.checked && confirmedSubject); +}); + +document.querySelector('#complete-form').addEventListener('submit', async (event) => { + event.preventDefault(); + errorBox.hidden = true; + const button = document.querySelector('#recover'); + button.disabled = true; + progress.textContent = 'Contacting trustees. Keep this window open.'; + try { + const result = await request('/api/claimant/complete', { + method: 'POST', + body: JSON.stringify({ open_hex: document.querySelector('#open-hex').value.trim(), confirmed_subject: confirmedSubject, roster: lines('roster'), trustees: trusteeRows(), announce_refs: announceRefs }) + }); + if (!result.restart_required) throw new Error('Activation did not request a safe restart.'); + progress.textContent = 'Recovered identity activation is complete.'; + document.querySelector('#restart').hidden = false; + document.querySelector('#complete-form').hidden = true; + document.querySelector('#restart').focus?.(); + } catch (error) { + showError(error instanceof Error ? error.message : 'Recovery did not complete.'); + progress.textContent = 'Recovery stopped without activation.'; + button.disabled = false; + } +}); + +loadSession().catch(() => showError('Could not load the claimant session. Reload this local page.')); diff --git a/crates/carapace-api/static/index.html b/crates/carapace-api/static/index.html index c2e4e0d..2c124b1 100644 --- a/crates/carapace-api/static/index.html +++ b/crates/carapace-api/static/index.html @@ -4,29 +4,29 @@ - - + + - + - + - +

                Recovery & trustees

                Split your key into pieces so a group of trustees can rebuild it if you lose access. - Each share below is a bearer secret: send one to each trustee yourself - the daemon - doesn't track who you gave it to. + The normal flow sends a signed recovery grant to each selected friend and tracks delivery. + The advanced manual flow shows bearer shares that you must protect and deliver yourself.

                {#if $status} @@ -179,6 +240,55 @@
                {/if} +
                + Restore retained vaults after identity recovery +

                Discovery-dependent operation · it is safe to retry after a network failure

                +

                Use the public restart handoff saved by claimant activation. Carapace contacts the verified trustee hints and accepts only the maximum announced epoch for each vault.

                +
                (event.preventDefault(), runRestartRestore())}> + + + +
                +
                {#if restartRestoring}

                Restore is in progress.

                {:else if restartResult}

                Restored {restartResult.restored} vault(s) from {restartResult.refs} maximum-epoch reference(s).

                {/if}
                +
                + + {#if $status?.ceremonies?.length} +

                Active recovery ceremonies

                +

                + Review the claimant and reason out of band before you approve a ceremony. Abort any ceremony + against your identity that you did not start. +

                +
                + {#each $status.ceremonies as ceremony (ceremony.ceremony_id)} +
                +
                +
                + {ceremony.claimant_display || 'Unnamed claimant'} +
                {ceremony.ceremony_id}
                +
                + {ceremony.phase} +
                + {#if ceremony.alarm} + + {/if} +
                +
                Reason
                {ceremony.reason || 'No reason supplied'}
                +
                Approvals
                {ceremony.approvals} / {ceremony.threshold}
                +
                Sponsor
                {ceremony.sponsor}
                +
                +
                + {#if ceremony.trustee && !ceremony.approved} + + {/if} + {#if ceremony.is_self_subject && !ceremony.takeover} + + {/if} +
                +
                + {/each} +
                + {/if} + {#if $status?.recovery_grants?.minted?.length}

                Paper cards (offline backstop)

                @@ -313,14 +423,16 @@ {/each} {/if} - {#if Object.keys($notes.recoverySets).length > 0} -

                Recovery sets split from this browser

                + {#if $status?.share_health.sets.length} +

                Authoritative recovery sets

                - {#each Object.values($notes.recoverySets) as rs (rs.rsid)} + {#each $status.share_health.sets as rs (rs.rsid)}
                rsid {rs.rsid} {rs.scope.kind === 'root' ? 'your root key' : `vault ${rs.scope.vid.slice(0, 10)}…`} - {rs.m}-of-{rs.n} + {rs.threshold}-of-{rs.issued} + {rs.trustees.filter((trustee) => trustee.delivered).length}/{rs.trustees.length} trustee grants delivered + {#if rs.warnings.length}{rs.warnings.join(', ')}{/if}
                {/each}
                @@ -336,10 +448,15 @@ Re-split (raise M or replace trustees) -
                - - -
                + {#if mode === 'split'} +

                Carapace will create recovery set {nextRsid}.

                +
                Advanced recovery-set id{#if manualSetId}{/if}
                + {:else} + + + {/if}
                {#if scopeKind === 'vault'} - + {/if} -
                -
                - - - - +
                +
                + Share delivery + + +
                + {#if deliveryMode === 'friends'} +
                + Select trustees ({selectedTrustees.length} selected) + {#if !$status?.friends.list.length} +

                Add friends before you create recovery protection.

                + {:else} +
                + {#each $status.friends.list as friend (friend)} + + {/each} +
                + {/if} +
                + {/if} +
                + + + {#if deliveryMode === 'manual'} + + + {:else} + of {selectedTrustees.length} selected + {/if}
                - - + - {#if splitWarnings.length} + {#if deliveryResult} +
                0} style="margin-top: 1rem"> +

                Verified share delivery

                +

                {deliveryResult.delivered.length} trustee(s) confirmed that they stored a signed recovery grant.

                + {#if deliveryResult.undelivered.length} +

                Could not reach {deliveryResult.undelivered.length} trustee(s). Carapace will retry delivery during maintenance.

                +
                  {#each deliveryResult.undelivered as trustee (trustee)}
                • {trustee}
                • {/each}
                + {:else} +

                All selected trustees received their shares.

                + {/if} +
                + {/if} + + {#if splitWarnings.length}
                {#each splitWarnings as w (w)}

                {w}

                {/each}
                @@ -386,10 +549,11 @@ {/if}

                Add a trustee (extend)

                +

                Peer-dependent operation · offline delivery stays queued and is safe to retry

                (e.preventDefault(), runExtend())}>
                - - + + @@ -418,23 +582,50 @@
                (e.preventDefault(), runOpen())}>

                Open (sponsor)

                - - +

                + Use the recovery request from the claimant. Confirm their identity and new device through + a separate trusted channel before you open the ceremony. +

                + + + {#if !$status?.recovery_grants.held.length} +

                + This device does not hold a recovery grant. It cannot sponsor a ceremony. +

                + {/if} - - - - + + + + {#if handoffError}{/if} +
                + Advanced manual values + + + + +
                - + {#if openResult}

                id {openResult.ceremony_id.slice(0, 12)}… · fanned out to {openResult.fanout_reached} peer(s)

                -
                Signed open - hand to the claimant
                +
                Verified sponsor ceremony package - hand to the claimant
                @@ -457,11 +648,15 @@ {/if} -
                (e.preventDefault(), runAbort())}> + (e.preventDefault(), runAbort())}>

                Abort

                - - + + + {#if abortHex}

                Send this abort to your trustees.

                @@ -487,6 +682,31 @@ color: var(--muted); } + fieldset { + border: 1px solid var(--hairline); + border-radius: var(--radius); + margin: 0 0 0.75rem; + padding: 0.75rem; + } + + fieldset > label, + .trustee-list label { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0.35rem 0; + } + + legend { + font-weight: 600; + } + + .trustee-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 0 1rem; + } + .list { display: flex; flex-direction: column; @@ -530,6 +750,70 @@ margin-bottom: 0.5rem; } + .grid select { + width: 100%; + margin-bottom: 0.5rem; + } + + .ceremony.alarm { + border-color: var(--coral); + } + + .alarm-text { + color: var(--coral); + font-weight: 600; + } + + dl { + display: grid; + gap: 0.5rem; + } + + dl div { + display: grid; + grid-template-columns: minmax(6rem, 0.25fr) 1fr; + gap: 0.75rem; + } + + dt { + color: var(--muted); + } + + dd { + margin: 0; + min-width: 0; + overflow-wrap: anywhere; + } + + button.danger { + border-color: var(--coral); + color: var(--coral); + } + + .confirm-action { + display: flex !important; + align-items: center; + gap: 0.5rem; + } + + .confirm-action input { + width: auto; + margin: 0; + } + + @media (max-width: 640px) { + .share-row, + .set-row { + align-items: stretch; + flex-direction: column; + } + + dl div { + grid-template-columns: 1fr; + gap: 0.1rem; + } + } + .resplit-head { display: flex; justify-content: space-between; diff --git a/gui/src/lib/components/SharedView.svelte b/gui/src/lib/components/SharedView.svelte index 1203ac5..c3bb9e1 100644 --- a/gui/src/lib/components/SharedView.svelte +++ b/gui/src/lib/components/SharedView.svelte @@ -1,6 +1,7 @@ + + +
                +
                +

                Claimant mode

                +

                Recover this Carapace identity

                +

                This isolated server cannot open vaults or use normal daemon actions.

                +
                + +
                + + +
                +

                1. Give the public session values to a sponsor

                +

                Verify the sponsor through a separate trusted channel. This package contains public values only.

                + + +
                + Advanced manual values + + +
                +
                + +
                +

                2. Import the signed open and trustee endpoints

                +

                The signed open must contain the exact public values above. Add one trustee user per line.

                + + + + +
                + Advanced manual values + + + +
                + + + +

                +
                + + +
                + + diff --git a/gui/static/claimant.js b/gui/static/claimant.js new file mode 100644 index 0000000..6fee8b1 --- /dev/null +++ b/gui/static/claimant.js @@ -0,0 +1,138 @@ +const token = document.querySelector('meta[name="carapace-claimant-token"]')?.content ?? ''; +const errorBox = document.querySelector('#error'); +const notice = document.querySelector('#notice'); +const progress = document.querySelector('#progress'); + +function showError(message) { + errorBox.textContent = message; + errorBox.hidden = false; +} + +async function request(path, init) { + const response = await fetch(path, { + ...init, + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...init?.headers } + }); + const body = await response.json(); + if (!response.ok) throw new Error(typeof body.error === 'string' ? body.error : 'The claimant request failed.'); + return body; +} + +async function copyPublicValue(id) { + try { + await navigator.clipboard.writeText(document.querySelector(`#${id}`).value); + notice.textContent = 'Copied.'; + } catch { + showError('Could not copy. Select and copy the public value manually.'); + } +} + +function lines(id) { + return document.querySelector(`#${id}`).value.split(/\r?\n/).map((value) => value.trim()).filter(Boolean); +} + +function trusteeRows() { + return lines('trustees').map((line) => { + const [node, rawAddresses = ''] = line.split('|', 2); + return { node: node.trim(), addrs: rawAddresses.split(',').map((value) => value.trim()).filter(Boolean) }; + }); +} + +let announceRefs = []; + +function importSponsorPackage() { + const value = JSON.parse(document.querySelector('#sponsor-package').value); + if (value.type !== 'carapace.sponsor-ceremony' || value.version !== 1 || + typeof value.open_hex !== 'string' || !Array.isArray(value.roster) || !Array.isArray(value.trustees)) { + throw new Error('The sponsor ceremony package type, version, or fields are invalid.'); + } + document.querySelector('#open-hex').value = value.open_hex; + document.querySelector('#roster').value = value.roster.join('\n'); + document.querySelector('#trustees').value = value.trustees.map((trustee) => + `${trustee.node}|${Array.isArray(trustee.addrs) ? trustee.addrs.join(',') : ''}`).join('\n'); + announceRefs = Array.isArray(value.announce_refs) ? value.announce_refs : []; +} + +let confirmedSubject = ''; + +async function previewOpen() { + errorBox.hidden = true; + importSponsorPackage(); + const preview = await request('/api/claimant/preview', { + method: 'POST', body: JSON.stringify({ open_hex: document.querySelector('#open-hex').value.trim() }) + }); + if (!preview.session_bound) throw new Error('The signed open is not bound to this session.'); + confirmedSubject = preview.subject; + document.querySelector('#subject-id').textContent = preview.subject; + document.querySelector('#subject-confirmation').hidden = false; + document.querySelector('#confirm-subject').checked = false; + document.querySelector('#recover').disabled = true; +} + +async function loadSession() { + const session = await request('/api/claimant/status'); + if (session.phase === 'activation_complete') { + document.querySelector('#restart').hidden = false; + document.querySelector('#complete-form').hidden = true; + return; + } + document.querySelector('#ceremony-enc').value = session.ceremony_enc; + document.querySelector('#new-node').value = session.new_node; + document.querySelector('#handoff').value = session.handoff; +} + +document.querySelectorAll('[data-copy]').forEach((button) => { + button.addEventListener('click', () => copyPublicValue(button.dataset.copy)); +}); + +document.querySelector('#preview-open').addEventListener('click', () => { + previewOpen().catch((error) => showError(error instanceof Error ? error.message : 'Could not verify the signed open.')); +}); + +document.querySelector('#sponsor-package').addEventListener('input', () => { + confirmedSubject = ''; + announceRefs = []; + document.querySelector('#subject-confirmation').hidden = true; + document.querySelector('#recover').disabled = true; +}); + +document.querySelector('#cancel').addEventListener('click', async () => { + errorBox.hidden = true; + await request('/api/claimant/cancel', { method: 'POST', body: '{}' }); + for (const id of ['sponsor-package', 'open-hex', 'roster', 'trustees']) document.querySelector(`#${id}`).value = ''; + confirmedSubject = ''; + announceRefs = []; + document.querySelector('#subject-confirmation').hidden = true; + document.querySelector('#recover').disabled = true; + progress.textContent = 'The old session keys were cleared. A fresh retry session is ready.'; + await loadSession(); +}); + +document.querySelector('#confirm-subject').addEventListener('change', (event) => { + document.querySelector('#recover').disabled = !(event.target.checked && confirmedSubject); +}); + +document.querySelector('#complete-form').addEventListener('submit', async (event) => { + event.preventDefault(); + errorBox.hidden = true; + const button = document.querySelector('#recover'); + button.disabled = true; + progress.textContent = 'Contacting trustees. Keep this window open.'; + try { + const result = await request('/api/claimant/complete', { + method: 'POST', + body: JSON.stringify({ open_hex: document.querySelector('#open-hex').value.trim(), confirmed_subject: confirmedSubject, roster: lines('roster'), trustees: trusteeRows(), announce_refs: announceRefs }) + }); + if (!result.restart_required) throw new Error('Activation did not request a safe restart.'); + progress.textContent = 'Recovered identity activation is complete.'; + document.querySelector('#restart').hidden = false; + document.querySelector('#complete-form').hidden = true; + document.querySelector('#restart').focus?.(); + } catch (error) { + showError(error instanceof Error ? error.message : 'Recovery did not complete.'); + progress.textContent = 'Recovery stopped without activation.'; + button.disabled = false; + } +}); + +loadSession().catch(() => showError('Could not load the claimant session. Reload this local page.')); diff --git a/gui/tests/browser/ui.spec.ts b/gui/tests/browser/ui.spec.ts new file mode 100644 index 0000000..b418e00 --- /dev/null +++ b/gui/tests/browser/ui.spec.ts @@ -0,0 +1,119 @@ +import AxeBuilder from '@axe-core/playwright'; +import { expect, test, type Page } from '@playwright/test'; + +const user = 'a'.repeat(64); +const status = { + node_id: 'b'.repeat(64), + addr: ['127.0.0.1:9999'], + friends: { count: 1, list: [user], grants: [{ user, grant_bytes: 1073741824 }] }, + peers: [{ user, display: 'Alex', node: 'b'.repeat(64), addrs: ['127.0.0.1:9999'] }], + vaults: { published: [], held_replicas: [] }, + share_health: { recovery_sets_owned: 0, shares_held: 0, sets: [], recovery: [] }, + recovery_grants: { minted: [], held: [] }, + ceremonies: [], + resplits: [], + pending_resplits: [], + reachability: 'local', + relay_networks: 0, + relay_diversity_warning: false +}; + +async function mockDaemon(page: Page) { + await page.route('**/api/**', async (route) => { + const path = new URL(route.request().url()).pathname; + if (path === '/api/status') return route.fulfill({ json: status }); + if (path === '/api/friends' && route.request().method() === 'GET') { + return route.fulfill({ json: { count: 1, list: [user] } }); + } + if (path === `/api/friends/${user}/unfriend`) { + return route.fulfill({ json: { was_friend: true, resplit_triggered: false, recovery_set_ids: [] } }); + } + return route.fulfill({ status: 404, json: { code: 'not_found', error: 'not found' } }); + }); +} + +test.beforeEach(async ({ page }) => mockDaemon(page)); + +test('keyboard navigation, destructive confirmation, and accessibility work', async ({ page }) => { + await page.goto('/#/friends'); + await expect(page.getByRole('heading', { name: 'Friends', exact: true })).toBeVisible(); + await page.keyboard.press('Tab'); + await expect(page.locator(':focus')).toBeVisible(); + await page.getByRole('button', { name: 'Unfriend' }).click(); + await expect(page.getByRole('button', { name: 'Confirm unfriend' })).toBeVisible(); + await page.getByRole('button', { name: 'Cancel' }).click(); + await expect(page.getByRole('button', { name: 'Confirm unfriend' })).toHaveCount(0); + const results = await new AxeBuilder({ page }).analyze(); + expect(results.violations).toEqual([]); +}); + +test('the narrow viewport has no horizontal overflow and keeps focus visible', async ({ page }) => { + await page.goto('/#/recovery'); + const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); + expect(overflow).toBeLessThanOrEqual(1); + const focusTarget = page.getByRole('link', { name: 'Recovery' }); + await focusTarget.focus(); + await expect(focusTarget).toBeFocused(); + const focusBox = await focusTarget.boundingBox(); + expect(focusBox).not.toBeNull(); + expect(focusBox!.x).toBeGreaterThanOrEqual(0); + expect(focusBox!.x + focusBox!.width).toBeLessThanOrEqual(await page.evaluate(() => innerWidth)); +}); + +test('changing operation status is announced in a live region', async ({ page }) => { + await page.route('**/api/sync', async (route) => { + await new Promise((resolve) => setTimeout(resolve, 150)); + await route.fulfill({ json: { restored: [] } }); + }); + await page.goto('/#/vaults'); + await page.locator('#sync-peer').selectOption('b'.repeat(64)); + await page.locator('#sync-out').fill('/tmp/restore'); + await page.getByRole('button', { name: 'Sync and restore' }).click(); + const live = page.locator('[aria-live="polite"]').filter({ hasText: 'Sync' }); + await expect(live).toContainText('Sync is in progress.'); + await expect(live).toContainText('There were no new vault versions to restore.'); +}); + +test('claimant workflow verifies the subject and moves focus after activation', async ({ page }, testInfo) => { + test.skip(testInfo.project.name === 'mobile-chromium', 'The separate responsive test covers the mobile viewport.'); + await page.route('**/api/claimant/**', async (route) => { + const path = new URL(route.request().url()).pathname; + if (path.endsWith('/status')) return route.fulfill({ json: { phase: 'ready', ceremony_enc: 'enc', new_node: 'node', handoff: 'handoff' } }); + if (path.endsWith('/preview')) return route.fulfill({ json: { session_bound: true, subject: user } }); + return route.fulfill({ json: { restart_required: true } }); + }); + await page.goto('/claimant.html'); + await page.locator('#sponsor-package').fill(JSON.stringify({ + type: 'carapace.sponsor-ceremony', version: 1, open_hex: '00', roster: [user], + trustees: [{ node: 'b'.repeat(64), addrs: ['127.0.0.1:1'] }] + })); + await page.getByRole('button', { name: 'Import package and show subject' }).click(); + await expect(page.locator('#subject-id')).toHaveText(user); + await page.locator('#confirm-subject').check(); + await page.getByRole('button', { name: 'Collect approvals and activate' }).click(); + await expect(page.locator('#restart')).toBeVisible(); + await expect(page.locator('#restart')).toBeFocused(); +}); + +test('claimant cancellation clears inputs and starts a fresh retry session', async ({ page }, testInfo) => { + test.skip(testInfo.project.name === 'mobile-chromium', 'The responsive test covers the mobile viewport.'); + await page.route('**/api/claimant/status', (route) => route.fulfill({ json: { phase: 'ready', ceremony_enc: 'enc', new_node: 'node', handoff: 'handoff' } })); + await page.route('**/api/claimant/cancel', (route) => route.fulfill({ json: { cancelled: true, retry_ready: true } })); + await page.goto('/claimant.html'); + await page.locator('#sponsor-package').fill('sensitive attempt data'); + await page.getByRole('button', { name: 'Cancel and clear this attempt' }).click(); + await expect(page.locator('#sponsor-package')).toHaveValue(''); + await expect(page.locator('#progress')).toContainText('fresh retry session is ready'); +}); + +test('claimant clipboard success and failure give truthful feedback', async ({ page, context }, testInfo) => { + test.skip(testInfo.project.name === 'mobile-chromium', 'Clipboard permission emulation runs in desktop Chromium.'); + await page.route('**/api/claimant/status', (route) => route.fulfill({ json: { phase: 'ready', ceremony_enc: 'enc', new_node: 'node', handoff: 'handoff' } })); + await context.grantPermissions(['clipboard-read', 'clipboard-write']); + await page.goto('/claimant.html'); + await page.getByRole('button', { name: 'Copy handoff package' }).click(); + await expect(page.locator('#notice')).toHaveText('Copied.'); + await page.evaluate(() => Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: () => Promise.reject(new Error('denied')) } })); + await page.getByRole('button', { name: 'Copy handoff package' }).click(); + await expect(page.getByRole('alert')).toContainText('Select and copy'); +}); diff --git a/gui/tests/error-banner.component.test.ts b/gui/tests/error-banner.component.test.ts new file mode 100644 index 0000000..02f2653 --- /dev/null +++ b/gui/tests/error-banner.component.test.ts @@ -0,0 +1,14 @@ +import { fireEvent, render, screen } from '@testing-library/svelte'; +import { describe, expect, it } from 'vitest'; +import ErrorBanner from '$lib/components/ErrorBanner.svelte'; +import { reportError } from '$lib/errors'; + +describe('ErrorBanner', () => { + it('renders an accessible alert and dismisses it', async () => { + reportError('The request failed safely.'); + render(ErrorBanner); + expect(screen.getByRole('alert')).toHaveTextContent('The request failed safely.'); + await fireEvent.click(screen.getByRole('button', { name: 'Dismiss' })); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + }); +}); diff --git a/gui/tests/fixtures/status-contract.json b/gui/tests/fixtures/status-contract.json new file mode 100644 index 0000000..a082b5b --- /dev/null +++ b/gui/tests/fixtures/status-contract.json @@ -0,0 +1,17 @@ +{ + "node_id": "", + "addr": [], + "relay_url": null, + "friends": { "count": 0, "list": [], "grants": [] }, + "peers": [], + "vaults": { "published": [], "held_replicas": [] }, + "share_health": { "recovery_sets_owned": 0, "shares_held": 0, "sets": [], "recovery": [] }, + "recovery_grants": { "minted": [], "held": [] }, + "ceremonies": [], + "resplits": [], + "pending_resplits": [], + "reachability": "direct", + "relay_networks": 0, + "relay_diversity_warning": true, + "por_latency_anomaly_count": 0 +} diff --git a/gui/tests/runtime.component.test.ts b/gui/tests/runtime.component.test.ts new file mode 100644 index 0000000..4d8f84f --- /dev/null +++ b/gui/tests/runtime.component.test.ts @@ -0,0 +1,50 @@ +import { get } from 'svelte/store'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { lastError } from '$lib/errors'; +import { copyToClipboard } from '$lib/format'; + +describe('clipboard feedback', () => { + beforeEach(() => lastError.set(null)); + + it('reports success only after the clipboard accepts the value', async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); + expect(await copyToClipboard('public-value')).toBe(true); + expect(writeText).toHaveBeenCalledWith('public-value'); + expect(get(lastError)).toBeNull(); + }); + + it('shows safe manual-copy guidance after clipboard rejection', async () => { + Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: vi.fn().mockRejectedValue(new Error('denied')) } }); + expect(await copyToClipboard('public-value')).toBe(false); + expect(get(lastError)).toMatch(/Select and copy the value manually/); + }); +}); + +describe('status WebSocket lifecycle', () => { + beforeEach(() => { vi.useFakeTimers(); vi.resetModules(); }); + afterEach(() => vi.useRealTimers()); + + it('retries an unexpected close and does not retry a deliberate stop', async () => { + const sockets: FakeSocket[] = []; + class FakeSocket { + onopen: (() => void) | null = null; + onmessage: ((event: MessageEvent) => void) | null = null; + onclose: (() => void) | null = null; + onerror: (() => void) | null = null; + constructor(public url: string) { sockets.push(this); } + close() { this.onclose?.(); } + } + vi.stubGlobal('WebSocket', FakeSocket); + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) })); + const feed = await import('$lib/statusStore'); + feed.startStatusFeed(); + expect(sockets).toHaveLength(1); + sockets[0].onclose?.(); + await vi.advanceTimersByTimeAsync(1_000); + expect(sockets).toHaveLength(2); + feed.stopStatusFeed(); + await vi.advanceTimersByTimeAsync(30_000); + expect(sockets).toHaveLength(2); + }); +}); diff --git a/gui/tests/setup.ts b/gui/tests/setup.ts new file mode 100644 index 0000000..ba23909 --- /dev/null +++ b/gui/tests/setup.ts @@ -0,0 +1,5 @@ +import '@testing-library/jest-dom/vitest'; +import { cleanup } from '@testing-library/svelte'; +import { afterEach } from 'vitest'; + +afterEach(cleanup); diff --git a/gui/tests/ui-contracts.test.mjs b/gui/tests/ui-contracts.test.mjs new file mode 100644 index 0000000..ea66794 --- /dev/null +++ b/gui/tests/ui-contracts.test.mjs @@ -0,0 +1,155 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { test } from 'node:test'; + +const source = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8'); + +test('API calls use the authenticated same-origin request boundary', () => { + const api = source('src/lib/api.ts'); + assert.match(api, /const BASE = ''/); + assert.match(api, /Authorization: `Bearer \$\{apiToken\(\)\}`/); + for (const endpoint of [ + "'/api/status'", + "'/api/sync'", + "'/api/vaults'", + "'/api/friends'", + "'/api/recovery/split'", + "'/api/recovery/ceremony/open'", + "'/api/recovery/ceremony/approve'", + "'/api/recovery/ceremony/abort'" + ]) { + assert.ok(api.includes(endpoint), `missing API contract ${endpoint}`); + } + assert.match(api, /res\.status === 401 \|\| res\.status === 403/); +}); + +test('the shell and alerts expose keyboard and accessibility semantics', () => { + const page = source('src/routes/+page.svelte'); + const errorBanner = source('src/lib/components/ErrorBanner.svelte'); + const shellHero = source('src/lib/components/ShellHero.svelte'); + assert.match(page, /