mirror of
https://github.com/Anon-Planet/thgtoa.git
synced 2026-08-07 11:35:06 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35cc075adc | ||
|
|
f2dc66e22e | ||
|
|
5a15f2615a | ||
|
|
8b4542f4d9 | ||
|
|
04f5036f3e | ||
|
|
f079b5aba8 | ||
|
|
fc3763f85c | ||
|
|
4e26ae486b | ||
|
|
9dee4c7691 | ||
|
|
4579c5f14e | ||
|
|
bf00272811 | ||
|
|
ae1c293c6b | ||
|
|
45a8539a9e | ||
|
|
cc5ad371a8 | ||
|
|
84a7ccbdd9 | ||
|
|
4eaca49a1c | ||
|
|
c5e5ae48e1 | ||
|
|
d1817e9049 | ||
|
|
ede2a53437 | ||
|
|
91a77ed552 | ||
|
|
1c3cf75cf0 | ||
|
|
121be79cd8 | ||
|
|
3b550119a8 | ||
|
|
c19389ce49 | ||
|
|
aabcbac3d9 | ||
|
|
e11a1eb1ce | ||
|
|
df6cfbc94b | ||
|
|
095bb0d8be | ||
|
|
8b81081089 | ||
|
|
ccc97461c9 | ||
|
|
8d74635d49 | ||
|
|
f71e5e2a28 | ||
|
|
3e28ec19ad | ||
|
|
192da89138 | ||
|
|
c658c354ee | ||
|
|
343ad7f037 | ||
|
|
85ea1fee66 |
@@ -0,0 +1,9 @@
|
||||
[tool.commitizen]
|
||||
name = "cz_conventional_commits"
|
||||
|
||||
# enforce sign-off below as well
|
||||
extra_arguments = ["-S", "--signoff"]
|
||||
|
||||
# harmless redundancy
|
||||
[tool.commitizen.customize]
|
||||
schema_pattern = '^(feat|add|fix|bugfix|revert|security|perf|refactor|change|chore|ci|docs|style|test|build)(\(.+\))?(!)?: .{1,72}(\n.*)*\nSigned-off-by: .+ <.+@.+>'
|
||||
@@ -1,3 +1,8 @@
|
||||
# 1. Push to main → 01-build.yml runs automatically → note the run ID
|
||||
# 2. Manually trigger 02-sign.yml with that build run ID → note the sign run ID
|
||||
# 3. Manually trigger 03-release.yml with: version=v1.2.5, sign_run_id=<id>
|
||||
# 4. Manually trigger 04-changelog.yml with: version=v1.2.5
|
||||
|
||||
name: 📖 Build PDFs
|
||||
|
||||
on:
|
||||
@@ -19,7 +24,7 @@ on:
|
||||
- "docs/**"
|
||||
- "mkdocs.yml"
|
||||
- "scripts/**"
|
||||
- ".github/workflows/build.yml"
|
||||
- ".github/workflows/01-build.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -34,7 +39,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 🛠️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: 🐍 Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -0,0 +1,261 @@
|
||||
name: 🔏 Sign PDFs
|
||||
|
||||
# Can be triggered:
|
||||
# 1. Automatically after build.yml completes on main
|
||||
# 2. Manually, pointing at a specific build run to pull PDFs from
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_run_id:
|
||||
description: 'build.yml run ID to download PDFs from'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
# Download artifacts from other runs + commit export/ files back to the repo
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
sign:
|
||||
name: Hash & Sign PDFs
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
light_sha256: ${{ steps.hashes.outputs.light_sha256 }}
|
||||
dark_sha256: ${{ steps.hashes.outputs.dark_sha256 }}
|
||||
light_b2: ${{ steps.hashes.outputs.light_b2 }}
|
||||
dark_b2: ${{ steps.hashes.outputs.dark_b2 }}
|
||||
|
||||
steps:
|
||||
- name: 🛠️ Checkout (for pgp/ key reference only)
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
sparse-checkout: pgp
|
||||
sparse-checkout-cone-mode: true
|
||||
|
||||
- name: 📥 Download PDF artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pdfs
|
||||
path: export/
|
||||
run-id: ${{ inputs.build_run_id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📋 List downloaded files
|
||||
run: ls -lh export/
|
||||
|
||||
- name: "#️⃣ Hash PDFs"
|
||||
id: hashes
|
||||
run: |
|
||||
cd export
|
||||
|
||||
for f in thgtoa.pdf thgtoa-dark.pdf; do
|
||||
[ -f "$f" ] || continue
|
||||
sha256sum "$f" | awk '{print $1}' > "${f}.sha256"
|
||||
b2sum "$f" | awk '{print $1}' > "${f}.b2sum"
|
||||
done
|
||||
|
||||
# Combined summary files
|
||||
sha256sum thgtoa.pdf thgtoa-dark.pdf 2>/dev/null > sha256sums.txt || \
|
||||
sha256sum thgtoa.pdf 2>/dev/null > sha256sums.txt
|
||||
b2sum thgtoa.pdf thgtoa-dark.pdf 2>/dev/null > b2sums.txt || \
|
||||
b2sum thgtoa.pdf 2>/dev/null > b2sums.txt
|
||||
|
||||
light_sha256=$(cat thgtoa.pdf.sha256 2>/dev/null || echo "")
|
||||
dark_sha256=$(cat thgtoa-dark.pdf.sha256 2>/dev/null || echo "")
|
||||
light_b2=$(cat thgtoa.pdf.b2sum 2>/dev/null || echo "")
|
||||
dark_b2=$(cat thgtoa-dark.pdf.b2sum 2>/dev/null || echo "")
|
||||
|
||||
echo "light_sha256=$light_sha256" >> $GITHUB_OUTPUT
|
||||
echo "dark_sha256=$dark_sha256" >> $GITHUB_OUTPUT
|
||||
echo "light_b2=$light_b2" >> $GITHUB_OUTPUT
|
||||
echo "dark_b2=$dark_b2" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "--- SHA-256 ---"
|
||||
cat sha256sums.txt
|
||||
echo "--- BLAKE2b ---"
|
||||
cat b2sums.txt
|
||||
|
||||
# GPG sign — detached ASCII-armor signatures use .asc extension
|
||||
- name: 🔑 Install GPG
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y gnupg
|
||||
|
||||
- name: 🔏 Import GPG signing key
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
run: |
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
||||
--pinentry-mode loopback --list-secret-keys
|
||||
|
||||
- name: 🔏 Sign PDFs and hash files
|
||||
env:
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
run: |
|
||||
sign() {
|
||||
local file="$1"
|
||||
[ -f "$file" ] || return 0
|
||||
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
||||
--pinentry-mode loopback \
|
||||
--detach-sign --armor --output "${file}.asc" "$file"
|
||||
echo "Signed: $file → ${file}.asc"
|
||||
}
|
||||
sign export/thgtoa.pdf
|
||||
sign export/thgtoa-dark.pdf
|
||||
sign export/sha256sums.txt
|
||||
sign export/b2sums.txt
|
||||
|
||||
# Commit export/ back to main
|
||||
- name: 📦 Checkout full repo for commit
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
path: repo
|
||||
|
||||
- name: 📂 Copy export files into repo
|
||||
run: cp -v export/* repo/export/
|
||||
|
||||
- name: 🔏 Configure GPG commit signing
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
run: |
|
||||
mkdir -p ~/.gnupg
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --quiet --import > /dev/null 2>&1 || true
|
||||
git config --global commit.gpgSign true
|
||||
git config --global gpg.program gpg
|
||||
git config --global gpg.exectimeout 180
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# If no change in git diff, do nothing
|
||||
- name: 📤 Commit and push export/ to main
|
||||
working-directory: repo
|
||||
run: |
|
||||
git add export/
|
||||
if git diff --cached --quiet; then
|
||||
echo "Nothing to commit — export/ is already up to date."
|
||||
else
|
||||
export GNUPGHOME=~/.gnupg
|
||||
gpg --list-secret-keys --with-colons > /dev/null 2>&1 || true
|
||||
git commit -S -m "chore(export): update PDFs, hashes and signatures [skip ci]"
|
||||
git config --global credential.helper 'cache --timeout=600'
|
||||
git push origin main
|
||||
fi
|
||||
|
||||
# Upload artifacts for 03-release.yml and verify job to consume
|
||||
- name: 📤 Upload signatures artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: signatures
|
||||
path: |
|
||||
export/sha256sums.txt
|
||||
export/b2sums.txt
|
||||
export/thgtoa.pdf.sha256
|
||||
export/thgtoa-dark.pdf.sha256
|
||||
export/thgtoa.pdf.b2sum
|
||||
export/thgtoa-dark.pdf.b2sum
|
||||
export/thgtoa.pdf.asc
|
||||
export/thgtoa-dark.pdf.asc
|
||||
export/sha256sums.txt.asc
|
||||
export/b2sums.txt.asc
|
||||
if-no-files-found: error
|
||||
retention-days: 90
|
||||
compression-level: 0
|
||||
|
||||
- name: 📤 Upload signed PDFs artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pdfs-signed
|
||||
path: |
|
||||
export/thgtoa.pdf
|
||||
export/thgtoa-dark.pdf
|
||||
if-no-files-found: warn
|
||||
retention-days: 90
|
||||
compression-level: 0
|
||||
|
||||
# Verify — runs after sign, surfaces results as a job summary
|
||||
verify:
|
||||
name: Verify hashes & signatures
|
||||
runs-on: ubuntu-latest
|
||||
needs: sign
|
||||
# Always run so the summary is visible even if sign partially failed
|
||||
if: always()
|
||||
# Do a sparse checkout on given patterns. Each pattern should be separated with new lines
|
||||
steps:
|
||||
- name: 🛠️ Checkout scripts and public key
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
sparse-checkout: |
|
||||
scripts/verify_pdf.py
|
||||
pgp
|
||||
|
||||
- name: 📥 Download signatures artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: signatures
|
||||
path: export/
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📥 Download signed PDFs artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pdfs-signed
|
||||
path: export/
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🔑 Install GPG and import public key
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y gnupg
|
||||
gpg --import pgp/anonymousplanet-release.asc
|
||||
|
||||
- name: 🐍 Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: 🔍 Run verify_pdf.py
|
||||
id: verify
|
||||
run: |
|
||||
# Capture output and exit code separately so we can write the
|
||||
# summary regardless of whether verification passed or failed.
|
||||
set +e
|
||||
output=$(python scripts/verify_pdf.py --hashes --signatures --export-dir export 2>&1)
|
||||
exit_code=$?
|
||||
set -e
|
||||
|
||||
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
|
||||
|
||||
# ── Job summary ──────────────────────────────────────────────
|
||||
{
|
||||
if [ "$exit_code" -eq 0 ]; then
|
||||
echo "## ✅ Verification passed"
|
||||
else
|
||||
echo "## ❌ Verification failed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "**Run:** [\`${{ github.run_id }}\`](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) · **Commit:** [\`${GITHUB_SHA::7}\`](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}) · **By:** \`${{ github.actor }}\`"
|
||||
echo ""
|
||||
echo "### Script output"
|
||||
echo '```'
|
||||
echo "$output"
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "### Hashes"
|
||||
echo '```'
|
||||
echo "── SHA-256 ──────────────────────────────────────────────────────"
|
||||
cat export/sha256sums.txt 2>/dev/null || echo "(not found)"
|
||||
echo ""
|
||||
echo "── BLAKE2b ──────────────────────────────────────────────────────"
|
||||
cat export/b2sums.txt 2>/dev/null || echo "(not found)"
|
||||
echo '```'
|
||||
} >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Propagate failure so the job is marked red if verification fails
|
||||
exit $exit_code
|
||||
@@ -0,0 +1,192 @@
|
||||
name: 🚀 Release
|
||||
|
||||
# Manual only — run this deliberately after build and sign are confirmed good.
|
||||
# Provide the 02-sign.yml run ID to pull artifacts from. The release tag is
|
||||
# automatically passed to the tag input. Exports "inputs.version" to $TAG.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sign_run_id:
|
||||
description: '02-sign.yml run ID to pull signatures and PDFs from'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Mark as pre-release?'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
version:
|
||||
description: 'Version string to record (e.g. v1.2.4) — required'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write # create releases and tags
|
||||
actions: read # download artifacts from other runs
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Publish GitHub Release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 🛠️ Checkout (for pgp/)
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
sparse-checkout: pgp
|
||||
sparse-checkout-cone-mode: true
|
||||
|
||||
# Download artifacts from the specified sign run
|
||||
- name: 📥 Download signatures artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: signatures
|
||||
path: release/
|
||||
run-id: ${{ inputs.sign_run_id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📥 Download signed PDFs artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pdfs-signed
|
||||
path: release/
|
||||
run-id: ${{ inputs.sign_run_id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📋 List release assets
|
||||
run: ls -lh release/
|
||||
|
||||
# Read hashes for the release body
|
||||
- name: "#️⃣ Read hashes"
|
||||
id: hashes
|
||||
run: |
|
||||
read_hash() { cat "release/$1" 2>/dev/null || echo "(not built)"; }
|
||||
echo "light_sha256=$(read_hash thgtoa.pdf.sha256)" >> $GITHUB_OUTPUT
|
||||
echo "dark_sha256=$(read_hash thgtoa-dark.pdf.sha256)" >> $GITHUB_OUTPUT
|
||||
echo "light_b2=$(read_hash thgtoa.pdf.b2sum)" >> $GITHUB_OUTPUT
|
||||
echo "dark_b2=$(read_hash thgtoa-dark.pdf.b2sum)" >> $GITHUB_OUTPUT
|
||||
|
||||
# VirusTotal
|
||||
- name: 🦠 Upload PDFs to VirusTotal
|
||||
id: vt
|
||||
uses: crazy-max/ghaction-virustotal@v5
|
||||
with:
|
||||
vt_api_key: ${{ secrets.VT_API_KEY }}
|
||||
files: |
|
||||
release/thgtoa.pdf
|
||||
release/thgtoa-dark.pdf
|
||||
|
||||
- name: 🔗 Build VT report URLs
|
||||
id: vt_urls
|
||||
run: |
|
||||
light_hash=$(cat release/thgtoa.pdf.sha256 2>/dev/null || echo "")
|
||||
dark_hash=$(cat release/thgtoa-dark.pdf.sha256 2>/dev/null || echo "")
|
||||
if [ -n "$light_hash" ]; then
|
||||
echo "light_vt=https://www.virustotal.com/gui/file/${light_hash}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "light_vt=(not built)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
if [ -n "$dark_hash" ]; then
|
||||
echo "dark_vt=https://www.virustotal.com/gui/file/${dark_hash}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "dark_vt=(not built)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Generate release tag — timestamp + short SHA, always unique
|
||||
- name: 🏷️ Generate release tag
|
||||
id: tag
|
||||
run: |
|
||||
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
|
||||
DATE=$(date -u +'%Y%m%d')
|
||||
TAG="${{ inputs.version }}"
|
||||
NAME="Release ${DATE} (${SHORT_SHA})"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||
echo "Tag: $TAG"
|
||||
|
||||
# Create GitHub Release
|
||||
- name: 🚀 Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.tag.outputs.tag }}
|
||||
name: ${{ steps.tag.outputs.name }}
|
||||
prerelease: ${{ inputs.prerelease || false }}
|
||||
draft: true
|
||||
fail_on_unmatched_files: false
|
||||
body: |
|
||||
## 📖 The Hitchhiker's Guide to Online Anonymity
|
||||
|
||||
Built from [`${{ inputs.version }}`](${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ inputs.version }}).
|
||||
|
||||
---
|
||||
|
||||
### 📄 Release assets
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `thgtoa.pdf` | Light mode PDF |
|
||||
| `thgtoa-dark.pdf` | Dark mode PDF (hacker theme) |
|
||||
| `sha256sums.txt` | SHA-256 checksums (both files) |
|
||||
| `b2sums.txt` | BLAKE2b checksums (both files) |
|
||||
| `thgtoa.pdf.sha256` | SHA-256 — light PDF |
|
||||
| `thgtoa-dark.pdf.sha256` | SHA-256 — dark PDF |
|
||||
| `thgtoa.pdf.b2sum` | BLAKE2b — light PDF |
|
||||
| `thgtoa-dark.pdf.b2sum` | BLAKE2b — dark PDF |
|
||||
| `*.asc` | GPG detached signatures (ASCII armor) |
|
||||
|
||||
---
|
||||
|
||||
### #️⃣ Hashes
|
||||
|
||||
**thgtoa.pdf** (light)
|
||||
```text
|
||||
SHA-256 ${{ steps.hashes.outputs.light_sha256 }}
|
||||
BLAKE2b ${{ steps.hashes.outputs.light_b2 }}
|
||||
```
|
||||
|
||||
**thgtoa-dark.pdf** (dark)
|
||||
```text
|
||||
SHA-256 ${{ steps.hashes.outputs.dark_sha256 }}
|
||||
BLAKE2b ${{ steps.hashes.outputs.dark_b2 }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔏 Verifying GPG signatures
|
||||
|
||||
```sh
|
||||
# Import the release signing key
|
||||
gpg --import pgp/anonymousplanet-release.asc
|
||||
|
||||
# Verify PDFs
|
||||
gpg --verify thgtoa.pdf.asc thgtoa.pdf
|
||||
gpg --verify thgtoa-dark.pdf.asc thgtoa-dark.pdf
|
||||
|
||||
# Verify hash files
|
||||
gpg --verify sha256sums.txt.asc sha256sums.txt
|
||||
gpg --verify b2sums.txt.asc b2sums.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🦠 VirusTotal scans
|
||||
|
||||
| File | Report |
|
||||
|------|--------|
|
||||
| `thgtoa.pdf` | ${{ steps.vt_urls.outputs.light_vt }} |
|
||||
| `thgtoa-dark.pdf` | ${{ steps.vt_urls.outputs.dark_vt }} |
|
||||
|
||||
files: |
|
||||
release/thgtoa.pdf
|
||||
release/thgtoa-dark.pdf
|
||||
release/sha256sums.txt
|
||||
release/b2sums.txt
|
||||
release/thgtoa.pdf.sha256
|
||||
release/thgtoa-dark.pdf.sha256
|
||||
release/thgtoa.pdf.b2sum
|
||||
release/thgtoa-dark.pdf.b2sum
|
||||
release/thgtoa.pdf.asc
|
||||
release/thgtoa-dark.pdf.asc
|
||||
release/sha256sums.txt.asc
|
||||
release/b2sums.txt.asc
|
||||
@@ -1,17 +1,14 @@
|
||||
name: 📝 Update Changelog
|
||||
|
||||
# Runs after build.yml completes on main — at that point we know what changed.
|
||||
# Can also be triggered manually to backfill a missing entry.
|
||||
# Manual only — run after a release is published. Provide the exact version
|
||||
# string (e.g. v1.2.4) to prepend to the changelog. Version is required to
|
||||
# prevent silent auto-increment drift from release tags.
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["📖 Build PDFs"]
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version string (e.g. v1.2.4) — leave blank to auto-increment'
|
||||
required: false
|
||||
description: 'Version string to record (e.g. v1.2.4) — required'
|
||||
required: true
|
||||
type: string
|
||||
dry_run:
|
||||
description: 'Dry run — print entry without committing'
|
||||
@@ -20,19 +17,16 @@ on:
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: write # commit changelog back to main
|
||||
contents: write # commit changelog back to main
|
||||
|
||||
jobs:
|
||||
changelog:
|
||||
name: Prepend changelog entry
|
||||
if: >
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 🛠️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
# Use a PAT so the commit triggers downstream workflows (GITHUB_TOKEN won't)
|
||||
token: ${{ secrets.CHANGELOG_PAT || secrets.GITHUB_TOKEN }}
|
||||
@@ -46,7 +40,7 @@ jobs:
|
||||
- name: 📝 Generate and prepend changelog entry
|
||||
env:
|
||||
DRY_RUN: ${{ inputs.dry_run || 'false' }}
|
||||
MANUAL_VERSION: ${{ inputs.version || '' }}
|
||||
MANUAL_VERSION: ${{ inputs.version }}
|
||||
GH_SHA: ${{ github.sha }}
|
||||
GH_REF: ${{ github.ref_name }}
|
||||
TRIGGERING_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
@@ -1,218 +0,0 @@
|
||||
# DEPRECATED — replaced by build.yml, sign.yml, and release.yml
|
||||
# This file is kept temporarily so in-flight runs are not broken.
|
||||
#
|
||||
# name: 📖 Build & Sign PDFs
|
||||
|
||||
on:
|
||||
workflow_dispatch: # manual only — no automatic triggers (deprecated)
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build-sign-release:
|
||||
name: Build, Sign & Release PDFs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🛠️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🐍 Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: 📦 Install Python dependencies
|
||||
run: pip install mkdocs-material pillow numpy
|
||||
|
||||
- name: 🖼️ Install poppler (pdftoppm) and qpdf
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y poppler-utils qpdf
|
||||
|
||||
- name: Setup Chrome
|
||||
uses: browser-actions/setup-chrome@v2
|
||||
with:
|
||||
chrome-version: 120
|
||||
install-dependencies: true
|
||||
install-chromedriver: true
|
||||
|
||||
- name: 🔑 Install GPG tools
|
||||
run: sudo apt-get install -y gnupg
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Build PDFs
|
||||
# ------------------------------------------------------------------ #
|
||||
- name: 🖨️ Build PDFs
|
||||
env:
|
||||
CI: true
|
||||
run: python scripts/build_guide_pdf.py --${{ inputs.build_mode || 'both' }}
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Hash (SHA-256 + BLAKE2b)
|
||||
# ------------------------------------------------------------------ #
|
||||
- name: #️⃣ Hash PDFs
|
||||
id: hashes
|
||||
run: |
|
||||
mkdir -p export
|
||||
|
||||
sha256sum export/thgtoa.pdf | awk '{print $1}' > export/thgtoa.pdf.sha256
|
||||
sha256sum export/thgtoa-dark.pdf | awk '{print $1}' > export/thgtoa-dark.pdf.sha256
|
||||
b2sum export/thgtoa.pdf | awk '{print $1}' > export/thgtoa.pdf.b2
|
||||
b2sum export/thgtoa-dark.pdf | awk '{print $1}' > export/thgtoa-dark.pdf.b2
|
||||
|
||||
# Also write combined human-readable files
|
||||
sha256sum export/thgtoa.pdf export/thgtoa-dark.pdf > export/sha256sums.txt
|
||||
b2sum export/thgtoa.pdf export/thgtoa-dark.pdf > export/b2sums.txt
|
||||
|
||||
# Expose hashes as step outputs for the release body
|
||||
echo "light_sha256=$(cat export/thgtoa.pdf.sha256)" >> $GITHUB_OUTPUT
|
||||
echo "dark_sha256=$(cat export/thgtoa-dark.pdf.sha256)" >> $GITHUB_OUTPUT
|
||||
echo "light_b2=$(cat export/thgtoa.pdf.b2)" >> $GITHUB_OUTPUT
|
||||
echo "dark_b2=$(cat export/thgtoa-dark.pdf.b2)" >> $GITHUB_OUTPUT
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# GPG sign (detached .sig for each PDF + each hash file)
|
||||
# ------------------------------------------------------------------ #
|
||||
- name: 🔏 Import GPG key
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
run: |
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
# Pre-cache the passphrase so signing doesn't prompt
|
||||
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
||||
--pinentry-mode loopback --list-secret-keys
|
||||
|
||||
- name: 🔏 GPG sign PDFs and hash files
|
||||
env:
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
run: |
|
||||
sign() {
|
||||
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
||||
--pinentry-mode loopback \
|
||||
--detach-sign --armor --output "${1}.sig" "$1"
|
||||
}
|
||||
sign export/thgtoa.pdf
|
||||
sign export/thgtoa-dark.pdf
|
||||
sign export/sha256sums.txt
|
||||
sign export/b2sums.txt
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# VirusTotal
|
||||
# ------------------------------------------------------------------ #
|
||||
- name: 🦠 Upload PDFs to VirusTotal
|
||||
id: vt
|
||||
uses: crazy-max/ghaction-virustotal@v5
|
||||
with:
|
||||
vt_api_key: ${{ secrets.VT_API_KEY }}
|
||||
files: |
|
||||
export/thgtoa.pdf
|
||||
export/thgtoa-dark.pdf
|
||||
|
||||
- name: 🔗 Build VT report URLs
|
||||
id: vt_urls
|
||||
run: |
|
||||
light_hash=$(cat export/thgtoa.pdf.sha256)
|
||||
dark_hash=$(cat export/thgtoa-dark.pdf.sha256)
|
||||
echo "light_vt=https://www.virustotal.com/gui/file/${light_hash}" >> $GITHUB_OUTPUT
|
||||
echo "dark_vt=https://www.virustotal.com/gui/file/${dark_hash}" >> $GITHUB_OUTPUT
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Create GitHub Release
|
||||
# ------------------------------------------------------------------ #
|
||||
- name: 🏷️ Generate release tag
|
||||
id: tag
|
||||
run: |
|
||||
TAG="release-$(date -u +'%Y%m%d-%H%M%S')"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "name=Release $(date -u +'%Y-%m-%d %H:%M UTC')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 🚀 Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.tag.outputs.tag }}
|
||||
name: ${{ steps.tag.outputs.name }}
|
||||
body: |
|
||||
## 📖 The Hitchhiker's Guide to Online Anonymity
|
||||
|
||||
Built from commit ${{ github.sha }} on `${{ github.ref_name }}`.
|
||||
|
||||
---
|
||||
|
||||
### 📄 Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `thgtoa.pdf` | Light mode PDF |
|
||||
| `thgtoa-dark.pdf` | Dark mode PDF (hacker theme) |
|
||||
| `sha256sums.txt` | SHA-256 checksums |
|
||||
| `b2sums.txt` | BLAKE2b checksums |
|
||||
| `*.sig` | GPG detached signatures (ASCII armor) |
|
||||
|
||||
---
|
||||
|
||||
### #️⃣ Hashes
|
||||
|
||||
#### thgtoa.pdf (Light)
|
||||
```
|
||||
SHA-256: ${{ steps.hashes.outputs.light_sha256 }}
|
||||
BLAKE2b: ${{ steps.hashes.outputs.light_b2 }}
|
||||
```
|
||||
|
||||
#### thgtoa-dark.pdf (Dark)
|
||||
```
|
||||
SHA-256: ${{ steps.hashes.outputs.dark_sha256 }}
|
||||
BLAKE2b: ${{ steps.hashes.outputs.dark_b2 }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔏 GPG Signatures
|
||||
|
||||
Detached signatures (`.sig`) are included in the release assets.
|
||||
Verify with:
|
||||
```bash
|
||||
gpg --verify thgtoa.pdf.sig thgtoa.pdf
|
||||
gpg --verify thgtoa-dark.pdf.sig thgtoa-dark.pdf
|
||||
```
|
||||
The signing key is published at `pgp/anonymousplanet-release.asc`.
|
||||
|
||||
---
|
||||
|
||||
### 🦠 VirusTotal Scans
|
||||
|
||||
| File | Report |
|
||||
|------|--------|
|
||||
| `thgtoa.pdf` | ${{ steps.vt_urls.outputs.light_vt }} |
|
||||
| `thgtoa-dark.pdf` | ${{ steps.vt_urls.outputs.dark_vt }} |
|
||||
|
||||
files: |
|
||||
export/thgtoa.pdf
|
||||
export/thgtoa-dark.pdf
|
||||
export/sha256sums.txt
|
||||
export/b2sums.txt
|
||||
export/thgtoa.pdf.sha256
|
||||
export/thgtoa-dark.pdf.sha256
|
||||
export/thgtoa.pdf.b2
|
||||
export/thgtoa-dark.pdf.b2
|
||||
export/thgtoa.pdf.sig
|
||||
export/thgtoa-dark.pdf.sig
|
||||
export/sha256sums.txt.sig
|
||||
export/b2sums.txt.sig
|
||||
draft: false
|
||||
prerelease: false
|
||||
fail_on_unmatched_files: true
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Upload everything as a workflow artifact (90-day archive)
|
||||
# ------------------------------------------------------------------ #
|
||||
- name: 📤 Upload export as workflow artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pdf-release-${{ steps.tag.outputs.tag }}
|
||||
path: export/*
|
||||
if-no-files-found: error
|
||||
retention-days: 90
|
||||
compression-level: 0
|
||||
@@ -14,7 +14,6 @@ jobs:
|
||||
|
||||
- name: Deploy docs
|
||||
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
||||
# Or use mhausenblas/mkdocs-deploy-gh-pages@nomaterial to build without the mkdocs-material theme
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CUSTOM_DOMAIN: anonymousplanet.org
|
||||
CUSTOM_DOMAIN: anonymousplanet.net
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
name: 🚀 Release
|
||||
|
||||
# Can be triggered:
|
||||
# 1. Automatically after sign.yml completes on main
|
||||
# 2. Manually, pointing at specific build/sign runs to pull artifacts from
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["🔏 Sign PDFs"]
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sign_run_id:
|
||||
description: 'sign.yml run ID to pull signatures from'
|
||||
required: true
|
||||
type: string
|
||||
build_run_id:
|
||||
description: 'build.yml run ID to pull PDFs from (leave blank to use pdfs-signed from sign run)'
|
||||
required: false
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Mark as pre-release?'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: write # create releases and tags
|
||||
actions: read # download artifacts from other runs
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Publish GitHub Release
|
||||
if: >
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 🛠️ Checkout (for commit metadata only)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: pgp
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Resolve which run IDs to pull artifacts from
|
||||
# ------------------------------------------------------------------ #
|
||||
- name: 🔍 Resolve run IDs
|
||||
id: runs
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
SIGN_RUN="${{ inputs.sign_run_id }}"
|
||||
BUILD_RUN="${{ inputs.build_run_id }}"
|
||||
else
|
||||
SIGN_RUN="${{ github.event.workflow_run.id }}"
|
||||
BUILD_RUN=""
|
||||
fi
|
||||
echo "sign_run=$SIGN_RUN" >> $GITHUB_OUTPUT
|
||||
echo "build_run=$BUILD_RUN" >> $GITHUB_OUTPUT
|
||||
echo "Sign run: $SIGN_RUN"
|
||||
echo "Build run: ${BUILD_RUN:-'(using pdfs-signed from sign run)'}"
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Download artifacts
|
||||
# ------------------------------------------------------------------ #
|
||||
- name: 📥 Download signatures artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: signatures
|
||||
path: release/
|
||||
run-id: ${{ steps.runs.outputs.sign_run }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📥 Download PDFs (from sign run)
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pdfs-signed
|
||||
path: release/
|
||||
run-id: ${{ steps.runs.outputs.sign_run }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📋 List release assets
|
||||
run: ls -lh release/
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Read hashes for the release body
|
||||
# ------------------------------------------------------------------ #
|
||||
- name: #️⃣ Read hashes
|
||||
id: hashes
|
||||
run: |
|
||||
read_hash() { cat "release/$1" 2>/dev/null || echo "(not built)"; }
|
||||
|
||||
echo "light_sha256=$(read_hash thgtoa.pdf.sha256)" >> $GITHUB_OUTPUT
|
||||
echo "dark_sha256=$(read_hash thgtoa-dark.pdf.sha256)" >> $GITHUB_OUTPUT
|
||||
echo "light_b2=$(read_hash thgtoa.pdf.b2)" >> $GITHUB_OUTPUT
|
||||
echo "dark_b2=$(read_hash thgtoa-dark.pdf.b2)" >> $GITHUB_OUTPUT
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# VirusTotal — upload whichever PDFs are present
|
||||
# ------------------------------------------------------------------ #
|
||||
- name: 🦠 Upload PDFs to VirusTotal
|
||||
id: vt
|
||||
uses: crazy-max/ghaction-virustotal@v5
|
||||
with:
|
||||
vt_api_key: ${{ secrets.VT_API_KEY }}
|
||||
files: |
|
||||
release/thgtoa.pdf
|
||||
release/thgtoa-dark.pdf
|
||||
|
||||
- name: 🔗 Build VT report URLs
|
||||
id: vt_urls
|
||||
run: |
|
||||
light_hash=$(cat release/thgtoa.pdf.sha256 2>/dev/null || echo "")
|
||||
dark_hash=$(cat release/thgtoa-dark.pdf.sha256 2>/dev/null || echo "")
|
||||
[ -n "$light_hash" ] && \
|
||||
echo "light_vt=https://www.virustotal.com/gui/file/${light_hash}" >> $GITHUB_OUTPUT || \
|
||||
echo "light_vt=(not built)" >> $GITHUB_OUTPUT
|
||||
[ -n "$dark_hash" ] && \
|
||||
echo "dark_vt=https://www.virustotal.com/gui/file/${dark_hash}" >> $GITHUB_OUTPUT || \
|
||||
echo "dark_vt=(not built)" >> $GITHUB_OUTPUT
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Tag + Release
|
||||
# ------------------------------------------------------------------ #
|
||||
- name: 🏷️ Generate release tag
|
||||
id: tag
|
||||
run: |
|
||||
TAG="v$(date -u +'%Y.%m.%d')-$(echo ${{ github.sha }} | cut -c1-7)"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "name=Release $(date -u +'%Y-%m-%d') (${TAG})" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 🚀 Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.tag.outputs.tag }}
|
||||
name: ${{ steps.tag.outputs.name }}
|
||||
prerelease: ${{ inputs.prerelease || false }}
|
||||
draft: false
|
||||
fail_on_unmatched_files: false
|
||||
body: |
|
||||
## 📖 The Hitchhiker's Guide to Online Anonymity
|
||||
|
||||
Built from [`${{ github.sha }}`](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}) on `${{ github.ref_name }}`.
|
||||
|
||||
---
|
||||
|
||||
### 📄 Release assets
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `thgtoa.pdf` | Light mode PDF |
|
||||
| `thgtoa-dark.pdf` | Dark mode PDF (hacker theme) |
|
||||
| `sha256sums.txt` | SHA-256 checksums (both files) |
|
||||
| `b2sums.txt` | BLAKE2b checksums (both files) |
|
||||
| `thgtoa.pdf.sha256` | SHA-256 — light PDF |
|
||||
| `thgtoa-dark.pdf.sha256` | SHA-256 — dark PDF |
|
||||
| `thgtoa.pdf.b2` | BLAKE2b — light PDF |
|
||||
| `thgtoa-dark.pdf.b2` | BLAKE2b — dark PDF |
|
||||
| `*.sig` | GPG detached signatures (ASCII armor) |
|
||||
|
||||
---
|
||||
|
||||
### #️⃣ Hashes
|
||||
|
||||
**thgtoa.pdf** (light)
|
||||
```
|
||||
SHA-256 ${{ steps.hashes.outputs.light_sha256 }}
|
||||
BLAKE2b ${{ steps.hashes.outputs.light_b2 }}
|
||||
```
|
||||
|
||||
**thgtoa-dark.pdf** (dark)
|
||||
```
|
||||
SHA-256 ${{ steps.hashes.outputs.dark_sha256 }}
|
||||
BLAKE2b ${{ steps.hashes.outputs.dark_b2 }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔏 Verifying GPG signatures
|
||||
|
||||
```bash
|
||||
# Import the release signing key
|
||||
gpg --import pgp/anonymousplanet-release.asc
|
||||
|
||||
# Verify PDFs
|
||||
gpg --verify thgtoa.pdf.sig thgtoa.pdf
|
||||
gpg --verify thgtoa-dark.pdf.sig thgtoa-dark.pdf
|
||||
|
||||
# Verify hash files
|
||||
gpg --verify sha256sums.txt.sig sha256sums.txt
|
||||
gpg --verify b2sums.txt.sig b2sums.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🦠 VirusTotal scans
|
||||
|
||||
| File | Report |
|
||||
|------|--------|
|
||||
| `thgtoa.pdf` | ${{ steps.vt_urls.outputs.light_vt }} |
|
||||
| `thgtoa-dark.pdf` | ${{ steps.vt_urls.outputs.dark_vt }} |
|
||||
|
||||
files: |
|
||||
release/thgtoa.pdf
|
||||
release/thgtoa-dark.pdf
|
||||
release/sha256sums.txt
|
||||
release/b2sums.txt
|
||||
release/thgtoa.pdf.sha256
|
||||
release/thgtoa-dark.pdf.sha256
|
||||
release/thgtoa.pdf.b2
|
||||
release/thgtoa-dark.pdf.b2
|
||||
release/thgtoa.pdf.sig
|
||||
release/thgtoa-dark.pdf.sig
|
||||
release/sha256sums.txt.sig
|
||||
release/b2sums.txt.sig
|
||||
@@ -1,165 +0,0 @@
|
||||
name: 🔏 Sign PDFs
|
||||
|
||||
# Can be triggered:
|
||||
# 1. Automatically after build.yml completes on main
|
||||
# 2. Manually, pointing at a specific build run to pull PDFs from
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["📖 Build PDFs"]
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_run_id:
|
||||
description: 'build.yml run ID to download PDFs from (leave blank for latest)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
actions: read # download artifacts from other runs
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sign:
|
||||
name: Hash & Sign PDFs
|
||||
# On workflow_run, only proceed if the build actually succeeded
|
||||
if: >
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
light_sha256: ${{ steps.hashes.outputs.light_sha256 }}
|
||||
dark_sha256: ${{ steps.hashes.outputs.dark_sha256 }}
|
||||
light_b2: ${{ steps.hashes.outputs.light_b2 }}
|
||||
dark_b2: ${{ steps.hashes.outputs.dark_b2 }}
|
||||
|
||||
steps:
|
||||
- name: 🛠️ Checkout (for pgp/ key reference only)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: pgp
|
||||
|
||||
- name: 🔑 Install GPG
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y gnupg
|
||||
|
||||
# Download PDFs from the triggering build run, or a manually specified one
|
||||
- name: 📥 Resolve source run ID
|
||||
id: src
|
||||
run: |
|
||||
if [ -n "${{ inputs.build_run_id }}" ]; then
|
||||
echo "run_id=${{ inputs.build_run_id }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "run_id=${{ github.event.workflow_run.id }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: 📥 Download PDF artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pdfs
|
||||
path: export/
|
||||
run-id: ${{ steps.src.outputs.run_id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📋 List downloaded files
|
||||
run: ls -lh export/
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Hash
|
||||
# ------------------------------------------------------------------ #
|
||||
- name: #️⃣ Hash PDFs
|
||||
id: hashes
|
||||
run: |
|
||||
cd export
|
||||
|
||||
for f in thgtoa.pdf thgtoa-dark.pdf; do
|
||||
[ -f "$f" ] || continue
|
||||
sha256sum "$f" | awk '{print $1}' > "${f}.sha256"
|
||||
b2sum "$f" | awk '{print $1}' > "${f}.b2"
|
||||
done
|
||||
|
||||
# Combined files (only include files that exist)
|
||||
sha256sum thgtoa.pdf thgtoa-dark.pdf 2>/dev/null > sha256sums.txt || \
|
||||
sha256sum thgtoa.pdf 2>/dev/null > sha256sums.txt
|
||||
b2sum thgtoa.pdf thgtoa-dark.pdf 2>/dev/null > b2sums.txt || \
|
||||
b2sum thgtoa.pdf 2>/dev/null > b2sums.txt
|
||||
|
||||
# Expose individual hashes as outputs (empty string if file absent)
|
||||
light_sha256=$(cat thgtoa.pdf.sha256 2>/dev/null || echo "")
|
||||
dark_sha256=$(cat thgtoa-dark.pdf.sha256 2>/dev/null || echo "")
|
||||
light_b2=$(cat thgtoa.pdf.b2 2>/dev/null || echo "")
|
||||
dark_b2=$(cat thgtoa-dark.pdf.b2 2>/dev/null || echo "")
|
||||
|
||||
echo "light_sha256=$light_sha256" >> $GITHUB_OUTPUT
|
||||
echo "dark_sha256=$dark_sha256" >> $GITHUB_OUTPUT
|
||||
echo "light_b2=$light_b2" >> $GITHUB_OUTPUT
|
||||
echo "dark_b2=$dark_b2" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "--- SHA-256 ---"
|
||||
cat sha256sums.txt
|
||||
echo "--- BLAKE2b ---"
|
||||
cat b2sums.txt
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# GPG sign
|
||||
# ------------------------------------------------------------------ #
|
||||
- name: 🔏 Import GPG signing key
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
run: |
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
# Pre-cache passphrase to prevent interactive prompt
|
||||
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
||||
--pinentry-mode loopback --list-secret-keys
|
||||
|
||||
- name: 🔏 Sign PDFs and hash files
|
||||
env:
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
run: |
|
||||
sign() {
|
||||
local file="$1"
|
||||
[ -f "$file" ] || return 0
|
||||
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
||||
--pinentry-mode loopback \
|
||||
--detach-sign --armor --output "${file}.sig" "$file"
|
||||
echo "Signed: $file"
|
||||
}
|
||||
sign export/thgtoa.pdf
|
||||
sign export/thgtoa-dark.pdf
|
||||
sign export/sha256sums.txt
|
||||
sign export/b2sums.txt
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Upload — PDFs + all signatures and hashes together
|
||||
# ------------------------------------------------------------------ #
|
||||
- name: 📤 Upload signatures artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: signatures
|
||||
path: |
|
||||
export/sha256sums.txt
|
||||
export/b2sums.txt
|
||||
export/thgtoa.pdf.sha256
|
||||
export/thgtoa-dark.pdf.sha256
|
||||
export/thgtoa.pdf.b2
|
||||
export/thgtoa-dark.pdf.b2
|
||||
export/thgtoa.pdf.sig
|
||||
export/thgtoa-dark.pdf.sig
|
||||
export/sha256sums.txt.sig
|
||||
export/b2sums.txt.sig
|
||||
if-no-files-found: error
|
||||
retention-days: 90
|
||||
compression-level: 0
|
||||
|
||||
- name: 📤 Upload signed PDFs artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pdfs-signed
|
||||
path: |
|
||||
export/thgtoa.pdf
|
||||
export/thgtoa-dark.pdf
|
||||
if-no-files-found: warn
|
||||
retention-days: 90
|
||||
compression-level: 0
|
||||
+4
-4
@@ -21,9 +21,9 @@ _site/
|
||||
_site_test/
|
||||
build/
|
||||
|
||||
# Export directory - but track hash files and signatures
|
||||
# Export directory — track only hashes and signatures, not the PDFs themselves
|
||||
export/thgtoa.pdf.sha256
|
||||
export/thgtoa-dark.pdf.sha256
|
||||
export/thgtoa.pdf.b2
|
||||
export/thgtoa-dark.pdf.b2
|
||||
*.sig
|
||||
export/thgtoa.pdf.b2sum
|
||||
export/thgtoa-dark.pdf.b2sum
|
||||
export/*.asc
|
||||
|
||||
+1
-18
@@ -62,24 +62,7 @@ MD012:
|
||||
# Consecutive blank lines
|
||||
maximum: 1
|
||||
# MD013/line-length - Line length
|
||||
#
|
||||
MD013:
|
||||
# Number of characters
|
||||
line_length: 80
|
||||
# Number of characters for headings
|
||||
heading_line_length: 80
|
||||
# Number of characters for code blocks
|
||||
code_block_line_length: 160
|
||||
# Include code blocks
|
||||
code_blocks: false
|
||||
# Include tables
|
||||
tables: false
|
||||
# Include headings
|
||||
headings: true
|
||||
# Strict length checking (e.g. allow for longer URLs)
|
||||
strict: false
|
||||
# Stern length checking
|
||||
stern: false
|
||||
MD013: false
|
||||
|
||||
# MD014/commands-show-output - Dollar signs used before commands without showing output
|
||||
# TODO: set false for now but we should consider enabling it
|
||||
|
||||
+12
-5
@@ -10,14 +10,21 @@ repos:
|
||||
- id: check-added-large-files
|
||||
- id: check-merge-conflict
|
||||
- id: check-symlinks
|
||||
- id: detect-private-key
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- id: mixed-line-ending
|
||||
args: [--fix=lf]
|
||||
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.41.0
|
||||
- repo: https://github.com/commitizen-tools/commitizen
|
||||
rev: v4.8.3
|
||||
hooks:
|
||||
- id: markdownlint
|
||||
- id: markdownlint-fix
|
||||
- id: commitizen
|
||||
stages: [commit-msg]
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: require-commit-body
|
||||
name: Avoid bad commits
|
||||
language: script
|
||||
entry: scripts/hooks/require-commit-body.sh
|
||||
stages: [commit-msg]
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Contributor License Agreement
|
||||
|
||||
This project is licensed under the **Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA 4.0)**.
|
||||
|
||||
By submitting a contribution to this repository, you agree to the terms described in this document.
|
||||
|
||||
---
|
||||
|
||||
## What Is a CLA?
|
||||
|
||||
A Contributor License Agreement (CLA) clarifies the terms under which you contribute to this project. It does not transfer ownership of your work—you keep all rights to what you create. It establishes that your contribution can be distributed under this project's license, and that you understand and accept the obligations that come with it.
|
||||
|
||||
---
|
||||
|
||||
## Our License: CC BY-SA 4.0
|
||||
|
||||
This repository uses the [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||
|
||||
> **Note on license history:** This project previously used CC BY-NC 4.0 (non-commercial). The switch to CC BY-SA 4.0 removes the non-commercial restriction and replaces it with a share-alike requirement. Contributions submitted before this change were made under the prior license; all new contributions fall under CC BY-SA 4.0.
|
||||
|
||||
In plain terms, CC BY-SA 4.0 means:
|
||||
|
||||
- **Attribution (BY):** Anyone who uses, adapts, or redistributes this work must give appropriate credit to the original authors, provide a link to the license, and indicate if changes were made.
|
||||
- **ShareAlike (SA):** Anyone who distributes adapted versions of this work must do so under the same license—CC BY-SA 4.0—or a compatible one. Derivative works cannot be relicensed under more restrictive terms.
|
||||
|
||||
The full legal text is available at: <https://creativecommons.org/licenses/by-sa/4.0/legalcode>
|
||||
|
||||
---
|
||||
|
||||
## What Counts as a Contribution
|
||||
|
||||
A "contribution" is any material you intentionally submit to this repository for inclusion in the project. This includes but is not limited to:
|
||||
|
||||
- Written content (guides, documentation, articles, explanations)
|
||||
- Edits, corrections, or translations of existing content
|
||||
- Code, scripts, configuration files, or tooling
|
||||
- Issues, bug reports, and feature requests that include original written content
|
||||
- Pull requests and their descriptions
|
||||
- Comments that substantively add to the project's content
|
||||
|
||||
Minor factual corrections, typo fixes, and purely mechanical formatting changes are contributed in good faith and treated as submitted under the same terms.
|
||||
|
||||
---
|
||||
|
||||
## Your Rights as a Contributor
|
||||
|
||||
Contributing to this project does **not** transfer copyright ownership of your work to the project maintainers or the Anonymous Planet collective. You retain full copyright over your original contributions.
|
||||
|
||||
By contributing, you grant the project a perpetual, worldwide, non-exclusive, royalty-free license to use, reproduce, adapt, and distribute your contribution under CC BY-SA 4.0—consistent with the license terms the entire project operates under.
|
||||
|
||||
---
|
||||
|
||||
## Your Obligations as a Contributor
|
||||
|
||||
When you submit a contribution, you confirm that:
|
||||
|
||||
1. **You have the right to submit it.** The contribution is your own original work, or you have the necessary rights to submit it under CC BY-SA 4.0. You are not submitting material that is proprietary, confidential, or owned by a third party without their permission.
|
||||
|
||||
2. **It is compatible with CC BY-SA 4.0.** If your contribution incorporates third-party material, that material must already be licensed under CC BY-SA 4.0 or a [compatible license](https://creativecommons.org/share-your-work/licensing-considerations/compatible-licenses/). Do not submit content under CC BY-NC 4.0 or any other license that conflicts with CC BY-SA 4.0.
|
||||
|
||||
3. **Attribution will carry forward.** You understand that your contribution—along with your credit as a contributor—may be included in redistributed or adapted versions of this project, and that downstream users will be required to attribute the project and preserve the CC BY-SA 4.0 license.
|
||||
|
||||
4. **Derivative works stay open.** You understand that anyone who builds on this project's content (including your contributions) must release their adaptations under CC BY-SA 4.0 or a compatible license. This is the core share-alike obligation.
|
||||
|
||||
---
|
||||
|
||||
## How Acceptance Works
|
||||
|
||||
This project does not use a formal CLA signature process. Acceptance is implicit:
|
||||
|
||||
> **By opening a pull request, submitting an issue with original content, or otherwise contributing material to this repository, you indicate that you have read this document and agree to its terms.**
|
||||
|
||||
If you are contributing on behalf of an organization or employer, you confirm that you are authorized to submit contributions under these terms on their behalf.
|
||||
|
||||
---
|
||||
|
||||
## Questions
|
||||
|
||||
If you are unsure whether your contribution is compatible with CC BY-SA 4.0, or have questions about these terms, open an issue before submitting. Maintainers will do their best to clarify.
|
||||
|
||||
For the full license text: <https://creativecommons.org/licenses/by-sa/4.0/legalcode>
|
||||
@@ -0,0 +1,427 @@
|
||||
Attribution-ShareAlike 4.0 International
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||
does not provide legal services or legal advice. Distribution of
|
||||
Creative Commons public licenses does not create a lawyer-client or
|
||||
other relationship. Creative Commons makes its licenses and related
|
||||
information available on an "as-is" basis. Creative Commons gives no
|
||||
warranties regarding its licenses, any material licensed under their
|
||||
terms and conditions, or any related information. Creative Commons
|
||||
disclaims all liability for damages resulting from their use to the
|
||||
fullest extent possible.
|
||||
|
||||
Using Creative Commons Public Licenses
|
||||
|
||||
Creative Commons public licenses provide a standard set of terms and
|
||||
conditions that creators and other rights holders may use to share
|
||||
original works of authorship and other material subject to copyright
|
||||
and certain other rights specified in the public license below. The
|
||||
following considerations are for informational purposes only, are not
|
||||
exhaustive, and do not form part of our licenses.
|
||||
|
||||
Considerations for licensors: Our public licenses are
|
||||
intended for use by those authorized to give the public
|
||||
permission to use material in ways otherwise restricted by
|
||||
copyright and certain other rights. Our licenses are
|
||||
irrevocable. Licensors should read and understand the terms
|
||||
and conditions of the license they choose before applying it.
|
||||
Licensors should also secure all rights necessary before
|
||||
applying our licenses so that the public can reuse the
|
||||
material as expected. Licensors should clearly mark any
|
||||
material not subject to the license. This includes other CC-
|
||||
licensed material, or material used under an exception or
|
||||
limitation to copyright. More considerations for licensors:
|
||||
wiki.creativecommons.org/Considerations_for_licensors
|
||||
|
||||
Considerations for the public: By using one of our public
|
||||
licenses, a licensor grants the public permission to use the
|
||||
licensed material under specified terms and conditions. If
|
||||
the licensor's permission is not necessary for any reason--for
|
||||
example, because of any applicable exception or limitation to
|
||||
copyright--then that use is not regulated by the license. Our
|
||||
licenses grant only permissions under copyright and certain
|
||||
other rights that a licensor has authority to grant. Use of
|
||||
the licensed material may still be restricted for other
|
||||
reasons, including because others have copyright or other
|
||||
rights in the material. A licensor may make special requests,
|
||||
such as asking that all changes be marked or described.
|
||||
Although not required by our licenses, you are encouraged to
|
||||
respect those requests where reasonable. More considerations
|
||||
for the public:
|
||||
wiki.creativecommons.org/Considerations_for_licensees
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Attribution-ShareAlike 4.0 International Public
|
||||
License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution-ShareAlike 4.0 International Public License ("Public
|
||||
License"). To the extent this Public License may be interpreted as a
|
||||
contract, You are granted the Licensed Rights in consideration of Your
|
||||
acceptance of these terms and conditions, and the Licensor grants You
|
||||
such rights in consideration of benefits the Licensor receives from
|
||||
making the Licensed Material available under these terms and
|
||||
conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. BY-SA Compatible License means a license listed at
|
||||
creativecommons.org/compatiblelicenses, approved by Creative
|
||||
Commons as essentially the equivalent of this Public License.
|
||||
|
||||
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
e. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
g. License Elements means the license attributes listed in the name
|
||||
of a Creative Commons Public License. The License Elements of this
|
||||
Public License are Attribution and ShareAlike.
|
||||
|
||||
h. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
i. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
k. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
l. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
m. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. Additional offer from the Licensor -- Adapted Material.
|
||||
Every recipient of Adapted Material from You
|
||||
automatically receives an offer from the Licensor to
|
||||
exercise the Licensed Rights in the Adapted Material
|
||||
under the conditions of the Adapter's License You apply.
|
||||
|
||||
c. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
b. ShareAlike.
|
||||
|
||||
In addition to the conditions in Section 3(a), if You Share
|
||||
Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter's License You apply must be a Creative Commons
|
||||
license with the same License Elements, this version or
|
||||
later, or a BY-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to, the
|
||||
Adapter's License You apply. You may satisfy this condition
|
||||
in any reasonable manner based on the medium, means, and
|
||||
context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms
|
||||
or conditions on, or apply any Effective Technological
|
||||
Measures to, Adapted Material that restrict exercise of the
|
||||
rights granted under the Adapter's License You apply.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material,
|
||||
including for purposes of Section 3(b); and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the
|
||||
use of the trademark "Creative Commons" or any other trademark or logo
|
||||
of Creative Commons without its prior written consent including,
|
||||
without limitation, in connection with any unauthorized modifications
|
||||
to any of its public licenses or any other arrangements,
|
||||
understandings, or agreements concerning use of licensed material. For
|
||||
the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
@@ -1,12 +1,26 @@
|
||||
<div align="center">
|
||||
<a href="https://anonymousplanet.net">
|
||||
<img src="https://anonymousplanet.net/media/profile.png"></a>
|
||||
|
||||
<p><em>The comprehensive guide for online anonymity and OpSec.</em></p>
|
||||
|
||||
<a href="https://github.com/Anon-Planet/thgtoa/stargazers">
|
||||
<img src="https://img.shields.io/github/stars/Anon-Planet?style=social"></a>
|
||||
<a href="https://github.com/Anon-Planet/thgtoa/releases/latest">
|
||||
<img src="https://img.shields.io/badge/MkDocs_Material-526CFE?logo=MaterialForMkDocs&logoColor=green"></a>
|
||||
<a href="https://github.com/Anon-Planet/thgtoa/actions/workflows/release.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/Anon-Planet/thgtoa/release.yml?label=release"></a></p>
|
||||
</div>
|
||||
|
||||
Welcome.
|
||||
|
||||
This is a guide with the aim of providing an introduction to various online tracking techniques, online ID verification techniques, and detailed guidance to creating and maintaining (truly) anonymous online identities. <span style="color: red">**It is written with hope for activists, journalists, scientists, lawyers, whistle-blowers, and good people being oppressed, censored, harassed anywhere!**</span> This guide has no affiliation with the [Anonymous](https://en.wikipedia.org/wiki/Anonymous_(hacker_group)) <sup>[[Wikiless]](https://wikiless.com/wiki/Anonymous_(hacker_group))</sup> <sup>[[Archive.org]](https://web.archive.org/web/https://en.wikipedia.org/wiki/Anonymous_(hacker_group))</sup> collective/movement.
|
||||
|
||||
This guide is an open-source non-profit initiative, [licensed](LICENSE.html) under **Creative Commons Attribution-NonCommercial 4.0 International** ([cc-by-nc-4.0](https://creativecommons.org/licenses/by-nc/4.0/) <sup>[[Archive.org]](https://web.archive.org/web/https://creativecommons.org/licenses/by-nc/4.0/)</sup>) and is **not sponsored/endorsed by any commercial/governmental entity**. This means that you are free to use our guide for pretty much any purpose **excluding commercially** as long as you do attribute it. There are no ads or any affiliate links.
|
||||
This guide is an open-source non-profit initiative, [licensed](LICENSE.html) under **Creative Commons Attribution-ShareAlike 4.0 International** ([cc-by-sa-4.0](https://creativecommons.org/licenses/by-sa/4.0/) <sup>[[Archive.org]](https://web.archive.org/web/https://creativecommons.org/licenses/by-sa/4.0/)</sup>) and is **not sponsored/endorsed by any commercial/governmental entity**. This means that you are free to use our guide for pretty much any purpose **excluding commercially** as long as you do attribute it. There are no ads or any affiliate links.
|
||||
|
||||
**If you would like to make a donation to help this project, you can do so from [here](donations.html) where you will also find the project goals. All the donations will be strictly used within the context of this project. All donations and spendings are logged on the donations page.**
|
||||
|
||||
**Ways to read or export the guide**
|
||||
|
||||
- **In your browser:** [Hitchhiker's Guide](https://www.anonymousplanet.org/guide/) (hosted site). After a local build you can also open `site/guide/index.html` directly.
|
||||
- **In your browser:** [Hitchhiker's Guide](https://anonymousplanet.net/) (hosted site). After a local build you can also open `site/guide/index.html` directly.
|
||||
- **Local HTML preview:** from the repository root, with Python 3 and [MkDocs Material](https://squidfunk.github.io/mkdocs-material/getting-started/) installed (`pip install mkdocs-material`), run `mkdocs serve` and open the URL printed in the terminal (for example `http://127.0.0.1:8000`).
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
anonymousplanet.org
|
||||
anonymousplanet.net
|
||||
|
||||
+124
-114
@@ -1,114 +1,124 @@
|
||||
---
|
||||
title: "Anonymous Planet"
|
||||
description: We are the maintainers of the Hitchhiker's Guide and the PSA Matrix space.
|
||||
schema:
|
||||
"@context": https://schema.org
|
||||
"@type": Organization
|
||||
"@id": https://www.anonymousplanet.org/
|
||||
name: Anonymous Planet
|
||||
url: https://www.anonymousplanet.org/about/
|
||||
logo: ../media/profile.png
|
||||
sameAs:
|
||||
- https://github.com/Anon-Planet
|
||||
- https://opencollective.com/anonymousplanetorg
|
||||
- https://mastodon.social/@anonymousplanet
|
||||
---
|
||||
{ align=right }
|
||||
|
||||
**Anonymous Planet** are the maintainers of the [_Hitchhiker's Guide_](../guide/index.md) and the [_PSA Community_](https://psa.anonymousplanet.org). It is responsible for maintaining the projects and code repositories. This project is part of our ongoing efforts to provide open-source tools and resources for the community, with regular updates and improvements added to the changelog.
|
||||
|
||||
The purpose: providing an introduction to various online tracking techniques, online ID verification techniques, and detailed guidance to creating and maintaining (truly) anonymous online identities. It is written with the hopes that good people (e.g., activists, journalists, scientists, lawyers, whistle-blowers, etc.) will be able to fight oppression, censorship and harassment! The website and projects are free (as in freedom) and not affiliated with any donor or projects discussed.
|
||||
|
||||
??? Note "Where do I start?"
|
||||
|
||||
Start either by going to [the beginning](../guide/index.md) or using the search at top right of the page. It is also available at whatever point you are in your reading.
|
||||
|
||||
??? Note "Notes on the journey"
|
||||
|
||||
This guide is a work in progress. It will probably never be "finished". You may (will) find broken links when you click on some search results and during some navigation steps. Please report these. Otherwise, most of the search functionality is a great experience and can help you find linked topics. Try to search for something in one section of the reading. It will show up in many other places.
|
||||
|
||||
??? Note "Disclaimer"
|
||||
|
||||
There might be some wrong or outdated information in this guide because no one is perfect. Your experience may vary. Remember, check regularly for an updated version of this guide. Please do your own independent, well-thought research. There is no one resource online that can provide 100% security, anonymity, and/or privacy.
|
||||
|
||||
This guide is a non-profit open-source initiative, licensed under Creative Commons **Attribution-NonCommercial** 4.0 International ([cc-by-nc-4.0](https://creativecommons.org/licenses/by-nc/4.0/) <sup>[[Archive.org]](https://web.archive.org/web/https://creativecommons.org/licenses/by-nc/4.0/)</sup>).
|
||||
|
||||
- For mirrors see [Mirrors](../mirrors/index.md) and the links at the bottom right of the page. You should see these on every page.
|
||||
|
||||
- For help in comparing versions see [Comparing versions](../guide/index.md#appendix-a6-comparing-versions)
|
||||
|
||||
Feel free to submit issues **(please do report anything wrong)** using GitHub Issues at: <https://github.com/Anon-Planet/thgtoa/issues>. We also accept Merge Requests (MR) from our Gitlab and many other places. Do not hesitate to report issues and suggestions!
|
||||
|
||||
??? Note "Discuss ideas on Matrix for real-time chat"
|
||||
|
||||
We offer a Matrix.org hosted space of our own. Check it out!
|
||||
|
||||
- Read [the rules](https://psa.anonymousplanet.org/), please
|
||||
- Matrix Room: https://matrix.to/#/#nth:anonymousplanet.net
|
||||
- Matrix Space: https://matrix.to/#/#psa:anonymousplanet.net
|
||||
- Admins: @daskolburn:thomcat.rocks and @thehidden:tchncs.de
|
||||
|
||||
Follow us on:
|
||||
|
||||
- Twitter at <https://twitter.com/AnonyPla>
|
||||
|
||||
- Mastodon at <https://mastodon.social/@anonymousplanet>
|
||||
|
||||
To contact me, see the updated information on the website or send an e-mail to <contact@anonymousplanet.org>
|
||||
|
||||
**Please consider [donating](../guide/index.md#donations) if you enjoy the project and want to support the hosting fees or support the funding of initiatives like the hosting of Tor Exit Nodes.**
|
||||
|
||||
### Recommended Reading
|
||||
|
||||
Some of those resources may, in order to sustain their project, contain or propose:
|
||||
|
||||
- Sponsored commercial content
|
||||
- Monetized content through third party platforms (such as YouTube)
|
||||
- Affiliate links to commercial services
|
||||
- Paid Services such as consultancy
|
||||
- Premium content such as ad-free content or updated content
|
||||
- Merchandising
|
||||
|
||||
_Note that these websites could contain affiliate/sponsored content and/or merchandising. This guide does not endorse and is not sponsored by any commercial entity in any way._
|
||||
|
||||
If you skipped those, you should really still consider viewing this YouTube playlist from the Techlore Go Incognito project (<https://github.com/techlore-official/go-incognito> <sup>[[Archive.org]](https://web.archive.org/web/https://github.com/techlore-official/go-incognito)</sup>) as an introduction before going further: <https://www.youtube.com/playlist?list=PL3KeV6Ui_4CayDGHw64OFXEPHgXLkrtJO> <sup>[[Invidious]](https://yewtu.be/playlist?list=PL3KeV6Ui_4CayDGHw64OFXEPHgXLkrtJO)</sup>. This guide will cover many of the topics in the videos of this playlist with more details and references as well as some added topics not covered within that series. This will just take you 2 or 3 hours to watch it all.
|
||||
|
||||
_Anonymous Planet_ **does not** participate in any sponsoring, endorsement, advertising, or other affiliate programs for any entity. We only rely on anonymous donations in a closed, transparent loop system.
|
||||
|
||||
??? Note "Privacy related"
|
||||
|
||||
- AnarSec: <https://www.anarsec.guide/>
|
||||
- EFF Surveillance Self-Defense: <https://ssd.eff.org/>
|
||||
- Prism-Break: <https://prism-break.org/>
|
||||
- Privacy Guides: <https://privacyguides.org>
|
||||
- Techlore: <https://techlore.tech>
|
||||
- The New Oil: <https://thenewoil.org>
|
||||
- PrivacyTools.io: <https://privacytools.io>
|
||||
|
||||
??? Note "Blogs and personal websites"
|
||||
|
||||
- CIA Officer's Blog: <https://officercia.mirror.xyz/>
|
||||
- Continuing Ed: <https://edwardsnowden.substack.com/>
|
||||
- Madaidan's Insecurities: <https://madaidans-insecurities.github.io/>
|
||||
- Seirdy's Home: <https://seirdy.one/>
|
||||
|
||||
??? Note "Useful resources"
|
||||
|
||||
- KYC? Not me: <https://kycnot.me/>
|
||||
- Library Genesis: <https://en.wikipedia.org/wiki/Library_Genesis> <sup>[[Wikiless]](https://wikiless.com/wiki/Library_Genesis)</sup> (see their latest known URL in the Wikipedia article)
|
||||
- Real World Onion Sites: <https://github.com/alecmuffett/real-world-onion-sites>
|
||||
- Sci-Hub <https://en.wikipedia.org/wiki/Sci-Hub> <sup>[[Wikiless]](https://wikiless.com/wiki/Sci-Hub)</sup> (see their latest known URL in the main Wikipedia article)
|
||||
- Terms of Service, Didn't Read: <https://tosdr.org>
|
||||
- Whonix Documentation: <https://www.whonix.org/wiki/Documentation>
|
||||
|
||||
??? Note "We are not affiliated with Anonymous or Riseup"
|
||||
|
||||
One or two of our community members uses or has used the resources of Riseup. We are not affiliated with Riseup in any manner.
|
||||
|
||||
We also hold **no affiliation** with the [Anonymous](https://en.wikipedia.org/wiki/Anonymous_(hacker_group)) <sup>[[Wikiless]](https://wikiless.com/wiki/Anonymous_(hacker_group))</sup> <sup>[[Archive.org]](https://web.archive.org/web/https://en.wikipedia.org/wiki/Anonymous_(hacker_group))</sup> hacker collective.
|
||||
|
||||
## License
|
||||
|
||||
!!! Danger ""
|
||||
|
||||
:fontawesome-brands-creative-commons: :fontawesome-brands-creative-commons-by: :fontawesome-brands-creative-commons-nd: This guide is an open-source non-profit initiative, licensed under [Creative Commons Attribution-NonCommercial 4.0 International](https://github.com/Anon-Planet/thgtoa/blob/master/LICENSE.md) and is not sponsored/endorsed by any commercial/governmental entity. This means that you are free to use our guide for pretty much any purpose excluding commercially as long as you do attribute it. There are no ads or any affiliate links.
|
||||
---
|
||||
title: "Anonymous Planet"
|
||||
description: "We are the maintainers of the Hitchhiker's Guide and the PSA Matrix space."
|
||||
hide:
|
||||
- toc
|
||||
schema:
|
||||
"@context": https://schema.org
|
||||
"@type": Organization
|
||||
"@id": https://anonymousplanet.net/
|
||||
name: Anonymous Planet
|
||||
url: https://anonymousplanet.net/about/
|
||||
logo: ../media/profile.png
|
||||
sameAs:
|
||||
- https://github.com/Anon-Planet
|
||||
- https://opencollective.com/anonymousplanetorg
|
||||
- https://mastodon.social/@anonymousplanet
|
||||
---
|
||||
|
||||
<div class="hero-block">
|
||||
<div class="hero-eyebrow">Made by people with skin in the game.</div>
|
||||
<h1 class="hero-title">Anonymous Planet<span class="hero-subtitle">Open source. No ads. No affiliates. No profit.</span></h1>
|
||||
<p class="hero-tagline">
|
||||
Written by people with backgrounds in signals intelligence, forensics, and Linux systems,
|
||||
not influencers with affiliate links.
|
||||
</p>
|
||||
<div class="hero-cta-row">
|
||||
<a href="../guide/" class="hero-cta hero-cta--primary">Read the Guide</a>
|
||||
<a href="../contribute/" class="hero-cta hero-cta--secondary">Support the Project</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Who We Are { #who }
|
||||
|
||||
Anonymous Planet are the maintainers of [_The Hitchhiker's Guide_](../guide/index.md) and the [_PSA Community_](https://psa.anonymousplanet.net). The project exists to provide open-source tools and resources for people who need them: activists, journalists, scientists, lawyers, whistleblowers, and anyone facing oppression, censorship, or harassment.
|
||||
|
||||
We have extensive backgrounds in signals intelligence, forensics, and various Linux distributions. We are not a privacy influencer operation with a bunch of recommendations and affiliate links. Everything here is compiled into a single, cohesive set of instructions and sub-guides — free as in freedom.
|
||||
|
||||
We are high-risk individuals. We do this because it matters.
|
||||
|
||||
!!! note "Disclaimer"
|
||||
This guide is a work in progress and will probably never be "finished". Some information may be outdated. Always cross-reference independently and check for updated versions regularly. No single resource can provide 100% security, anonymity, or privacy.
|
||||
|
||||
!!! tip "Where to start"
|
||||
Go to [the beginning of the guide](../guide/index.md) or use the search at the top right. The [Techlore Go Incognito playlist](https://www.youtube.com/playlist?list=PL3KeV6Ui_4CayDGHw64OFXEPHgXLkrtJO) is also a good 2–3 hour introduction before diving in.
|
||||
|
||||
---
|
||||
|
||||
## Community { #community }
|
||||
|
||||
<div class="index-grid">
|
||||
|
||||
<!-- <div class="index-card">
|
||||
<h3 class="index-card__title">Matrix Space</h3>
|
||||
<p class="index-card__body">Our Matrix.org hosted community space. Read the rules before joining. Admins: <code>@daskolburn:thomcat.rocks</code> and <code>@thehidden:tchncs.de</code>.</p>
|
||||
<a href="https://matrix.to/#/#psa:anonymousplanet.net" class="index-card__link">Join the Space</a>
|
||||
<a href="https://matrix.to/#/#nth:anonymousplanet.net" class="index-card__link" style="margin-top:0.4em;">Join the Room</a>
|
||||
<a href="https://psa.anonymousplanet.net/" class="index-card__link" style="margin-top:0.4em;">Read the Rules</a>
|
||||
</div> -->
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Mastodon</h3>
|
||||
<p class="index-card__body">Follow us for announcements on the Fediverse.</p>
|
||||
<a href="https://ioc.exchange/@anonymousplanet" class="index-card__link" rel="me">@anonymousplanet@ioc.exchange</a>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">X / Twitter</h3>
|
||||
<p class="index-card__body">Announcements also cross-posted to X.</p>
|
||||
<a href="https://x.com/anonplanetorg" class="index-card__link">@anonplanetorg</a>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">GitHub Issues</h3>
|
||||
<p class="index-card__body">Report errors, broken links, or outdated content. Pull requests welcome across all our mirrors.</p>
|
||||
<a href="https://github.com/Anon-Planet/thgtoa/issues" class="index-card__link">Open an issue</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Contact { #contact }
|
||||
|
||||
Email: <contact@anonymousplanet.net>
|
||||
|
||||
For mirrors, see the [Mirrors](../mirrors/index.md) page and the links in the site footer.
|
||||
For version comparison, see [Comparing Guide Versions](../guide/index.md#comparing-versions).
|
||||
|
||||
---
|
||||
|
||||
## Recommended Reading { #reading }
|
||||
|
||||
!!! warning "Disclosure"
|
||||
Some linked resources may contain sponsored content, affiliate links, paid services, or monetized content. Anonymous Planet does not endorse and is not sponsored by any commercial entity. We do not participate in any affiliate programs.
|
||||
|
||||
??? tip "Privacy & security"
|
||||
- [AnarSec](https://www.anarsec.guide/)
|
||||
- [EFF Surveillance Self-Defense](https://ssd.eff.org/)
|
||||
- [Prism-Break](https://prism-break.org/)
|
||||
- [Privacy Guides](https://privacyguides.org)
|
||||
- [Techlore](https://techlore.tech)
|
||||
- [The New Oil](https://thenewoil.org)
|
||||
- [PrivacyTools.io](https://privacytools.io)
|
||||
|
||||
??? tip "Blogs"
|
||||
- [CIA Officer's Blog](https://officercia.mirror.xyz/)
|
||||
- [Edward Snowden — Continuing Ed](https://edwardsnowden.substack.com/)
|
||||
- [Madaidan's Insecurities](https://madaidans-insecurities.github.io/)
|
||||
- [Seirdy's Home](https://seirdy.one/)
|
||||
|
||||
??? tip "Useful resources"
|
||||
- [KYC? Not me](https://kycnot.me/)
|
||||
- [Library Genesis](https://en.wikipedia.org/wiki/Library_Genesis)
|
||||
- [Real World Onion Sites](https://github.com/alecmuffett/real-world-onion-sites)
|
||||
- [Sci-Hub](https://en.wikipedia.org/wiki/Sci-Hub)
|
||||
- [Terms of Service, Didn't Read](https://tosdr.org)
|
||||
- [Whonix Documentation](https://www.whonix.org/wiki/Documentation)
|
||||
|
||||
---
|
||||
|
||||
!!! note "We are not affiliated with Anonymous or Riseup"
|
||||
We hold no affiliation with the [Anonymous](https://en.wikipedia.org/wiki/Anonymous_(hacker_group)) hacker collective or with Riseup. One or two community members uses Riseup resources personally — that is the full extent of any connection.
|
||||
|
||||
+101
-22
@@ -1,12 +1,12 @@
|
||||
---
|
||||
title: "Release Notes"
|
||||
description: "Release Notes"
|
||||
title: "Changelog"
|
||||
description: "Release notes for the Hitchhiker's Guide to Online Anonymity."
|
||||
schema:
|
||||
"@context": https://schema.org
|
||||
"@type": Organization
|
||||
"@id": https://www.anonymousplanet.org/
|
||||
"@id": https://anonymousplanet.net/
|
||||
name: Anonymous Planet
|
||||
url: https://www.anonymousplanet.org/authors/
|
||||
url: https://anonymousplanet.net/changelog/
|
||||
logo: ../media/profile.png
|
||||
sameAs:
|
||||
- https://github.com/Anon-Planet
|
||||
@@ -14,45 +14,122 @@ schema:
|
||||
- https://mastodon.social/@anonymousplanet
|
||||
---
|
||||
|
||||
# Release Notes
|
||||
|
||||
Notable changes to the guide and its tooling. Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
<div class="hero-block">
|
||||
<div class="hero-eyebrow">Auto-generated after every release.</div>
|
||||
<h1 class="hero-title">Changelog<span class="hero-subtitle">What changed, and when.</span></h1>
|
||||
<p class="hero-tagline">
|
||||
Follows <a href="https://keepachangelog.com/en/1.1.0/">Keep a Changelog</a> and
|
||||
<a href="https://semver.org/spec/v2.0.0.html">Semantic Versioning</a>.
|
||||
Generated from conventional commits by machine, only.
|
||||
</p>
|
||||
<div class="hero-cta-row">
|
||||
<a href="https://github.com/Anon-Planet/thgtoa/releases" class="hero-cta hero-cta--primary">GitHub Releases</a>
|
||||
<a href="../verify/" class="hero-cta hero-cta--secondary">Verify a Release</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change nothing below this line, please. -->
|
||||
---
|
||||
|
||||
## [v1.2.3] — 2026-05-22
|
||||
## [v1.2.5]
|
||||
|
||||
CI/CD pipeline split into independent stages, dark PDF quality improved, and the changelog is now updated automatically on every release. v1.2.2 was just a placeholder, this is a minor but CI breaking change.
|
||||
!!! Note "Meta"
|
||||
|
||||
!!! success "Added"
|
||||
- Website theme customization
|
||||
- License change to accomodate our fiscal host Open Source Collective
|
||||
|
||||
- **Dark mode PDF** (`scripts/convert.py`): pixel-level converter replaces the broken `--prefers-color-scheme=dark` Chromium flag. Produces a 200 DPI hacker-themed PDF (`#1f1f31` background, `#e0e0e0` text, `#5e8bde` links) with batched page processing to avoid OOM.
|
||||
!!! Note "Changed"
|
||||
|
||||
- Comprehensive updates throughout the guide reflecting the transition to Whonix 18.x as the newest version
|
||||
- Replaced outdated Whonix.org wiki docs links with GitHub releases mirror and Archive.org backups
|
||||
- Updated Virtualbox hardening instructions (e.g., network time desync offsets, Spectre/Meltdown mitigations)
|
||||
- Added AppArmor enabling guidance for Debian-based Whonix 18.x Workstation VMs
|
||||
- Upgraded download instructions to use GitHub Releases as primary source with Archive.org fallback
|
||||
- Added new "Whonix Improvements" section documenting key changes from 17.x to 18.x (automated release upgrade, improved Qubes integration)
|
||||
- Updated Qubes OS compatibility notes: Whonix 18.x officially supported on R4.3+, Whonix 17.x compatible with R4.2+
|
||||
- Enhanced system verification instructions after upgrades (checkvm, tor --verify)
|
||||
- Improved documentation links to use clean markdown format with Archive.org mirrors for resilience
|
||||
- Added comprehensive upgrade path guidance with backup procedures
|
||||
- Updated 196! Wikipedia reference links throughout the entire guide
|
||||
|
||||
!!! Note "Added"
|
||||
|
||||
- All Whonix.org/wiki links now have Archive.org mirror backups for availability
|
||||
- Upgrade path documented: Whonix 17 to 18 with automated release-upgrade support
|
||||
- Detailed Qubes OS compatibility notes for both Whonix versions
|
||||
- You can now get the Anonymous Planet PGP keyring from the site (copy/paste or download)
|
||||
|
||||
!!! Note "Improved"
|
||||
|
||||
- Virtualbox hardening section reorganized
|
||||
- AppArmor configuration guidance added where applicable (Whonix 18.x)
|
||||
- KVM alternative documented in Appendix N for Linux users seeking better security than VirtualBox
|
||||
- Whonix Improvements subsection added before "Pick your guest workstation" VMs section
|
||||
- Removed appendix-style writing
|
||||
|
||||
## [v1.2.4]
|
||||
|
||||
!!! Note "Meta"
|
||||
|
||||
- Rename workflows (GH - now we can know the order)
|
||||
|
||||
!!! Note "Changed"
|
||||
|
||||
- Change the repo URL for our tor mirror
|
||||
- Fix recommended reading admonition
|
||||
- Refactoring some things and removing others
|
||||
- More meta changes to the pipeline
|
||||
- Rewrite developer guide for current pipeline
|
||||
|
||||
!!! Note "Fixed"
|
||||
|
||||
- Fix an inline reference
|
||||
- Use the Anonymous Planet RSK for releases (we used the MSK for testing)
|
||||
- Prevent history dump and filter noise commits
|
||||
- Actually save per-page PDFs for qpdf, not PNGs
|
||||
- Fail fast with helpful message if pdftoppm or qpdf missing
|
||||
|
||||
## [v1.2.3]
|
||||
|
||||
CI/CD pipeline split into independent stages, dark PDF quality improved, release signing automated, and the changelog now updates itself on every build. Skipping v1.2.2 which was a placeholder and contained broken Python unsuitable for a tag/release.
|
||||
|
||||
???+ tip "Added"
|
||||
|
||||
- **Dark mode PDF** (`scripts/convert.py`): pixel-level converter replaces the broken `--prefers-color-scheme=dark` Chromium flag. Produces a 200 DPI hacker-themed PDF (`#1f1f31` background, `#e0e0e0` text, `#5e8bde` links) with batched page processing to avoid OOM on large documents.
|
||||
- **Three independent CI workflows** replacing the old monolithic `build-sign-release.yml`:
|
||||
- `build.yml` — builds PDFs and uploads them as an artifact; no secrets required.
|
||||
- `sign.yml` — downloads the PDF artifact, computes SHA-256 and BLAKE2b hashes, GPG-signs all outputs, and uploads a `signatures` artifact. Can be re-run against any historical build.
|
||||
- `release.yml` — downloads both artifacts, uploads to VirusTotal, and publishes a tagged GitHub Release with all 12 assets attached. **Can be triggered manually against any previous sign run**.
|
||||
- **`scripts/update_changelog.py`**: reads `git log` since the last version tag, categorises commits by conventional-commit prefix, and prepends a new entry here automatically after each successful build.
|
||||
- **`changelog.yml`** workflow: commits the auto-generated changelog entry back to `main` after every build, with `dry_run` and `manual_version` dispatch inputs for testing.
|
||||
- `01-build.yml`: builds PDFs and uploads them as an artifact; no secrets required, can be re-run freely.
|
||||
- `02-sign.yml`: downloads the PDF artifact, computes SHA-256 and BLAKE2b hashes, GPG-signs all outputs, and uploads a `signatures` artifact. Can be re-run against any historical build.
|
||||
- `03-release.yml`: downloads both artifacts, uploads to VirusTotal, and publishes a tagged GitHub Release with all 12 assets attached. Can be triggered manually against any previous sign run.
|
||||
- **`scripts/update_changelog.py`**: reads `git log` since the last version tag, categorises commits by conventional-commit prefix, and prepends a new entry to this file automatically after each successful build.
|
||||
- **`04-changelog.yml`** workflow: commits the auto-generated changelog entry back to `main` after every build, with `dry_run` and `manual_version` dispatch inputs for safe local testing.
|
||||
- **`scripts/tag_release.py`**: interactive guided helper for maintainers to create GPG-signed annotated tags. Checks clean tree and branch, auto-increments the version, pulls the message from the changelog, resolves the release signing key, creates and verifies the tag, then prints the push command.
|
||||
- **`docs/code/develop.md`**: full developer reference covering prerequisites, local build instructions, the pipeline flow, all required GitHub Secrets, the release process, verification steps, and a troubleshooting section for every known CI failure mode.
|
||||
|
||||
!!! warning "Changed"
|
||||
|
||||
- `build-sign-release.yml` is now deprecated — push triggers removed, manual dispatch only. Will be deleted once in-flight runs complete.
|
||||
- The full pipeline (build → sign → release) now chains automatically via `workflow_run` on every push to `main`.
|
||||
- `build-sign-release.yml` deprecated (now removed) - push triggers removed, manual dispatch only. Will be deleted once in-flight runs complete.
|
||||
- The full pipeline (build → sign → release → changelog) now chains automatically via `workflow_run` on every push to `main`.
|
||||
- GPG signing uses `--pinentry-mode loopback` and `--passphrase-fd 0` to avoid interactive prompts on headless runners.
|
||||
- VirusTotal scans moved to the release stage so they run once per release, not once per build.
|
||||
- `.gitignore` updated to track `.b2` per-file hash files alongside existing `.sha256` and `.sig` entries.
|
||||
- Stale information removed from the guide; deprecated ODT section in Appendix A6 commented out.
|
||||
- Footer copyright information corrected.
|
||||
|
||||
!!! bug "Fixed"
|
||||
|
||||
- Broken internal links and a mismatched cross-reference in `docs/about/index.md`.
|
||||
- Deprecated ODT section commented out in Appendix A6 of the guide.
|
||||
- `_save_images_as_pdf` in `convert.py` was passing raw PNG files to `qpdf --pages`, which only accepts PDF inputs. Fixed by quantizing each page to palette mode (256 colours, FASTOCTREE) and saving as a single-page PDF before merging.
|
||||
- `convert.py` now fails immediately with install instructions if `pdftoppm` or `qpdf` are missing, instead of crashing with an unhelpful `FileNotFoundError`.
|
||||
- Pillow `KeyError: 'JPEG'` on CI resolved by installing `mkdocs-material[imaging]` and using palette-mode PDF encoding instead of RGB+JPEG.
|
||||
- Orphaned footnote citations `[^536]` and `[^537]` (Australian privacy law and the Identify and Disrupt Act) restored at the key disclosure law paragraph in the guide.
|
||||
- Broken internal links and mismatched cross-references throughout the guide corrected.
|
||||
|
||||
---
|
||||
|
||||
## [v1.2.1] — 2025
|
||||
## [v1.2.1]
|
||||
|
||||
First automated PDF build and the start of the CI pipeline.
|
||||
|
||||
!!! success "Added"
|
||||
???+ tip "Added"
|
||||
|
||||
- `scripts/build_guide_pdf.py`: builds the MkDocs site and renders the full guide to a single PDF via headless Chromium (Chrome or Edge). Supports `--dark`, `--light`, and `--both` modes.
|
||||
- GitHub Actions workflow that installs Chromium, runs the build script, and uploads `export/thgtoa.pdf` as an artifact on every push to `main` or manual dispatch.
|
||||
@@ -71,5 +148,7 @@ First automated PDF build and the start of the CI pipeline.
|
||||
|
||||
---
|
||||
|
||||
[v1.2.5]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.5
|
||||
[v1.2.4]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.4
|
||||
[v1.2.3]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.3
|
||||
[v1.2.1]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.1
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# Development
|
||||
|
||||
??? Note "How the pipeline works"
|
||||
|
||||
**Automatic PDF Generation:** - Builds both light and dark mode PDFs from MkDocs source
|
||||
**SHA256 Hash Generation:** - Creates hash files for integrity verification
|
||||
**GPG Signature Signing:** - Signs all PDFs and hash files with repository GPG key
|
||||
**VirusTotal Scanning:** - Automatically scans PDFs and updates release notes
|
||||
**Release Automation:** - Packages everything into GitHub releases
|
||||
|
||||
## Architecture
|
||||
|
||||
### Build PDF Workflow (`build-sign-release.yml`)
|
||||
|
||||
!!! Note "Steps"
|
||||
|
||||
- Checkout repository
|
||||
- Set up Python and MkDocs Material
|
||||
- Install Chromium browser
|
||||
- Generate both light and dark mode PDFs with `scripts\build_guide_pdf.py`
|
||||
- Create SHA256 and blake2 hash files in `export/`
|
||||
- Sign all files with GPG in `export/`
|
||||
- Upload artifacts to GitHub Actions **manually**
|
||||
|
||||
### SHA256 Hash Verification
|
||||
|
||||
!!! Note "**How it works**"
|
||||
|
||||
- Each PDF gets a unique SHA256 hash calculated at build time
|
||||
- Hash stored in `.sha256` files alongside the PDFs
|
||||
- Combined `sha256sum.txt` for batch verification
|
||||
|
||||
### GPG Signature Verification
|
||||
|
||||
**Purpose:** Verify authenticity and prevent tampering
|
||||
|
||||
!!! Note "How it works"
|
||||
|
||||
- Detached signatures created for each PDF and hash file
|
||||
- Public keys available in `/pgp/` directory
|
||||
|
||||
**Verification command:**
|
||||
```bash
|
||||
gpg --import pgp/anonymousplanet-master.asc
|
||||
gpg --verify export/thgtoa.pdf.sig export/thgtoa.pdf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*This workflow is designed for security-conscious users who need to verify the authenticity and integrity of downloaded documents.*
|
||||
+302
-62
@@ -1,69 +1,309 @@
|
||||
---
|
||||
title: Content Contributions
|
||||
title: "Content Contributions"
|
||||
description: "How to contribute to the Hitchhiker's Guide — building, signing, releasing, and writing."
|
||||
hide:
|
||||
- toc
|
||||
schema:
|
||||
"@context": https://schema.org
|
||||
"@type": Organization
|
||||
"@id": https://anonymousplanet.net/
|
||||
name: Anonymous Planet
|
||||
url: https://anonymousplanet.net/code/
|
||||
logo: ../media/profile.png
|
||||
sameAs:
|
||||
- https://github.com/Anon-Planet
|
||||
- https://opencollective.com/anonymousplanetorg
|
||||
---
|
||||
You can [submit bugs and feature requests](https://github.com/Anon-Planet/thgtoa/issues/new) with detailed information about your issue or idea:
|
||||
|
||||
- If you'd like to propose an addition, please follow the standards outlined here.
|
||||
- If you're reporting an issue, please be sure to include the expected behaviour, the observed behaviour, and steps to reproduce the problem.
|
||||
- This can require technical knowledge, but you can also get involved in conversations about bug reports and feature requests. This is a great way to get involved without getting too overwhelmed!
|
||||
- [Help fellow committers test recently submitted pull requests](https://github.com/Anon-Planet/thgtoa/pulls). Simply by pulling down a pull request and testing it, you can help ensure our new code contributions for stability and quality.
|
||||
|
||||
For those of you who are looking to add content to the guide, include the following:
|
||||
|
||||
##### <u>Pull Requests</u>
|
||||
|
||||
- **Do** create a [topic branch] to work on instead of working directly on `main`. This helps to:
|
||||
+ Protect the process.
|
||||
+ Ensures users are aware of commits on the branch being considered for merge.
|
||||
+ Allows for a location for more commits to be offered without mingling with other contributor changes.
|
||||
+ Allows contributors to make progress while a PR is still being reviewed.
|
||||
- **Do** follow the [50/72 rule] for Git commit messages.
|
||||
- **Do** write "WIP" on your PR and/or open a [draft PR] if submitting unfinished changes..
|
||||
- **Do** make sure the title of a draft PR makes it immediately clear that it's a draft
|
||||
- **Do** target your pull request to the **main branch**.
|
||||
- **Do** specify a descriptive title to make searching for your pull request easier.
|
||||
- **Don't** leave your pull request description blank.
|
||||
- **Don't** abandon your pull request. Being responsive helps us land your changes faster.
|
||||
- **Don't** post questions in older closed PRs.
|
||||
- **Do** stick to the guide to find common style issues.
|
||||
- **Don't** make mass changes (such as replacing "I" with "we") using automated serach/replace functionality.
|
||||
+ Search/replace doesn't understand context, and as such, will inevitably cause inconsistencies and make the guide harder to read.
|
||||
+ If it's part of a larger PR, it'll also make the reviewer's life harder, as they'll have to go through manually and undo everything by hand.
|
||||
+ _If you're going to make mass changes, take the time to do it properly_. Otherwise we'll just have to undo it anyway.
|
||||
+ If your change contains backslashes (`\`), either escape them with another backslash (`\\`) or put them in a ```code block```.
|
||||
|
||||
When reporting guide issues:
|
||||
|
||||
- **Do** write a detailed description of your issue and use a descriptive title.
|
||||
- **Do** make it as detailed as possible and don't just submit 50 line changes without explaining.
|
||||
- **Don't** file duplicate reports; search for your bug before filing a new report.
|
||||
- **Don't** attempt to report issues on a closed PR.
|
||||
|
||||
### Large PRs
|
||||
|
||||
Please split large sets of changes into multiple PRs. For example, a PR that adds Windows 11 support, removes Windows AME references, and fixes typos can be split into 3 PRs. This makes PRs easier to review prior to merging.
|
||||
|
||||
For an example of what _not_ to do, see: <https://github.com/Anon-Planet/thgtoa/pull/51>. This PR contains enough changes to split into multiple smaller and individually reviewable PRs.
|
||||
|
||||
### Updating PRs
|
||||
|
||||
While a PR is being reviewed, modifications may be made to it by the reviewer prior to merging. If this is the case, a new branch will be created for the PR's review. If you would like to submit a change to a PR that is in the process of being reviewed, _do not update the PR directly_. This will only cause merge conflicts and delay the PR from being merged. Instead, submit your changes to the PR's review branch.
|
||||
|
||||
For an example of what _not_ to do, see: <https://github.com/Anon-Planet/thgtoa/pull/51>. Instead of submitting changes to the PR directly, they should have been submitted as changes to the PR's associated review branch.
|
||||
<div class="hero-block">
|
||||
<div class="hero-eyebrow">Open source. Every PR matters.</div>
|
||||
<h1 class="hero-title">Contribute<span class="hero-subtitle">Help us improve the Hitchhiker's Guide..</span></h1>
|
||||
<p class="hero-tagline">
|
||||
Contributions range from fixing a typo to writing entire new sections.
|
||||
</p>
|
||||
<div class="hero-cta-row">
|
||||
<a href="https://github.com/Anon-Planet/thgtoa/issues/new" class="hero-cta hero-cta--primary">Open an Issue</a>
|
||||
<a href="#pipeline" class="hero-cta hero-cta--secondary">Release Pipeline</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
**Thank you** for taking the few moments to read this far! You're already way ahead of the
|
||||
curve, so keep it up!
|
||||
## Setup { #setup }
|
||||
|
||||
[discussions]: https://github.com/Anon-Planet/thgtoa/discussions
|
||||
[issues]: https://github.com/Anon-Planet/thgtoa/issues
|
||||
[help fellow users with open issues]: https://github.com/Anon-Planet/thgtoa/issues
|
||||
[topic branch]: http://git-scm.com/book/en/Git-Branching-Branching-Workflows#Topic-Branches
|
||||
[Qubes#7457]: https://github.com/QubesOS/qubes-issues/issues/7457
|
||||
[50/72 rule]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
|
||||
[draft pr]: https://help.github.com/en/articles/about-pull-requests#draft-pull-requests
|
||||
[console output]: https://docs.github.com/en/free-pro-team@latest/github/writing-on-github/creating-and-highlighting-code-blocks#fenced-code-blocks
|
||||
[verification steps]: https://docs.github.com/en/free-pro-team@latest/github/writing-on-github/basic-writing-and-formatting-syntax#task-lists
|
||||
[reference associated issues]: https://github.com/blog/1506-closing-issues-via-pull-requests
|
||||
[help fellow committers test recently submitted pull requests]: https://github.com/Anon-Planet/thgtoa/pulls
|
||||
Install these before anything else.
|
||||
|
||||
=== "Linux / macOS"
|
||||
|
||||
```sh
|
||||
# Python 3.11+
|
||||
python3 --version
|
||||
|
||||
# poppler (pdftoppm) and qpdf
|
||||
sudo apt install poppler-utils qpdf # Debian/Ubuntu
|
||||
brew install poppler qpdf # macOS
|
||||
|
||||
# GPG
|
||||
sudo apt install gnupg # Debian/Ubuntu
|
||||
brew install gnupg # macOS
|
||||
|
||||
# Python dependencies
|
||||
pip install "mkdocs-material[imaging]" pillow numpy
|
||||
```
|
||||
|
||||
=== "Windows"
|
||||
|
||||
```powershell
|
||||
# Python 3.11+ from https://python.org
|
||||
|
||||
# poppler: https://github.com/oschwartz10612/poppler-windows/releases
|
||||
# Extract and add the bin\ folder to PATH
|
||||
|
||||
# qpdf: https://github.com/qpdf/qpdf/releases
|
||||
# Extract and add the bin\ folder to PATH
|
||||
|
||||
# GPG: https://gpg4win.org
|
||||
|
||||
# Python dependencies
|
||||
pip install "mkdocs-material[imaging]" pillow numpy
|
||||
```
|
||||
|
||||
You also need **Google Chrome** or **Microsoft Edge** installed for the light-mode PDF build (headless Chromium).
|
||||
|
||||
---
|
||||
|
||||
## Repository Layout { #layout }
|
||||
|
||||
```txt
|
||||
.github/
|
||||
workflows/
|
||||
01-build.yml # builds PDFs, uploads artifact
|
||||
02-sign.yml # hashes + GPG signs, uploads signatures artifact
|
||||
03-release.yml # publishes GitHub Release with all assets
|
||||
04-changelog.yml # prepends a new entry to docs/changelog/index.md
|
||||
publish.yml # deploys MkDocs site to GitHub Pages
|
||||
docs/
|
||||
guide/index.md # the guide (single Markdown file)
|
||||
changelog/ # release notes
|
||||
code/ # this page
|
||||
export/ # PDF output (PDFs gitignored; .sha256, .b2sum, .asc tracked)
|
||||
pgp/ # public signing keys
|
||||
scripts/
|
||||
build_guide_pdf.py # MkDocs + Chromium PDF builder
|
||||
convert.py # pixel-based dark mode PDF converter
|
||||
install_fonts.py # install fonts locally
|
||||
update_changelog.py # auto-generates changelog entries from git log
|
||||
setup_workflow.py # GitHub Secrets setup assistant
|
||||
verify_pdf.py # signature verification helper
|
||||
archived/
|
||||
tag_release.py # ARCHIVED — GPG tag helper (not used in current flow)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Building Locally { #build }
|
||||
|
||||
```sh
|
||||
python scripts/build_guide_pdf.py --both
|
||||
```
|
||||
|
||||
Builds the MkDocs site, renders it to `export/thgtoa.pdf` via headless Chromium, then produces `export/thgtoa-dark.pdf`.
|
||||
|
||||
| Flag | Effect |
|
||||
|------|--------|
|
||||
| `--both` | Light PDF then dark PDF |
|
||||
| _(none)_ | Light PDF only |
|
||||
| `--dark` | Dark PDF only (light PDF must already exist) |
|
||||
|
||||
Build only the dark PDF from an existing light:
|
||||
|
||||
```sh
|
||||
python scripts/convert.py export/thgtoa.pdf export/thgtoa-dark.pdf
|
||||
```
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--dpi` | `200` | Rasterization DPI |
|
||||
| `--batch-size` | `50` | Pages per batch — reduce if OOM |
|
||||
| `--bg` | `1f1f31` | Background colour (hex) |
|
||||
| `--text` | `e0e0e0` | Body text colour (hex) |
|
||||
| `--link` | `5e8bde` | Link colour (hex) |
|
||||
|
||||
Preview the site:
|
||||
|
||||
```sh
|
||||
mkdocs serve
|
||||
# Opens at http://127.0.0.1:8000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contributing Content { #contributing }
|
||||
|
||||
<div class="index-grid">
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Use a Topic Branch</h3>
|
||||
<p class="index-card__body">Never commit directly to <code>main</code>. Use a topic branch per change so PRs stay reviewable and independent.</p>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Small PRs</h3>
|
||||
<p class="index-card__body">Split large changes into multiple PRs — one for new content, one for fixes, one for style. Big PRs block merges and create review debt.</p>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Conventional Commits</h3>
|
||||
<p class="index-card__body">All commits must follow <code><type>(<scope>): <description></code> format. Enforced by the <code>commitizen</code> pre-commit hook.</p>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Describe Your Changes</h3>
|
||||
<p class="index-card__body">Never leave a PR description blank. Include what changed, why, and any context a reviewer needs. Link related issues.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
### Commit Types
|
||||
|
||||
| Type | Changelog bucket |
|
||||
|------|-----------------|
|
||||
| `feat`, `feature`, `add` | Added |
|
||||
| `fix`, `bugfix`, `revert`, `security` | Fixed |
|
||||
| `perf`, `refactor`, `change`, `chore`, `ci`, `docs`, `style`, `test`, `build` | Changed |
|
||||
|
||||
Examples:
|
||||
|
||||
```sh
|
||||
feat: add dark-mode PDF export
|
||||
fix(scripts): handle locked PDF on Windows
|
||||
docs: update developer workflow guide
|
||||
chore(ci): pin Chrome version to 120
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
- **Do** target PRs at the `main` branch
|
||||
- **Do** write "WIP" or open a draft PR for unfinished work
|
||||
- **Do** follow the [50/72 rule](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) for commit messages
|
||||
- **Don't** make mass search/replace changes without context-checking every instance
|
||||
- **Don't** abandon a PR mid-review — stay responsive
|
||||
- **Don't** modify a PR directly while it's under active review — submit changes to the review branch instead
|
||||
|
||||
For an example of what _not_ to do, see [PR #51](https://github.com/Anon-Planet/thgtoa/pull/51).
|
||||
|
||||
!!! warning "Before you push"
|
||||
- Make sure the working tree is clean (`git status`)
|
||||
- Run `mkdocs build` locally if you changed `docs/` to catch broken links
|
||||
- If you added footnotes, verify each has both a definition `[^N]:` and at least one inline citation `[^N]`
|
||||
|
||||
---
|
||||
|
||||
## Release Pipeline { #pipeline }
|
||||
|
||||
The pipeline is fully manual after the initial build — no step triggers the next automatically. This prevents version mismatches between what was built, what was signed, and what gets released.
|
||||
|
||||
```txt
|
||||
push to main (or manual trigger)
|
||||
│
|
||||
▼
|
||||
01-build.yml
|
||||
Builds thgtoa.pdf + thgtoa-dark.pdf.
|
||||
Uploads artifact: pdfs — note the run ID.
|
||||
│
|
||||
│ manually trigger 02-sign.yml with the build run ID
|
||||
▼
|
||||
02-sign.yml
|
||||
Hashes (SHA-256 + BLAKE2b) and GPG-signs all files.
|
||||
Commits export/ back to main.
|
||||
Uploads: signatures, pdfs-signed — note the run ID.
|
||||
│
|
||||
│ manually trigger 03-release.yml with the sign run ID
|
||||
▼
|
||||
03-release.yml
|
||||
Runs VirusTotal. Creates GitHub Release tagged release-YYYYMMDD-<short-sha>.
|
||||
│
|
||||
│ manually trigger 04-changelog.yml with the version string
|
||||
▼
|
||||
04-changelog.yml
|
||||
Prepends a new ## [vX.Y.Z] entry to docs/changelog/index.md and commits.
|
||||
```
|
||||
|
||||
### Release Tags
|
||||
|
||||
Tags use the format `release-YYYYMMDD-<short-sha>`, e.g. `release-20260527-abc1234`. No version decision is needed at release time — the tag is always unique and traceable to the exact commit.
|
||||
|
||||
The version string (e.g. `v1.2.4`) is a separate, human-assigned label that lives only in the changelog.
|
||||
|
||||
### Triggering Each Step
|
||||
|
||||
**Build:** Push to `main` or go to **Actions → Build PDFs → Run workflow**. Note the run ID.
|
||||
|
||||
**Sign:** **Actions → Sign PDFs → Run workflow**, enter the build run ID. Note the run ID.
|
||||
|
||||
**Release:** **Actions → Release → Run workflow**, enter the sign run ID.
|
||||
|
||||
**Changelog:** **Actions → Update Changelog → Run workflow**, enter the version string. Use `dry_run: true` to preview.
|
||||
|
||||
---
|
||||
|
||||
## Verifying a Release { #verify }
|
||||
|
||||
```sh
|
||||
# Import the release signing key
|
||||
gpg --import pgp/anonymousplanet.asc
|
||||
|
||||
# Verify the PDFs
|
||||
gpg --verify thgtoa.pdf.asc thgtoa.pdf
|
||||
gpg --verify thgtoa-dark.pdf.asc thgtoa-dark.pdf
|
||||
|
||||
# Verify the hash files
|
||||
gpg --verify sha256sums.txt.asc sha256sums.txt
|
||||
gpg --verify b2sums.txt.asc b2sums.txt
|
||||
|
||||
# Check the PDF hashes match
|
||||
sha256sum -c sha256sums.txt
|
||||
b2sum -c b2sums.txt
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```txt
|
||||
gpg: Signature made Sun 31 May 2026 03:23:26 AM EDT
|
||||
gpg: using EDDSA key C3023DBEA3FB38C438BA1EEDCEC60AEDE8B992A2
|
||||
gpg: Good signature from "Anonymous Planet Release Signing Key" [ultimate]
|
||||
Primary key fingerprint: C302 3DBE A3FB 38C4 38BA 1EED CEC6 0AED E8B9 92A2
|
||||
```
|
||||
|
||||
You can safely ignore GitHub/Codeberg warnings like "The email in this signature doesn't match the committer email."
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting { #troubleshooting }
|
||||
|
||||
**`cairosvg` missing during MkDocs build**
|
||||
`pip install "mkdocs-material[imaging]"` — required by the `social` plugin.
|
||||
|
||||
**`KeyError: 'JPEG'` in convert.py**
|
||||
`sudo apt install libjpeg-dev && pip install --force-reinstall pillow`
|
||||
|
||||
**`qpdf: can't find PDF header`**
|
||||
qpdf only accepts PDF inputs — ensure you are on the current version of `convert.py`.
|
||||
|
||||
**GPG signing fails — `No secret key`**
|
||||
Re-export: `gpg --armor --export-secret-keys <fingerprint>` and re-paste the full block including headers into the `GPG_PRIVATE_KEY` secret.
|
||||
|
||||
**GPG signing fails — `Bad passphrase`**
|
||||
The `GPG_PASSPHRASE` secret has a trailing space or newline. Re-paste without surrounding whitespace.
|
||||
|
||||
**`03-release.yml` fails on VirusTotal**
|
||||
`VT_API_KEY` is missing, invalid, or over the 500 req/day free-tier limit. Re-run after a few minutes.
|
||||
|
||||
**`02-sign.yml` fails downloading PDF artifact**
|
||||
Wrong `build_run_id`, or the artifact expired (90-day retention). Trigger a new build.
|
||||
|
||||
**Changelog already contains version X**
|
||||
`update_changelog.py` errors if the version is already present. Choose the next version string.
|
||||
|
||||
**Footnote warnings — `link '#fnref:N' has no anchor`**
|
||||
A definition `[^N]:` exists without a matching inline citation. Add the citation or remove the orphaned definition.
|
||||
|
||||
+89
-87
@@ -1,87 +1,89 @@
|
||||
---
|
||||
title: Impressum
|
||||
---
|
||||
# A Constitution for an Anonymous Planet.
|
||||
|
||||
To amend the rules and regulations of the network and of the PSA community, this constitution is hereby set forth. It is applicable to all the projects of the initiative, especially the Hitchhiker's Guide to Online Anonymity. All members/collaborators must abide by these lines when contributing within the context of the initiative.
|
||||
|
||||
## Requirements
|
||||
|
||||
> Content is licensed under **[Creative Commons Attribution NonCommercial](https://creativecommons.org/licenses/by-nc/3.0/)** to prevent commercial usage.
|
||||
|
||||
### Anonymity above everything.
|
||||
Anonymity is necessary to maintain the balance of power, specifically to help journalists, whistleblowers, lawyers, scientists, and victims of oppression. Anonymity first, even if that means using non-free and/or proprietary means. Security and privacy are second, again, even if using non-free or non-open-source and/or proprietary means. In this sense, the ends may at times justify proprietary means.
|
||||
|
||||
### Independence.
|
||||
The Anonymous Planet initiative has no affiliation with the "Anonymous" collective and does not endorse their activities.
|
||||
Any overlap of their activities and our guide are purely coincidental.
|
||||
|
||||
### Accessibility.
|
||||
We will strive to always keep available the following methods of reading the Hitchhiker's Guide:
|
||||
|
||||
- online;
|
||||
- offline (e.g., PDF, ODT);
|
||||
- via the Tor network
|
||||
|
||||
### Freedom.
|
||||
Maintain free, open-source, and non-commercial nature of all our projects. This does not mean proprietary and/or closed-source tools won't be recommendeded. All scientific knowledge should be free for anyone and we support and encourage Sci-Hub and LibGen. Any attempt to erode the freedom of information and flow of knowledge of our projects, in any manner, is hostile.
|
||||
|
||||
### Verifiability, falsifiability and reproducibility.
|
||||
We will make every effort to be transparent about any and all bias we have.
|
||||
Anyone claiming to be unbiased is lying, therefore we will not falsely claim to be.
|
||||
|
||||
All our content shall be verifiable, reproducible and fact-checked:
|
||||
|
||||
- academic references (e.g., studies, papers, and peer reviewed publications);
|
||||
- reputable media references (e.g., articles, videos, and documentaries);
|
||||
- official documentation (e.g., manuals, field guides, and technical documents);
|
||||
- renowned and reputable expert review;
|
||||
- direct testing by our own collaborators for falsifiablity
|
||||
|
||||
### Innocence.
|
||||
Suspected offenders are innocent until proven guilty, with zero tolerance for abuse of power or position.
|
||||
|
||||
Any accusing/moderating member is:
|
||||
|
||||
- Subject to the burden of proving the wrong-doing of the offender.
|
||||
- Required to motivate any sanction.
|
||||
|
||||
Any offender has the right to:
|
||||
|
||||
- Face their accuser (know who is accusing them).
|
||||
- Appeal sanctions to an uninvolved third party.
|
||||
- Participate in their own incrimination (the burden of proof lies with the accuser).
|
||||
- Due process of the above.
|
||||
|
||||
### Freedom of thought.
|
||||
Open-minded and pragmatic - with no tolerance for gatekeeping.
|
||||
|
||||
Critical thinking and fact-checking are strongly encouraged; we welcome criticism including of a harsh nature (excluding ad-hominem and slurs).
|
||||
|
||||
### We do not tolerate intolerance.
|
||||
See the [Paradox of Tolerance](https://en.wikipedia.org/wiki/Paradox_of_tolerance), which includes hate speech.
|
||||
|
||||
### No analytics.
|
||||
Note that, while we will never use analytics, the (now free) platforms hosting our content might be gathering such analytics outside of our control, such as Github pages. As the initiative progresses, we will strive to avoid these as soon as possible.
|
||||
|
||||
### No profit.
|
||||
Any excess donations will only be used to support our main projects first and possibly support other intitiatives (like hosting Tor exit nodes). In all cases, we abide by the following principles:
|
||||
|
||||
- Funding transparency (i.e., all donations, spendings, source code, and future goals will be public).
|
||||
- Acceptance of donations from any entity anonymously or acknowledged (opt-in) will not have any influence on our content.
|
||||
- No sponsored content.
|
||||
- No affiliate links.
|
||||
- No product placements.
|
||||
- No advertising.
|
||||
|
||||
**Disclaimer: it is possible that, coincidentally, a donation could correlate with a recommendation. It will then be clearly stated that while the donation was welcome, the donating entity will not be gaining visibility/coverage/endorsement/recommendations due to such a donation.**
|
||||
|
||||
## Core Goals.
|
||||
|
||||
Help people in need of anonymity to maintain both their physical and digital safety.
|
||||
|
||||
## Non-Goals.
|
||||
|
||||
Help any people who are using this knowledge for bad purposes. Helping people takes precedence and we know our content can be used nefariously. Our initiative believes in having one good person given an anonymous voice, safely, is worth the risk of having several using our content for evil. As we do adhere to a fair "rule of law" system which, having 9 criminals and 1 innocent person free, is much better than having one innocent person in prison among 9 criminals.
|
||||
|
||||
**Yours faithfully, Anonymous Planet**
|
||||
---
|
||||
title: "A Constitution"
|
||||
description: "The rules, values, and principles governing the Anonymous Planet initiative and PSA community."
|
||||
hide:
|
||||
- toc
|
||||
---
|
||||
|
||||
<div class="hero-block">
|
||||
<div class="hero-eyebrow">Applicable to all projects of the initiative.</div>
|
||||
<h1 class="hero-title">A Constitution<span class="hero-subtitle">The rules we hold ourselves to.</span></h1>
|
||||
<div class="hero-cta-row">
|
||||
<a href="#principles" class="hero-cta hero-cta--primary">Our Principles</a>
|
||||
<a href="#goals" class="hero-cta hero-cta--secondary">Core Goals</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Principles { #principles }
|
||||
|
||||
<div class="index-grid">
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Anonymity Above Everything</h3>
|
||||
<p class="index-card__body">Anonymity is necessary to maintain the balance of power — for journalists, whistleblowers, lawyers, scientists, and victims of oppression. Anonymity first, even if that means using proprietary means. Security and privacy are second. The ends may at times justify proprietary means.</p>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Independence</h3>
|
||||
<p class="index-card__body">Anonymous Planet has no affiliation with the "Anonymous" collective and does not endorse their activities. Any overlap between their activities and this guide is purely coincidental.</p>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Accessibility</h3>
|
||||
<p class="index-card__body">The Hitchhiker's Guide will always be available online, offline (PDF, ODT), and via the Tor network. Removing any of these access methods is hostile to the mission.</p>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Freedom</h3>
|
||||
<p class="index-card__body">All projects are and will remain free, open-source, and non-commercial. Proprietary tools may be recommended where they serve anonymity. All scientific knowledge should be free — we support Sci-Hub and LibGen. Any attempt to erode the freedom of information in our projects is hostile.</p>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Verifiability & Reproducibility</h3>
|
||||
<p class="index-card__body">We will be transparent about our biases — anyone claiming to be unbiased is lying. All content shall be verifiable and fact-checked via academic references, reputable media, official documentation, expert review, and direct testing by collaborators.</p>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Innocence</h3>
|
||||
<p class="index-card__body">Suspected offenders are innocent until proven guilty. Zero tolerance for abuse of power. Accusers bear the burden of proof. Offenders have the right to face their accuser, appeal to an uninvolved third party, and receive due process.</p>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Freedom of Thought</h3>
|
||||
<p class="index-card__body">Open-minded and pragmatic — no gatekeeping. Critical thinking and harsh criticism are welcome. Ad-hominem attacks and slurs are not.</p>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">No Tolerance for Intolerance</h3>
|
||||
<p class="index-card__body">See the <a href="https://en.wikipedia.org/wiki/Paradox_of_tolerance">Paradox of Tolerance</a>. Hate speech is included.</p>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">No Analytics</h3>
|
||||
<p class="index-card__body">We will never use analytics. Hosting platforms (e.g. GitHub Pages) may gather analytics outside our control. As the initiative grows, we will move away from these platforms where possible.</p>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">No Profit</h3>
|
||||
<p class="index-card__body">Excess donations fund our projects first, then initiatives like Tor exit nodes. All donations, spendings, source code, and goals are public. No sponsored content, no affiliate links, no product placements, no advertising. Donations never influence recommendations.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
It is possible that, coincidentally, a donation could correlate with a recommendation. Where this occurs it will be clearly stated that the donation was welcome, but confers no visibility, coverage, endorsement, or preferential recommendation.
|
||||
|
||||
---
|
||||
|
||||
## Goals { #goals }
|
||||
|
||||
**Core goal:** Help people who need anonymity maintain their physical and digital safety.
|
||||
|
||||
**Non-goal:** Help people use this knowledge for harmful purposes.
|
||||
|
||||
We know our content can be misused. We believe that giving one good person a safe, anonymous voice is worth the risk of a few using it for ill — in the same way that a fair rule of law accepts that freeing nine criminals is better than imprisoning one innocent person.
|
||||
|
||||
---
|
||||
|
||||
_Yours faithfully, Anonymous Planet_
|
||||
|
||||
+176
-111
@@ -1,111 +1,176 @@
|
||||
---
|
||||
title: How to Get Involved
|
||||
---
|
||||
There are multiple ways you can add to the guide. Donations to support this project are welcome but are entirely optional. Those donations are mainly used to pay for Tor onion hosting (VPS), mail hosting, domain name registration, and to maintain/run Tor exit nodes. **No profit is ever being made**. All donations and spendings are being logged here below for transparency. Some costs for load balancer servers have been omitted for privacy reasons, but are not paid for with existing Anonymous Planet finances.
|
||||
|
||||
<span style="color: red">**Current project donation goals:**</span>
|
||||
|
||||
- <del>Funding for a VPS for hosting our .onion website</del>: **done**
|
||||
- <del>Funding for extending our domain name</del>: **Recovery of original domain secured until 2029**
|
||||
- Funding for a decent mail hosting
|
||||
- Funding for a VPS for hosting various services
|
||||
|
||||
## Donate using Monero (XMR)
|
||||
|
||||
Total Monero donations received: **7.101317184263 XMR**
|
||||
Total Monero remaining: **2.059336719397 XMR**
|
||||
|
||||
Here is the address for the main project:
|
||||
|
||||
```46crzj54eL493BA68pPT4A1MZyKQxrpZu9tVNsfsoa5nT85QqCt8cDTfy1fcTH1oyjdtUbhmpZ4QcVtfEXB337Ng6PS21ML```
|
||||
|
||||
![][1]
|
||||
|
||||
## Donate using Bitcoin (BTC)
|
||||
|
||||
Total Bitcoin donations received: **1.89353 mBTC**
|
||||
Total Bitcoin remaining: **0 mBTC**
|
||||
|
||||
Here are the addresses for the main project:
|
||||
|
||||
SegWit address: ```bc1qp9g2c6dquh5lnvft50esxsl97kupdpyqyd4kkv```
|
||||
Legacy address: ```1BBgBSVe6w4DWq2BewUQhDEjsNovhfPswD```
|
||||
|
||||
![][2]_____________________![][3]
|
||||
|
||||
## Content Contributions
|
||||
|
||||
You can easily contribute code or information suggestions at our code repositories listed at the bottom of the website and on the [Mirrors](../mirrors/index.md) tab above. We have many options that are easily accessible. Please follow our [contributing guidelines](../code/index.md) and use good PR syntax.
|
||||
|
||||
**Thank you for any contribution. All donations will be mentioned on this page.**
|
||||
|
||||
### Donations log
|
||||
|
||||
- 2021-02-06 16:48: 0.1 XMR
|
||||
- 2021-03-15 00:09: 1.24869 mBTC
|
||||
- 2021-03-15 08:41: 0.07896 mBTC
|
||||
- 2021-03-31 16:28: 1 XMR (Special thanks for this very generous donation)
|
||||
- 2021-04-03 22:31: 0.5 XMR (Special thanks for this very generous donation)
|
||||
- 2021-05-07 06:22: 0.010433355105 XMR
|
||||
- 2021-06-16 03:05: 0.03 XMR
|
||||
- 2021-06-27 18:39: 0.05 XMR
|
||||
- 2021-07-12 07:24: 0.02 XMR
|
||||
- 2021-07-16 14:31: 0.1 mBTC
|
||||
- 2021-07-20 21:01: 0.058981 XMR
|
||||
- 2021-07-24 15:16: 0.000000000001 XMR
|
||||
- 2021-07-25 02:37: 0.000000000001 XMR
|
||||
- 2021-08-03 00:17: 0.04119191113 XMR
|
||||
- 2021-08-07 15:05: 0.206328241262 XMR
|
||||
- 2021-08-10 11:42: 0.21 mBTC
|
||||
- 2021-08-13 00:25: 0.25 XMR
|
||||
- 2021-08-14 04:58: 0.25588 mBTC
|
||||
- 2021-08-30 17:32: 0.000000000001 XMR
|
||||
- 2021-09-17 14:34: 0.018 XMR
|
||||
- 2021-10-01 06:23: 0.000000002137 XMR
|
||||
- 2021-10-02 19:16: 1 XMR (Special thanks for this very generous donation)
|
||||
- 2021-10-17 15:40: 0.02 XMR
|
||||
- 2021-10-18 16:06: 0.1958 XMR
|
||||
- 2021-11-12 20:42: 0.02 XMR
|
||||
- 2021-11-14 18:28: 0.018 XMR
|
||||
- 2021-12-03 21:38: 0.10134722595 XMR
|
||||
- 2021-12-16 01:16: 1 XMR (Special thanks for this very generous donation)
|
||||
- 2021-12-16 18:06: 0.017 XMR
|
||||
- 2022-01-09 17:54: 0.045918219893 XMR
|
||||
- 2022-01-15 17:35: 0.014 XMR
|
||||
- 2022-01-24 21:08: 0.010786 XMR
|
||||
- 2022-01-26 12:07: 0.010391 XMR
|
||||
- 2022-02-03 19:59: 0.013013984 XMR
|
||||
- 2022-02-18 17:27: 0.019 XMR
|
||||
- 2022-03-14 10:25: 0.0139887 XMR
|
||||
- 2022-07-30 03:51: 0.0222 XMR
|
||||
- 2022-09-28 05:13: 2 XMR
|
||||
- 2022-08-19: SimpleLogin.io Lifetime Premium
|
||||
- 2022-09-19: 0.345024603905 XMR (Special thanks to a previous maintainer)
|
||||
|
||||
#### Spendings log
|
||||
|
||||
- 2021-03-12: 0.08181086 XMR (+fees) for domain anonymousplanet.org (1 year)
|
||||
- 2021-03-16: 1.20179 mBTC (+fees) for domain anonymousplanet.org renewal (extension 3 years totalling 4 years)
|
||||
- 2021-04-01: 0.8317 XMR (+fees) for basic VPS for Tor Mirror hosting
|
||||
- <del>2021-04-05: 0.99367 mBTC (+fees +exchange from XMR to BTC) for Mail Hosting (1 year): <span style="color: red">**Lost**</span>
|
||||
- <del>2021-04-13: 0.71895 mBTC (+fees +exchange from XMR to BTC) for Mail Hosting (extension to 2 years)</del>: <span style="color: red">**Lost**</span>
|
||||
- 2021-04-25: 0.02892 mBTC (Wallet to Wallet transfer fee)
|
||||
- 2021-07-13: 0.78463 mBTC (+fees +exchange from BTC to XMR) for consolidation
|
||||
- <del>2021-07-13: 0.067261698061 XMR (+fees) for a Tor Exit Node (01) Hosting (3 months)</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2021-07-15: 0.151959953047 XMR (+fees) for a Tor Exit Node (02) Hosting (6 months)</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2021-08-16: 0.253331471239 XMR (+fees) for a Tor Exit Node (03) Hosting (12 months)</del>: <span style="color: red">**Lost**</span>
|
||||
- 2021-08-18: AtomicSwap conversion from remaining mBTC (-0.56588) to XMR (+0.081904862179)
|
||||
- <del>2021-08-19: 0.0644 XMR (+fees) for Mail Hosting extension</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2021-09-18: 0.246971511836 XMR (+fees) for renewal 1 year of Tor Exit Node 01</del>: <span style="color: red">**Lost**</span>
|
||||
- 2021-10-04: 0.26954 XMR (+fees) for domain anonymousplanet.org extension until 2029
|
||||
- <del>2021-10-06: 0.236073464623 XMR (+fees) for a Tor Exit Node (04) Hosting (12 months)</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2021-10-18: 0.01952 XMR (+fees) for testing a new VPS hosting provider (Privex.io) for one month</del>: <span style="color: red">**Ended**</span>
|
||||
- <del>2021-10-30: 0.240787814495 XMR (+fees) for a Synapse Hosting VPS (12 months) with bots to help grow the community. This is a test program that will be converted into a Tor Exit Node in case of failure</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2022-01-01: 0.28055816111 XMR (+fees) for renewal 1 year of Tor Exit Node 02</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2022-02-02: 0.966793601024 XMR (+fees) to sponsor a special project (w/ Universal Declaration of Human Rights)</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2022-07-11: 0.503232784687 XMR (+fees) for 1984.is VPS (12 months)</del>: <span style="color: red">**Ended**</span>
|
||||
- <del>2022-09-19: 0.345024603905 XMR (+fees) for upgrading VPS RAM/Disk</del>: <span style="color: red">**Ended**</span>
|
||||
|
||||
[1]: ../media/monero.png
|
||||
[2]: ../media/bitcoin-segwit.png
|
||||
[3]: ../media/bitcoin-legacy.png
|
||||
---
|
||||
title: Contribute & Donate
|
||||
description: "Support the Anonymous Planet project through crypto donations or content contributions. No fiat. No tracking. No profit."
|
||||
hide:
|
||||
- toc
|
||||
---
|
||||
|
||||
<div class="hero-block">
|
||||
<div class="hero-eyebrow">Support the project if you can!</div>
|
||||
<h1 class="hero-title">No ads. No grants.<br><span class="hero-subtitle">Anonymous donations welcome.</span></h1>
|
||||
<p class="hero-tagline">
|
||||
Donations fund Tor .onion hosting, domain registration, bulletproof mail hosting, and exit node operations.
|
||||
Every cent is logged below. No profit is ever made.
|
||||
</p>
|
||||
<div class="hero-cta-row">
|
||||
<a href="#xmr" class="hero-cta hero-cta--primary">Donate with Monero (XMR)</a>
|
||||
<a href="#btc" class="hero-cta hero-cta--secondary">Donate with Bitcoin (BTC)</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="donate-goals-block">
|
||||
<h2 class="donate-goals-title">Current Goals</h2>
|
||||
<ul class="donate-goals-list">
|
||||
<li class="donate-goal donate-goal--done"><span class="donate-goal-icon">✓</span> VPS for .onion hosting <strong>funded</strong></li>
|
||||
<li class="donate-goal donate-goal--done"><span class="donate-goal-icon">✓</span> Domain <code>anonymousplanet.net</code> — <strong>secured until 2029</strong></li>
|
||||
<li class="donate-goal donate-goal--open"><span class="donate-goal-icon">○</span> Decent mail hosting</li>
|
||||
<li class="donate-goal donate-goal--open"><span class="donate-goal-icon">○</span> VPS for general project services</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Donate with Monero (XMR) { #xmr }
|
||||
|
||||
Monero is the preferred donation method. Transactions are private by default.
|
||||
|
||||
<div class="donate-stat-row">
|
||||
<div class="donate-stat"><span class="donate-stat-label">Total received</span><span class="donate-stat-value">7.101317 XMR</span></div>
|
||||
<div class="donate-stat"><span class="donate-stat-label">Remaining balance</span><span class="donate-stat-value">0 XMR</span></div>
|
||||
</div>
|
||||
|
||||
<div class="donate-address-block">
|
||||
<div class="donate-address-label">XMR address</div>
|
||||
<code class="donate-address">46crzj54eL493BA68pPT4A1MZyKQxrpZu9tVNsfsoa5nT85QqCt8cDTfy1fcTH1oyjdtUbhmpZ4QcVtfEXB337Ng6PS21ML</code>
|
||||
<div class="donate-qr">
|
||||
<img src="../media/monero.png" alt="Monero QR code" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Donate with Bitcoin (BTC) { #btc }
|
||||
|
||||
Bitcoin donations are accepted on both SegWit and legacy addresses.
|
||||
|
||||
<div class="donate-stat-row">
|
||||
<div class="donate-stat"><span class="donate-stat-label">Total received</span><span class="donate-stat-value">1.89353 mBTC</span></div>
|
||||
<div class="donate-stat"><span class="donate-stat-label">Remaining balance</span><span class="donate-stat-value">0 mBTC</span></div>
|
||||
</div>
|
||||
|
||||
<div class="donate-address-block">
|
||||
<div class="donate-address-label">SegWit address</div>
|
||||
<code class="donate-address">bc1qp9g2c6dquh5lnvft50esxsl97kupdpyqyd4kkv</code>
|
||||
<div class="donate-address-label" style="margin-top:1em;">Legacy address</div>
|
||||
<code class="donate-address">1BBgBSVe6w4DWq2BewUQhDEjsNovhfPswD</code>
|
||||
<div class="donate-qr donate-qr--row">
|
||||
<figure>
|
||||
<img src="../media/bitcoin-segwit.png" alt="Bitcoin SegWit QR code" loading="lazy">
|
||||
<figcaption>SegWit</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="../media/bitcoin-legacy.png" alt="Bitcoin Legacy QR code" loading="lazy">
|
||||
<figcaption>Legacy</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Contribute Content { #contribute }
|
||||
|
||||
<div class="index-grid">
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Submit a PR</h3>
|
||||
<p class="index-card__body">Fix errors, add sections, or update outdated advice. Follow the contributing guidelines and use conventional commit syntax.</p>
|
||||
<a href="../code/" class="index-card__link">Developer guide</a>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Use a Mirror</h3>
|
||||
<p class="index-card__body">GitHub, Codeberg, GitLab, Disroot, and a Tor onion. Pick the one that fits your threat model.</p>
|
||||
<a href="../mirrors/" class="index-card__link">View mirrors</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Transparency Log
|
||||
|
||||
All donations and spendings are recorded here. <span style="color:var(--status-error)">Red entries</span> indicate funds tied to services that were lost.
|
||||
|
||||
### Donations
|
||||
|
||||
| Date / Time | Amount | Note |
|
||||
|---|---|---|
|
||||
| 2021-02-06 16:48 | 0.1 XMR | |
|
||||
| 2021-03-15 00:09 | 1.24869 mBTC | |
|
||||
| 2021-03-15 08:41 | 0.07896 mBTC | |
|
||||
| 2021-03-31 16:28 | 1 XMR | :heart: Generous donation |
|
||||
| 2021-04-03 22:31 | 0.5 XMR | :heart: Generous donation |
|
||||
| 2021-05-07 06:22 | 0.010433 XMR | |
|
||||
| 2021-06-16 03:05 | 0.03 XMR | |
|
||||
| 2021-06-27 18:39 | 0.05 XMR | |
|
||||
| 2021-07-12 07:24 | 0.02 XMR | |
|
||||
| 2021-07-16 14:31 | 0.1 mBTC | |
|
||||
| 2021-07-20 21:01 | 0.058981 XMR | |
|
||||
| 2021-07-24 15:16 | 0.000000000001 XMR | |
|
||||
| 2021-07-25 02:37 | 0.000000000001 XMR | |
|
||||
| 2021-08-03 00:17 | 0.04119 XMR | |
|
||||
| 2021-08-07 15:05 | 0.206328 XMR | |
|
||||
| 2021-08-10 11:42 | 0.21 mBTC | |
|
||||
| 2021-08-13 00:25 | 0.25 XMR | |
|
||||
| 2021-08-14 04:58 | 0.25588 mBTC | |
|
||||
| 2021-08-30 17:32 | 0.000000000001 XMR | |
|
||||
| 2021-09-17 14:34 | 0.018 XMR | |
|
||||
| 2021-10-01 06:23 | 0.000000002137 XMR | |
|
||||
| 2021-10-02 19:16 | 1 XMR | :heart: Generous donation |
|
||||
| 2021-10-17 15:40 | 0.02 XMR | |
|
||||
| 2021-10-18 16:06 | 0.1958 XMR | |
|
||||
| 2021-11-12 20:42 | 0.02 XMR | |
|
||||
| 2021-11-14 18:28 | 0.018 XMR | |
|
||||
| 2021-12-03 21:38 | 0.10134 XMR | |
|
||||
| 2021-12-16 01:16 | 1 XMR | :heart: Generous donation |
|
||||
| 2021-12-16 18:06 | 0.017 XMR | |
|
||||
| 2022-01-09 17:54 | 0.045918 XMR | |
|
||||
| 2022-01-15 17:35 | 0.014 XMR | |
|
||||
| 2022-01-24 21:08 | 0.010786 XMR | |
|
||||
| 2022-01-26 12:07 | 0.010391 XMR | |
|
||||
| 2022-02-03 19:59 | 0.013013 XMR | |
|
||||
| 2022-02-18 17:27 | 0.019 XMR | |
|
||||
| 2022-03-14 10:25 | 0.0139887 XMR | |
|
||||
| 2022-07-30 03:51 | 0.0222 XMR | |
|
||||
| 2022-09-28 05:13 | 2 XMR | |
|
||||
| 2022-08-19 | SimpleLogin Lifetime Premium | In-kind |
|
||||
| 2022-09-19 | 0.345024 XMR | :heart: From a previous maintainer |
|
||||
|
||||
### Spendings
|
||||
|
||||
| Date | Amount | Purpose | Status |
|
||||
|---|---|---|---|
|
||||
| 2021-03-12 | 0.08181 XMR | Domain `anonymousplanet.net` (1 yr) | Active |
|
||||
| 2021-03-16 | 1.20179 mBTC | Domain renewal (+3 yr, 4 yr total) | Active |
|
||||
| 2021-04-01 | 0.8317 XMR | VPS for Tor mirror hosting | Active |
|
||||
| 2021-04-05 | 0.99367 mBTC | Mail hosting (1 yr) | <span class="status-indicator status-error">Lost</span> |
|
||||
| 2021-04-13 | 0.71895 mBTC | Mail hosting (ext. to 2 yr) | <span class="status-indicator status-error">Lost</span> |
|
||||
| 2021-04-25 | 0.02892 mBTC | Wallet transfer fee | — |
|
||||
| 2021-07-13 | 0.78463 mBTC | BTC→XMR consolidation swap | — |
|
||||
| 2021-07-13 | 0.067261 XMR | Tor exit node 01 (3 mo) | <span class="status-indicator status-error">Lost</span> |
|
||||
| 2021-07-15 | 0.151959 XMR | Tor exit node 02 (6 mo) | <span class="status-indicator status-error">Lost</span> |
|
||||
| 2021-08-16 | 0.253331 XMR | Tor exit node 03 (12 mo) | <span class="status-indicator status-error">Lost</span> |
|
||||
| 2021-08-18 | −0.56588 mBTC / +0.081904 XMR | AtomicSwap BTC→XMR | — |
|
||||
| 2021-08-19 | 0.0644 XMR | Mail hosting extension | <span class="status-indicator status-error">Lost</span> |
|
||||
| 2021-09-18 | 0.246971 XMR | Tor exit node 01 renewal (1 yr) | <span class="status-indicator status-error">Lost</span> |
|
||||
| 2021-10-04 | 0.26954 XMR | Domain ext. to 2029 | Active |
|
||||
| 2021-10-06 | 0.236073 XMR | Tor exit node 04 (12 mo) | <span class="status-indicator status-error">Lost</span> |
|
||||
| 2021-10-18 | 0.01952 XMR | Privex.io VPS test (1 mo) | <span class="status-indicator status-warning">Ended</span> |
|
||||
| 2021-10-30 | 0.240787 XMR | Synapse hosting VPS (12 mo) | <span class="status-indicator status-error">Lost</span> |
|
||||
| 2022-01-01 | 0.28055 XMR | Tor exit node 02 renewal (1 yr) | <span class="status-indicator status-error">Lost</span> |
|
||||
| 2022-02-02 | 0.966793 XMR | Special project sponsorship (UDHR) | <span class="status-indicator status-error">Lost</span> |
|
||||
| 2022-07-11 | 0.503232 XMR | 1984.is VPS (12 mo) | <span class="status-indicator status-warning">Ended</span> |
|
||||
| 2022-09-19 | 0.345024 XMR | VPS RAM/disk upgrade | <span class="status-indicator status-warning">Ended</span> |
|
||||
|
||||
!!! note "About lost funds"
|
||||
Entries marked **Lost** represent funds spent on services that are no longer available.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+12932
-12619
File diff suppressed because it is too large
Load Diff
+46
-23
@@ -1,40 +1,63 @@
|
||||
---
|
||||
title: ""
|
||||
description: We are the maintainers of the Hitchhiker's Guide and the PSA Matrix space.
|
||||
title: "Home"
|
||||
description: "The Hitchhiker's Guide to Online Anonymity"
|
||||
schema:
|
||||
"@context": https://schema.org
|
||||
"@type": Organization
|
||||
"@id": https://www.anonymousplanet.org/
|
||||
"@type": WebPage
|
||||
"@id": https://anonymousplanet.net/
|
||||
name: Anonymous Planet
|
||||
url: https://www.anonymousplanet.org/authors/
|
||||
logo: ../media/profile.png
|
||||
url: https://anonymousplanet.net/
|
||||
logo: media/profile.png
|
||||
sameAs:
|
||||
- https://github.com/Anon-Planet
|
||||
- https://opencollective.com/anonymousplanetorg
|
||||
- https://mastodon.social/@anonymousplanet
|
||||
---
|
||||
|
||||
# **Hello, and welcome to the Hitchhiker's Guide.**
|
||||
<div class="hero-block">
|
||||
<div class="hero-eyebrow">Anonymous Planet presents</div>
|
||||
<h1 class="hero-title">The Hitchhiker's Guide<br><span class="hero-subtitle">to Online Anonymity (and Privacy)</span></h1>
|
||||
<p class="hero-tagline">
|
||||
You are being tracked. Your metadata is being sold. Your identity is a product. This guide is your manual out.
|
||||
</p>
|
||||
<div class="hero-cta-row">
|
||||
<a href="guide/" class="hero-cta hero-cta--primary">Read the Guide</a>
|
||||
<a href="verify/" class="hero-cta hero-cta--secondary">Verify & Download</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
**9FA5 436D 0EE3 6098 5157 3825 17EC A05F 768D EDF6**
|
||||
<div class="principles-strip">
|
||||
<span class="principle"><span class="principle-no">No ads.</span></span>
|
||||
<span class="principle-sep">/</span>
|
||||
<span class="principle"><span class="principle-no">No affiliate links.</span></span>
|
||||
<span class="principle-sep">/</span>
|
||||
<span class="principle"><span class="principle-no">No bullshit.</span></span>
|
||||
</div>
|
||||
|
||||
You'll use it to [**verify the checksum** and **GPG signature** of all files for authenticity.](verify/index.md)
|
||||
Please share this project if you enjoy it and you think it might be useful to others.
|
||||
<div class="index-grid">
|
||||
|
||||
{ align=right }
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Read Online</h3>
|
||||
<p class="index-card__body">The full guide — tracking techniques, ID verification, anonymous identity creation, and operational security from first principles.</p>
|
||||
<a href="guide/" class="index-card__link">Open guide</a>
|
||||
</div>
|
||||
|
||||
Anonymous Planet is a collective of volunteers.
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Download PDF</h3>
|
||||
<p class="index-card__body">Offline copy as PDF or ODT. Verify the file's integrity with the provided SHA256 checksum and GPG signature before trusting it.</p>
|
||||
<a href="export/thgtoa.pdf" class="index-card__link">Get the files</a>
|
||||
</div>
|
||||
|
||||
??? person "Das Kolburn"
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Tor Mirror</h3>
|
||||
<p class="index-card__body">Access over the Tor network. The .onion address means your IP and destination stay private — no exit node exposure.</p>
|
||||
<a href="mirrors/" class="index-card__link">View mirrors</a>
|
||||
</div>
|
||||
|
||||
- [:simple-github: GitHub](https://github.com/NobodySpecial256 "@NobodySpecial256")
|
||||
- [:fontawesome-solid-envelope: E-mail](mailto:contact@anonymousplanet.org)
|
||||
- [:simple-matrix: Personal Matrix](https://matrix.to/#/@daskolburn:thomcat.rocks "@daskolburn:thomcat.rocks"), [:simple-matrix: Org Matrix](https://matrix.to/#/@daskolburn:anonymousplanet.net "@daskolburn:anonymousplanet.net")
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Verify Authenticity</h3>
|
||||
<p class="index-card__body">Check GPG signatures, SHA256 hashes, and fetch our public keyring. Never trust a download you haven't verified.</p>
|
||||
<a href="verify/" class="index-card__link">How to verify</a>
|
||||
</div>
|
||||
|
||||
??? person "Nope"
|
||||
|
||||
- [:simple-github: GitHub](https://github.com/nopeitsnothing "@nopeitsnothing")
|
||||
- [:simple-mastodon: Mastodon](https://ioc.exchange/@unknown "@unknown@ioc.exchange"){rel=me}
|
||||
- [:fontawesome-solid-house: Homepage](https://www.itsnothing.net)
|
||||
- [:fontawesome-solid-envelope: E-mail](mailto:contact@anonymousplanet.org)
|
||||
- [:simple-matrix: Personal Matrix](https://matrix.to/#/@thehidden:tchncs.de "@thehidden:tchncs.de"), [:simple-matrix: Org Matrix](https://matrix.to/#/@nope:anonymousplanet.net "@nope:anonymousplanet.net")
|
||||
</div>
|
||||
|
||||
+101
-21
@@ -1,41 +1,121 @@
|
||||
---
|
||||
title: "Our Mirrors"
|
||||
description: Maintainers of the Hitchhiker's Guide and the PSA Community.
|
||||
title: "Mirrors"
|
||||
description: "Every place you can read or download the Hitchhiker's Guide."
|
||||
hide:
|
||||
- toc
|
||||
schema:
|
||||
"@context": https://schema.org
|
||||
"@type": Organization
|
||||
"@id": https://www.anonymousplanet.org/
|
||||
"@id": https://anonymousplanet.net/
|
||||
name: Anonymous Planet
|
||||
url: https://www.anonymousplanet.org/mirrors/
|
||||
url: https://anonymousplanet.net/mirrors/
|
||||
logo: ../media/profile.png
|
||||
sameAs:
|
||||
- https://github.com/Anon-Planet
|
||||
- https://opencollective.com/anonymousplanetorg
|
||||
- https://mastodon.social/@anonymousplanet
|
||||
---
|
||||
{ align=right }
|
||||
|
||||
<div class="hero-block">
|
||||
<div class="hero-eyebrow">Censorship-resistant by design.</div>
|
||||
<h1 class="hero-title">Mirrors<span class="hero-subtitle">Pick the one that fits your threat model.</span></h1>
|
||||
<p class="hero-tagline">
|
||||
The guide is mirrored across GitHub, Codeberg, GitLab, Disroot, and a Tor onion.
|
||||
We have contingencies in place.
|
||||
A signed PDF is also available for offline reading.
|
||||
</p>
|
||||
<div class="hero-cta-row">
|
||||
<a href="#read" class="hero-cta hero-cta--primary">Read the Guide</a>
|
||||
<a href="#git" class="hero-cta hero-cta--secondary">Git Mirrors</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
!!! Note "Where to find the Hitchhiker's Guide"
|
||||
## Read the Guide { #read }
|
||||
|
||||
- [Original](https://anonymousplanet.org)
|
||||
- [Tor v3](http://thgtoa3jzy3doku7hkna32htpghjijefscwvh4dyjgfydbbjkeiohgid.onion) **Down**
|
||||
- [Archive.org](https://web.archive.org/web/https://anonymousplanet.org)
|
||||
- [Archive.today](https://archive.fo/anonymousplanet.org)
|
||||
- [Archive.today over Tor](http://archiveiya74codqgiixo33q62qlrqtkgmcitqx5u2oeqnmn5bpcbiyd.onion/anonymousplanet.org)
|
||||
<div class="index-grid">
|
||||
|
||||
!!! Note "PDF export (single file)"
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Original</h3>
|
||||
<p class="index-card__body">The canonical source. Hosted on GitHub Pages at the project's primary domain.</p>
|
||||
<a href="https://anonymousplanet.net" class="index-card__link">anonymousplanet.net</a>
|
||||
</div>
|
||||
|
||||
The guide is also available as a **PDF** (images and layout preserved). It is built automatically in GitHub Actions: open [**Build guide PDF**](https://github.com/Anon-Planet/thgtoa/actions/workflows/build-sign-release.yml) on the [**source repository**](https://github.com/Anon-Planet/thgtoa), pick a successful run, and download the **`thgtoa`** and **`thgtoa-dark`** artifacts. You can start a fresh build anytime (**Actions** → **Build guide PDF** → **Run workflow**).
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Tor Onion</h3>
|
||||
<p class="index-card__body">v3 onion service. Currently <strong>down</strong>. Check back or use a clearnet mirror.</p>
|
||||
<a href="http://thgtoa3jzy3doku7hkna32htpghjijefscwvh4dyjgfydbbjkeiohgid.onion" class="index-card__link">Onion address</a>
|
||||
</div>
|
||||
|
||||
To produce the same file locally, clone the repository and run `python3 scripts/build_guide_pdf.py --both` (Python, [MkDocs Material](https://squidfunk.github.io/mkdocs-material/getting-started/), and **Google Chrome** or **Microsoft Edge** required). More detail is in the [repository README](https://github.com/Anon-Planet/thgtoa#ways-to-read-or-export-the-guide).
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">PDF Export</h3>
|
||||
<p class="index-card__body">A signed, single-file PDF built automatically from the source. Both light and dark editions. See the release notes for hash and signature files.</p>
|
||||
<a href="https://github.com/Anon-Planet/thgtoa/releases" class="index-card__link">Releases</a>
|
||||
</div>
|
||||
|
||||
!!! Note "Our official git mirrors"
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Archive.org</h3>
|
||||
<p class="index-card__body">Wayback Machine snapshot. Useful if the primary domain is unreachable.</p>
|
||||
<a href="https://web.archive.org/web/https://anonymousplanet.net" class="index-card__link">Wayback Machine</a>
|
||||
</div>
|
||||
|
||||
- [Github](https://github.com/anon-planet)
|
||||
- [Darktea](http://it7otdanqu7ktntxzm427cba6i53w6wlanlh23v5i3siqmos47pzhvyd.onion/anonymousplanetorg) (Tor Only)
|
||||
- [Gitlab](https://gitlab.com/anonymousplanetorg)
|
||||
- [0xacab](https://0xacab.org/anonypla) ([Tor Onion](http://wmj5kiic7b6kjplpbvwadnht2nh2qnkbnqtcv3dyvpqtz7ssbssftxid.onion/anonypla))
|
||||
- [Codeberg](https://codeberg.org/anonymousplanet)
|
||||
- [Disroot](https://git.disroot.org/anon-planet) ([Tor Onion](http://kgtz2pmmov5jfvn3z4mqryffjnnw6krzrgxxoyaqhqckjrr4pckyhsqd.onion/anon-planet))
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Archive.today</h3>
|
||||
<p class="index-card__body">Alternative archival snapshot. Also accessible via their Tor onion.</p>
|
||||
<a href="https://archive.fo/anonymousplanet.net" class="index-card__link">Archive.today</a>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Archive.today (Tor)</h3>
|
||||
<p class="index-card__body">Archive.today over Tor for censorship-resistant access.</p>
|
||||
<a href="http://archiveiya74codqgiixo33q62qlrqtkgmcitqx5u2oeqnmn5bpcbiyd.onion/anonymousplanet.net" class="index-card__link">Onion</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Git Mirrors { #git }
|
||||
|
||||
<div class="index-grid">
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">GitHub</h3>
|
||||
<p class="index-card__body">Primary development host. Issues, pull requests, and CI live here.</p>
|
||||
<a href="https://github.com/anon-planet" class="index-card__link">github.com/anon-planet</a>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Codeberg</h3>
|
||||
<p class="index-card__body">EU-based, non-profit Gitea instance. No Microsoft.</p>
|
||||
<a href="https://codeberg.org/anonymousplanet" class="index-card__link">codeberg.org</a>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">GitLab</h3>
|
||||
<p class="index-card__body">GitLab.com mirror. Accepts merge requests.</p>
|
||||
<a href="https://gitlab.com/anonymousplanetorg" class="index-card__link">gitlab.com</a>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">0xacab</h3>
|
||||
<p class="index-card__body">Activist-run GitLab instance. Also accessible via Tor onion.</p>
|
||||
<a href="https://0xacab.org/anonypla" class="index-card__link">0xacab.org</a>
|
||||
<a href="http://wmj5kiic7b6kjplpbvwadnht2nh2qnkbnqtcv3dyvpqtz7ssbssftxid.onion/anonypla" class="index-card__link" style="margin-top:0.4em;">Onion</a>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Disroot</h3>
|
||||
<p class="index-card__body">Privacy-respecting Gitea instance. Also accessible via Tor onion.</p>
|
||||
<a href="https://git.disroot.org/anon-planet" class="index-card__link">git.disroot.org</a>
|
||||
<a href="http://kgtz2pmmov5jfvn3z4mqryffjnnw6krzrgxxoyaqhqckjrr4pckyhsqd.onion/anon-planet" class="index-card__link" style="margin-top:0.4em;">Onion</a>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Darktea</h3>
|
||||
<p class="index-card__body">Tor-only Gitea mirror. Only accessible via the onion address.</p>
|
||||
<a href="http://it7otdanqu7ktntxzm427cba6i53w6wlanlh23v5i3siqmos47pzhvyd.onion/anonymousplanetorg" class="index-card__link">Onion only</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEZc0QYxYJKwYBBAHaRw8BAQdAm8mOR8/0qWrm9Tqzfl9Ks5rjtIbQZLAR/qxH
|
||||
HVGJsxi0LUFub255bW91cyBQbGFuZXQgRW1haWwgRW5jcnlwdGlvbi9TaWduaW5n
|
||||
IEtleYiTBBMWCgA7FiEE/L0sq979H7ounnWRoags0t0s+JAFAmXNEGMCGwMFCwkI
|
||||
BwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQoags0t0s+JDbRAEAuZlBmMGgZ3bh
|
||||
12Js9jjDcu+jhKqL4fJrJG5z9+KFkQwA/An1StA6EhcM7qlzZ5bzm2SZAbP9hQRZ
|
||||
GmfaeU2P5KgHiHgEMBYKACAWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaiCYTgId
|
||||
AAAKCRAX7KBfdo3t9gNUAP9/SyGBYJ7s9YeqLHOJ+veQZjZYHvFGQ7yPn0Fetx0Z
|
||||
LAD/UOQ8rP2QaldCMyVSG8SqfPd7n++SEAXWAl2gAo9mhg6IdQQQFgoAHRYhBJ+l
|
||||
Q20O42CYUVc4JRfsoF92je32BQJmpEUQAAoJEBfsoF92je32tD8A/ir9hE8UjrJE
|
||||
psG+PNfxYAwAagKUGbAMDUxQp3z+t81+AP45hYT4aR89zSQaankHLs3Lh7Cp5ael
|
||||
NBe/BtfR9hCLAYkCMwQQAQgAHRYhBF7WeRgs0hkTAMDm6kyyELegkVLWBQJqIJiM
|
||||
AAoJEEyyELegkVLWjZYP/j1k5vl+r0NDQXmE8hS9IKhaQPggP72iXc5RWeMQHuIv
|
||||
b1laQZm64xerJNdAh0uk1bwfmJnVGfyxBUrlCgAIeVGRSlni2Rig4azaQ1IS0pqF
|
||||
4sC1KzKEhEaNdkh3pJyGtP1cikcSjWeU2oYQou3/7VN3vNyW+n8OAVF+2fsC5d78
|
||||
EvdpZgal+komb+J8Bt552uDbCCVI4TFIPBZmHWoXjaP6L+730YphbV7Aw0L5J6OO
|
||||
ob0nzHn4X0dIvGE7Phdp2e1yNRUOSRLh8B/D5OiE9k7CaeYmJNPv5qOw/R+NgrrA
|
||||
ZFnoOuwHo0D+aL9WT9q4aM/cDCEIbvhQ4l5ZhVGqZuQ9wxNCgPi3ZiZRTfk1PW4v
|
||||
uMw1xGwXBKy7jDO12xWIWWv9MiwIQLw0OxSxKbr76rgucq7e7JrWr64rItu5Wm7F
|
||||
8qxg2cwmDat6tFSRVWlEDy8oNkRMJNjdQJDu3ez9YOfJNnApAz94Of1XU7CUuYjY
|
||||
PV88BaHdUBVtANEzy0iSDCcSj6auzLfv9dBN8cOdUxlVcrPf2jjK6JR/6qe6VWNp
|
||||
wRg9VQW2fe8HJTMUt0o9qQBJUsF68KOHtIdoE4az9AyyBNKl67dKqLB9HoIItLzD
|
||||
MJRcbS2p6plCTNagwPVvgtPRChll9JP3jLPVhRL2BixYVkbHUoJxsEfscTUl6Azt
|
||||
tEtBbm9ueW1vdXMgUGxhbmV0IEVtYWlsIEVuY3J5cHRpb24vU2lnbmluZyBLZXkg
|
||||
PGFub255bW91c3BsYW5ldEBkaXNyb290Lm9yZz6IkwQTFgoAOxYhBPy9LKve/R+6
|
||||
Lp51kaGoLNLdLPiQBQJmhqXqAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheA
|
||||
AAoJEKGoLNLdLPiQJ34A/RJT9Hyj7hT/0D1BbDU6s6YzD+/x7Pyq2+9kNSI0L77W
|
||||
AQAAG+CfDrKDXJtBNKZVNFZpld3wUeoIOcAqLl7KpsVGCYh1BBAWCgAdFiEEn6VD
|
||||
bQ7jYJhRVzglF+ygX3aN7fYFAmakRRAACgkQF+ygX3aN7fbypgEAnEg0IbWnpaLj
|
||||
/4wU179vUZZu/Y0DE63GbJuZjj72hKUA/0xyzIgSvXByjoOkEwCn5w1+RPYXKw7Z
|
||||
syERsDCUAAMIiQIzBBABCAAdFiEEXtZ5GCzSGRMAwObqTLIQt6CRUtYFAmogmIwA
|
||||
CgkQTLIQt6CRUtafPQ/+LTWFU84tDZAM0Hp7bWB0dw8nP0JvNQ2WtZf0flh+r1tF
|
||||
cmVnc9szZBh+zzSpY25iK5+Waa6+l1POYSQpkS67VR0Jrv9nL94YrRhqalSRWsjW
|
||||
MQJO+Obu4LIRIqiMZLJlAd9Bg9FshYagbQDVDOI8v9mxqCzIVm3tBx1Jp57ATHgm
|
||||
sMDWn7l1BI0SkLlG49LYxVDQ6QAx4XLCQw+JzdiJs+yExa5ymYmV61evVVbDV5UF
|
||||
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
|
||||
Eztpvc4cyyGAmI42SLM/jKbqO2yWqwVoAg==
|
||||
=ww/S
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -0,0 +1,165 @@
|
||||
---
|
||||
title: "PGP"
|
||||
description: "Import our GPG keys to verify our releases and signed content."
|
||||
hide:
|
||||
- toc
|
||||
schema:
|
||||
"@context": https://schema.org
|
||||
"@type": Organization
|
||||
"@id": https://anonymousplanet.net/
|
||||
name: Anonymous Planet PGP
|
||||
url: https://anonymousplanet.net/pgp/
|
||||
logo: ../media/profile.png
|
||||
sameAs:
|
||||
- https://github.com/Anon-Planet
|
||||
- https://opencollective.com/anonymousplanetorg
|
||||
- https://mastodon.social/@anonymousplanet
|
||||
---
|
||||
|
||||
<div class="hero-block">
|
||||
<div class="hero-eyebrow">Trust, but verify.</div>
|
||||
<h1 class="hero-title">Our Public Keys<span class="hero-subtitle">Import. Verify. Trust nothing blindly.</span></h1>
|
||||
<p class="hero-tagline">
|
||||
Anonymous Planet signs all releases and communications with GnuPG (PGP) keys.
|
||||
Verify fingerprints against multiple independent sources before importing.
|
||||
</p>
|
||||
<div class="hero-cta-row">
|
||||
<a href="anonymousplanet.asc" class="hero-cta hero-cta--primary">Download Public Keyring</a>
|
||||
<a href="#keys" class="hero-cta hero-cta--secondary">View Fingerprints</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Our Keys { #keys }
|
||||
|
||||
The full keyring is at `https://anonymousplanet.net/pgp/anonymousplanet.asc`.<br>Always verify fingerprints against our [GitHub releases](https://github.com/Anon-Planet/thgtoa/releases) before importing.
|
||||
|
||||
<div class="index-grid">
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Master Signing Key (MSK)</h3>
|
||||
<p class="index-card__body">Announcements, signing subkeys, and trust anchoring. The root of our web of trust.</p>
|
||||
<code class="pgp-fingerprint">9FA5 436D 0EE3 6098 5157 3825 17EC A05F 768D EDF6</code>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Release Signing Key (RSK)</h3>
|
||||
<p class="index-card__body">Signs all guide releases and distribution artifacts. Check this against every release.</p>
|
||||
<code class="pgp-fingerprint">C302 3DBE A3FB 38C4 38BA 1EED CEC6 0AED E8B9 92A2</code>
|
||||
</div>
|
||||
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Email Encryption / Signing Key (ESK)</h3>
|
||||
<p class="index-card__body">Use this to send us encrypted mail or verify signed correspondence from us.</p>
|
||||
<code class="pgp-fingerprint">FCBD 2CAB DEFD 1FBA 2E9E 7591 A1A8 2CD2 DD2C F890</code>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Key Rotation { #rotation }
|
||||
|
||||
Keys may be rotated periodically. Rotation is always announced via [GitHub Releases](https://github.com/Anon-Planet/thgtoa/releases) and the [changelog](../changelog/index.md). If a key you have on file does not appear here, treat it as compromised and re-import.
|
||||
|
||||
---
|
||||
|
||||
## Public Keyring { #keyring }
|
||||
|
||||
<div class="donate-address-block">
|
||||
<div class="donate-address-label">Keyring URL</div>
|
||||
<code class="donate-address">https://anonymousplanet.net/pgp/anonymousplanet.asc</code>
|
||||
</div>
|
||||
|
||||
```txt
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEZc0QYxYJKwYBBAHaRw8BAQdAm8mOR8/0qWrm9Tqzfl9Ks5rjtIbQZLAR/qxH
|
||||
HVGJsxi0LUFub255bW91cyBQbGFuZXQgRW1haWwgRW5jcnlwdGlvbi9TaWduaW5n
|
||||
IEtleYiTBBMWCgA7FiEE/L0sq979H7ounnWRoags0t0s+JAFAmXNEGMCGwMFCwkI
|
||||
BwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQoags0t0s+JDbRAEAuZlBmMGgZ3bh
|
||||
12Js9jjDcu+jhKqL4fJrJG5z9+KFkQwA/An1StA6EhcM7qlzZ5bzm2SZAbP9hQRZ
|
||||
GmfaeU2P5KgHiHgEMBYKACAWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaiCYTgId
|
||||
AAAKCRAX7KBfdo3t9gNUAP9/SyGBYJ7s9YeqLHOJ+veQZjZYHvFGQ7yPn0Fetx0Z
|
||||
LAD/UOQ8rP2QaldCMyVSG8SqfPd7n++SEAXWAl2gAo9mhg6IdQQQFgoAHRYhBJ+l
|
||||
Q20O42CYUVc4JRfsoF92je32BQJmpEUQAAoJEBfsoF92je32tD8A/ir9hE8UjrJE
|
||||
psG+PNfxYAwAagKUGbAMDUxQp3z+t81+AP45hYT4aR89zSQaankHLs3Lh7Cp5ael
|
||||
NBe/BtfR9hCLAYkCMwQQAQgAHRYhBF7WeRgs0hkTAMDm6kyyELegkVLWBQJqIJiM
|
||||
AAoJEEyyELegkVLWjZYP/j1k5vl+r0NDQXmE8hS9IKhaQPggP72iXc5RWeMQHuIv
|
||||
b1laQZm64xerJNdAh0uk1bwfmJnVGfyxBUrlCgAIeVGRSlni2Rig4azaQ1IS0pqF
|
||||
4sC1KzKEhEaNdkh3pJyGtP1cikcSjWeU2oYQou3/7VN3vNyW+n8OAVF+2fsC5d78
|
||||
EvdpZgal+komb+J8Bt552uDbCCVI4TFIPBZmHWoXjaP6L+730YphbV7Aw0L5J6OO
|
||||
ob0nzHn4X0dIvGE7Phdp2e1yNRUOSRLh8B/D5OiE9k7CaeYmJNPv5qOw/R+NgrrA
|
||||
ZFnoOuwHo0D+aL9WT9q4aM/cDCEIbvhQ4l5ZhVGqZuQ9wxNCgPi3ZiZRTfk1PW4v
|
||||
uMw1xGwXBKy7jDO12xWIWWv9MiwIQLw0OxSxKbr76rgucq7e7JrWr64rItu5Wm7F
|
||||
8qxg2cwmDat6tFSRVWlEDy8oNkRMJNjdQJDu3ez9YOfJNnApAz94Of1XU7CUuYjY
|
||||
PV88BaHdUBVtANEzy0iSDCcSj6auzLfv9dBN8cOdUxlVcrPf2jjK6JR/6qe6VWNp
|
||||
wRg9VQW2fe8HJTMUt0o9qQBJUsF68KOHtIdoE4az9AyyBNKl67dKqLB9HoIItLzD
|
||||
MJRcbS2p6plCTNagwPVvgtPRChll9JP3jLPVhRL2BixYVkbHUoJxsEfscTUl6Azt
|
||||
tEtBbm9ueW1vdXMgUGxhbmV0IEVtYWlsIEVuY3J5cHRpb24vU2lnbmluZyBLZXkg
|
||||
PGFub255bW91c3BsYW5ldEBkaXNyb290Lm9yZz6IkwQTFgoAOxYhBPy9LKve/R+6
|
||||
Lp51kaGoLNLdLPiQBQJmhqXqAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheA
|
||||
AAoJEKGoLNLdLPiQJ34A/RJT9Hyj7hT/0D1BbDU6s6YzD+/x7Pyq2+9kNSI0L77W
|
||||
AQAAG+CfDrKDXJtBNKZVNFZpld3wUeoIOcAqLl7KpsVGCYh1BBAWCgAdFiEEn6VD
|
||||
bQ7jYJhRVzglF+ygX3aN7fYFAmakRRAACgkQF+ygX3aN7fbypgEAnEg0IbWnpaLj
|
||||
/4wU179vUZZu/Y0DE63GbJuZjj72hKUA/0xyzIgSvXByjoOkEwCn5w1+RPYXKw7Z
|
||||
syERsDCUAAMIiQIzBBABCAAdFiEEXtZ5GCzSGRMAwObqTLIQt6CRUtYFAmogmIwA
|
||||
CgkQTLIQt6CRUtafPQ/+LTWFU84tDZAM0Hp7bWB0dw8nP0JvNQ2WtZf0flh+r1tF
|
||||
cmVnc9szZBh+zzSpY25iK5+Waa6+l1POYSQpkS67VR0Jrv9nL94YrRhqalSRWsjW
|
||||
MQJO+Obu4LIRIqiMZLJlAd9Bg9FshYagbQDVDOI8v9mxqCzIVm3tBx1Jp57ATHgm
|
||||
sMDWn7l1BI0SkLlG49LYxVDQ6QAx4XLCQw+JzdiJs+yExa5ymYmV61evVVbDV5UF
|
||||
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
|
||||
Eztpvc4cyyGAmI42SLM/jKbqO2yWqwVoAg==
|
||||
=ww/S
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
```
|
||||
@@ -0,0 +1,108 @@
|
||||
.md-typeset code,
|
||||
.md-typeset .admonition code {
|
||||
background-color: rgba(30, 10, 40, 0.80);
|
||||
border: 1px solid rgba(125, 70, 152, 0.30);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
color: var(--crt-amber);
|
||||
}
|
||||
|
||||
.md-typeset a::after,
|
||||
a:not(.btn)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 1px;
|
||||
background: var(--bg-card);
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.md-typeset a:hover::after,
|
||||
a:not(.btn):hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.admonition-note,
|
||||
.admonition-todo,
|
||||
.admonition-important {
|
||||
background: linear-gradient(180deg,
|
||||
var(--bg-secondary) 0%,
|
||||
rgba(0, 255, 0, 0.05) 100%
|
||||
) !important;
|
||||
}
|
||||
|
||||
.admonition-tip,
|
||||
.admonition-hint,
|
||||
.admonition-new {
|
||||
background: linear-gradient(180deg,
|
||||
var(--bg-secondary) 0%,
|
||||
rgba(0, 255, 0, 0.05) 100%
|
||||
) !important;
|
||||
}
|
||||
|
||||
.admonition-caution,
|
||||
.admonition-warning {
|
||||
background: linear-gradient(180deg,
|
||||
var(--bg-secondary) 0%,
|
||||
rgba(255, 165, 0, 0.08) 100%
|
||||
) !important;
|
||||
}
|
||||
|
||||
.admonition-error,
|
||||
.admonition-failure {
|
||||
background: linear-gradient(180deg,
|
||||
var(--bg-secondary) 0%,
|
||||
rgba(239, 68, 68, 0.08) 100%
|
||||
) !important;
|
||||
}
|
||||
|
||||
.md-typeset h1,
|
||||
.md-typeset h2,
|
||||
.md-typeset h3,
|
||||
.md-footer h4,
|
||||
.md-footer p {
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
.admonition-note,
|
||||
.admonition-tip,
|
||||
.admonition-important,
|
||||
.md-typeset details.admonition {
|
||||
background: var(--bg-color) !important;
|
||||
}
|
||||
|
||||
.md-nav__link {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.md-typeset h1,
|
||||
.md-typeset h2,
|
||||
.md-typeset h3,
|
||||
.md-footer h4,
|
||||
.md-footer p {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
code, pre, kbd,
|
||||
.md-typeset code,
|
||||
.md-typeset pre,
|
||||
.md-typeset kbd {
|
||||
font-family: 'Fira Code VF', 'Fira Code', monospace;
|
||||
font-feature-settings: "calt" 1;
|
||||
font-variant-ligatures: contextual;
|
||||
}
|
||||
|
||||
@supports not (font-variation-settings: normal) {
|
||||
code, pre, kbd,
|
||||
.md-typeset code,
|
||||
.md-typeset pre,
|
||||
.md-typeset kbd {
|
||||
font-family: 'Fira Code', monospace;
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
/* Generate dark mode PDF of the HTML at guide/index.html */
|
||||
|
||||
/*
|
||||
DARK_MODE_PDF.CSS
|
||||
Use this stylesheet when generating a PDF from HTML.
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Color Palette */
|
||||
--bg-color: #121212; /* Deep dark grey (easier on eyes than pure black) */
|
||||
--text-primary: #e0e0e0; /* Off-white for readability */
|
||||
--text-secondary: #a0a0a0; /* Grey for captions/metadata */
|
||||
--accent-color: #bb86fc; /* Light purple accent (optional) */
|
||||
--border-color: #333333; /* Subtle borders */
|
||||
|
||||
/* Fonts - System fonts ensure best rendering across PDF engines */
|
||||
--font-main: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* --- RESET & BASE STYLES --- */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-main);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* --- TYPOGRAPHY & HEADINGS --- */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-secondary); /* Slightly dimmer text for body copy */
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* --- CONTAINER & LAYOUT --- */
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 40px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Cards / Sections with dark backgrounds */
|
||||
.card {
|
||||
background-color: #1e1e1e;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* --- TABLES (Common in PDFs) --- */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #2c2c2c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* --- IMAGES --- */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 20px 0;
|
||||
/* This ensures high contrast images don't get washed out */
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
/* --- CRITICAL FOR PDF GENERATORS --- */
|
||||
/* Forces the browser/PDF engine to print background colors and graphics */
|
||||
@media print {
|
||||
@page {
|
||||
size: A4; /* Change to 'Letter' if preferred */
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.card, table th {
|
||||
-webkit-print-color-adjust: exact !important; /* Chrome/Safari */
|
||||
print-color-adjust: exact !important; /* Firefox/Standard */
|
||||
background-color: #1e1e1e !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* Prevent page breaks in the middle of a sentence or card if possible */
|
||||
.card {
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
/* Hide elements you don't want in PDF (like navigation bars) */
|
||||
nav, footer, button {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/* Title sheet: visible only when printing / generating PDF (not on screen). */
|
||||
.pdf-title-page {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.pdf-title-page {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
padding: 5rem 2rem 4rem;
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.pdf-title-page__title {
|
||||
font-size: 1.65rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 1.25rem;
|
||||
}
|
||||
|
||||
.pdf-title-page__subtitle {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.4;
|
||||
margin: 0 0 2rem;
|
||||
}
|
||||
|
||||
.pdf-title-page__meta {
|
||||
font-size: 0.95rem;
|
||||
font-style: normal;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Guide landing: small floating logo so opening copy flows beside it (HTML + PDF). */
|
||||
.guide-intro-lead {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.guide-intro-lead > p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.guide-intro-lead > p:first-child img {
|
||||
float: right;
|
||||
max-width: 6.5rem;
|
||||
height: auto;
|
||||
margin: 0 0 0.5rem 1rem;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.guide-intro-lead > p:first-child img {
|
||||
max-width: 5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
.md-footer {
|
||||
background-color: var(--bg-primary);
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.md-footer-meta {
|
||||
background-color: var(--bg-primary);
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.md-footer-meta__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
padding: 0.75em 1em;
|
||||
}
|
||||
|
||||
.md-footer-copyright {
|
||||
color: var(--text-muted);
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.md-footer-copyright a {
|
||||
color: var(--crt-amber);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.md-footer-copyright a:hover {
|
||||
color: var(--crt-green);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.md-social {
|
||||
display: inline-flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.75em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.md-social__link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
background-color: transparent;
|
||||
opacity: 0.65;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.md-social__link:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.md-social__link svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
fill: var(--crt-green);
|
||||
}
|
||||
|
||||
.md-footer__link {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.75rem;
|
||||
color: var(--crt-amber);
|
||||
}
|
||||
|
||||
.md-footer__link:hover {
|
||||
color: var(--crt-green);
|
||||
}
|
||||
|
||||
@media (max-width: 60em) {
|
||||
.md-social { gap: 0.5em; }
|
||||
.md-social__link svg { width: 18px; height: 18px; }
|
||||
.md-footer-meta__inner { flex-direction: column; align-items: flex-start; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.md-footer *,
|
||||
.md-footer-meta * {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
.md-footer { border-top: 2px solid var(--text-primary); }
|
||||
.md-social__link { opacity: 1; }
|
||||
}
|
||||
|
||||
.hero-block {
|
||||
text-align: center;
|
||||
padding: 4em 1em 3em;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.hero-eyebrow {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.hero-eyebrow::before {
|
||||
content: "// ";
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-family: var(--font-main);
|
||||
font-size: clamp(2.2rem, 6vw, 3.6rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
color: var(--crt-green) !important;
|
||||
margin: 0 0 0.25em;
|
||||
text-shadow: 0 0 30px rgba(0, 240, 128, 0.25);
|
||||
}
|
||||
|
||||
.hero-block .hero-title::before {
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
color: var(--crt-amber);
|
||||
display: block;
|
||||
font-size: 0.65em;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.04em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.hero-tagline {
|
||||
font-family: var(--font-main);
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.7;
|
||||
max-width: 56ch;
|
||||
margin: 1.5em auto 2em;
|
||||
}
|
||||
|
||||
.hero-cta-row {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.hero-cta {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
padding: 0.6em 1.6em;
|
||||
border-radius: 3px;
|
||||
text-decoration: none;
|
||||
transition: all 0.18s ease;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.hero-cta--primary {
|
||||
background: rgba(0, 240, 128, 0.12);
|
||||
border: 1px solid var(--accent-green);
|
||||
color: var(--crt-green);
|
||||
}
|
||||
|
||||
.hero-cta--primary:hover {
|
||||
background: rgba(0, 240, 128, 0.22);
|
||||
box-shadow: 0 0 16px rgba(0, 240, 128, 0.25);
|
||||
color: var(--crt-green);
|
||||
}
|
||||
|
||||
.hero-cta--secondary {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--crt-amber);
|
||||
}
|
||||
|
||||
.hero-cta--secondary:hover {
|
||||
border-color: var(--crt-amber);
|
||||
color: var(--crt-amber);
|
||||
background: rgba(231, 217, 163, 0.07);
|
||||
}
|
||||
|
||||
.alert-banner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75em;
|
||||
background: rgba(255, 76, 76, 0.07);
|
||||
border: 1px solid rgba(255, 76, 76, 0.28);
|
||||
border-left: 4px solid var(--crt-red);
|
||||
padding: 0.85em 1.2em;
|
||||
max-width: 70ch;
|
||||
margin: 2.5em auto 3em;
|
||||
border-radius: 0 4px 4px 0;
|
||||
font-size: 0.88rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.alert-banner .alert-icon {
|
||||
color: var(--crt-red);
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.05em;
|
||||
}
|
||||
|
||||
.alert-banner a {
|
||||
color: var(--crt-red);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.alert-banner a:hover {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.alert-banner code {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.82em;
|
||||
background: rgba(255, 76, 76, 0.12);
|
||||
padding: 0.1em 0.35em;
|
||||
border-radius: 3px;
|
||||
color: #ff8888;
|
||||
}
|
||||
|
||||
.index-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 1.25em;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto 3.5em;
|
||||
}
|
||||
|
||||
.index-card {
|
||||
background: rgba(125, 70, 152, 0.10);
|
||||
border: 1px solid rgba(125, 70, 152, 0.35);
|
||||
border-radius: 6px;
|
||||
padding: 1.5em 1.4em 1.3em;
|
||||
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.index-card:hover {
|
||||
border-color: rgba(125, 70, 152, 0.70);
|
||||
box-shadow: 0 4px 20px rgba(125, 70, 152, 0.15);
|
||||
}
|
||||
|
||||
.index-card__title {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.9rem;
|
||||
color: var(--crt-amber) !important;
|
||||
margin: 0 0 0.5em;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.index-card .index-card__title::before {
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
.index-card__body {
|
||||
font-size: 0.88rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.55;
|
||||
flex: 1;
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
|
||||
.index-card__link {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.75rem;
|
||||
color: var(--crt-green);
|
||||
text-decoration: none;
|
||||
letter-spacing: 0.04em;
|
||||
transition: color 0.15s ease;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.index-card__link:hover {
|
||||
color: var(--accent-green);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.principles-strip {
|
||||
text-align: center;
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.95rem;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 2em 1em 3em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.principle-no {
|
||||
color: var(--crt-green);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.principle-sep {
|
||||
margin: 0 0.75em;
|
||||
color: var(--border-color);
|
||||
}
|
||||
|
||||
.quick-access-card {
|
||||
background: rgba(125, 70, 152, 0.10);
|
||||
border: 1px solid rgba(125, 70, 152, 0.35);
|
||||
border-radius: 6px;
|
||||
padding: 1.4em 1.3em 1.2em;
|
||||
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.quick-access-card:hover {
|
||||
border-color: rgba(125, 70, 152, 0.70);
|
||||
box-shadow: 0 4px 16px rgba(125, 70, 152, 0.15);
|
||||
}
|
||||
|
||||
.quick-access-card h3,
|
||||
.quick-access-card h4 {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.95rem;
|
||||
color: var(--crt-amber) !important;
|
||||
margin: 0 0 0.5em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.quick-access-card p {
|
||||
font-size: 0.88rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.55;
|
||||
margin: 0 0 0.9em;
|
||||
}
|
||||
|
||||
.quick-access-card a {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.78rem;
|
||||
color: var(--crt-amber);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.quick-access-card a:hover {
|
||||
color: var(--crt-green);
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
@font-face {
|
||||
font-family: 'Fira Code VF';
|
||||
src: url('../fonts/fira-code/fira-code-latin-ext-wght-normal.woff2') format('woff2');
|
||||
font-weight: 300 700;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, 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: 'Fira Code VF';
|
||||
src: url('../fonts/fira-code/fira-code-latin-wght-normal.woff2') format('woff2');
|
||||
font-weight: 300 700;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
|
||||
U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||
U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
|
||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'EB Garamond';
|
||||
src: url('../fonts/eb-garamond/eb-garamond-latin-ext-400-normal.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, 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: 'EB Garamond';
|
||||
src: url('../fonts/eb-garamond/eb-garamond-latin-400-normal.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
|
||||
U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||
U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
|
||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'EB Garamond';
|
||||
src: url('../fonts/eb-garamond/eb-garamond-latin-ext-400-italic.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: block;
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, 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: 'EB Garamond';
|
||||
src: url('../fonts/eb-garamond/eb-garamond-latin-400-italic.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: block;
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
|
||||
U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||
U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
|
||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'EB Garamond';
|
||||
src: url('../fonts/eb-garamond/eb-garamond-latin-ext-700-normal.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, 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: 'EB Garamond';
|
||||
src: url('../fonts/eb-garamond/eb-garamond-latin-700-normal.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
|
||||
U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||
U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
|
||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'EB Garamond';
|
||||
src: url('../fonts/eb-garamond/eb-garamond-latin-ext-700-italic.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: block;
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, 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: 'EB Garamond';
|
||||
src: url('../fonts/eb-garamond/eb-garamond-latin-700-italic.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: block;
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
|
||||
U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||
U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
|
||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
--bg-color: #020307;
|
||||
--bg-primary: #060d06;
|
||||
--bg-secondary: #0a150a;
|
||||
--bg-card: #0f1f0f;
|
||||
--text-primary: #c5c5c5;
|
||||
--text-secondary: #a0a0a0;
|
||||
--text-muted: #6b7a6b;
|
||||
--accent-green: #00f080;
|
||||
--accent-green-dim: #039634;
|
||||
--crt-green: #00f080;
|
||||
--crt-amber: #e7d9a3;
|
||||
--crt-blue: #00d9ff;
|
||||
--crt-red: #ff4c4c;
|
||||
--border-color: #0b85005d;
|
||||
--terminal-green-glow: rgba(0, 255, 0, 0.15);
|
||||
--terminal-line-color: rgba(0, 255, 0, 0.30);
|
||||
--terminal-border-faint: rgba(0, 255, 0, 0.08);
|
||||
--status-success: #4ade80;
|
||||
--status-warning: #fbbf24;
|
||||
--status-error: #ef4444;
|
||||
--status-info: #60a5fa;
|
||||
--code-font: 'Fira Code VF', 'Fira Code', monospace;
|
||||
--font-main: 'EB Garamond', Georgia, serif;
|
||||
--md-primary-fg-color: #4052b500;
|
||||
--md-default-bg-color: rgba(0, 255, 0, 0.08);
|
||||
}
|
||||
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-main);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--crt-green);
|
||||
font-weight: 700;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
h2, h3, h4, h5, h6 {
|
||||
color: var(--crt-green);
|
||||
font-weight: 600;
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--crt-green);
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 20px auto;
|
||||
animation: none !important;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--bg-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 40px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.no-js .md-sidebar {
|
||||
align-self: auto;
|
||||
}
|
||||
|
||||
[data-md-component="content"] .md-content__inner,
|
||||
.md-typeset {
|
||||
}
|
||||
|
||||
body:has([data-md-url*="/guide/"]) .md-typeset p,
|
||||
body:has([data-md-url*="/guide/"]) .md-typeset li,
|
||||
body:has([data-md-url*="/guide/"]) .md-typeset dt,
|
||||
body:has([data-md-url*="/guide/"]) .md-typeset dd,
|
||||
body:has([data-md-url*="/guide/"]) .md-typeset blockquote p {
|
||||
font-family: var(--font-main);
|
||||
font-size: 18px;
|
||||
line-height: 1.75;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@media print {
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.card,
|
||||
table th {
|
||||
-webkit-print-color-adjust: exact !important;
|
||||
print-color-adjust: exact !important;
|
||||
background-color: var(--bg-card) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
nav, footer, button {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] {
|
||||
--bg-color: #ffffff;
|
||||
--bg-primary: #f8f8f8;
|
||||
--bg-secondary: #f0f0f0;
|
||||
--bg-card: #ececec;
|
||||
--text-primary: #1a1a1a;
|
||||
--text-secondary: #2c2c2c;
|
||||
--text-muted: #555555;
|
||||
--accent-green: #006633;
|
||||
--accent-green-dim: #008844;
|
||||
--crt-green: #005522;
|
||||
--crt-amber: #4a3800;
|
||||
--crt-blue: #003d6b;
|
||||
--crt-red: #8b0000;
|
||||
--border-color: #cccccc;
|
||||
--terminal-green-glow: rgba(0, 102, 51, 0.08);
|
||||
--terminal-line-color: #bbbbbb;
|
||||
--terminal-border-faint: #e0e0e0;
|
||||
--status-success: #1a5c2e;
|
||||
--status-warning: #7a4f00;
|
||||
--status-error: #8b0000;
|
||||
--status-info: #003d6b;
|
||||
--md-default-bg-color: #ffffff;
|
||||
--md-default-fg-color: #1a1a1a;
|
||||
--md-primary-fg-color: #1a1a1a;
|
||||
--md-primary-bg-color: #ffffff;
|
||||
--md-typeset-color: #1a1a1a;
|
||||
--md-typeset-a-color: #003d6b;
|
||||
--md-code-bg-color: #f4f4f4;
|
||||
--md-code-fg-color: #1a1a1a;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] body::before,
|
||||
[data-md-color-scheme="default"] body::after {
|
||||
display: none !important;
|
||||
content: none !important;
|
||||
background: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] h1,
|
||||
[data-md-color-scheme="default"] h2,
|
||||
[data-md-color-scheme="default"] h3,
|
||||
[data-md-color-scheme="default"] h4,
|
||||
[data-md-color-scheme="default"] h5,
|
||||
[data-md-color-scheme="default"] h6,
|
||||
[data-md-color-scheme="default"] .md-typeset h1,
|
||||
[data-md-color-scheme="default"] .md-typeset h2,
|
||||
[data-md-color-scheme="default"] .md-typeset h3,
|
||||
[data-md-color-scheme="default"] .md-typeset h4,
|
||||
[data-md-color-scheme="default"] .md-typeset h5,
|
||||
[data-md-color-scheme="default"] .md-typeset h6 {
|
||||
color: var(--crt-green) !important;
|
||||
font-family: var(--font-main);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] body,
|
||||
[data-md-color-scheme="default"] .md-typeset {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] a,
|
||||
[data-md-color-scheme="default"] .md-typeset a {
|
||||
color: var(--crt-blue);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] img,
|
||||
[data-md-color-scheme="default"] .md-typeset img {
|
||||
filter: none !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] pre,
|
||||
[data-md-color-scheme="default"] .md-typeset pre, {
|
||||
background: #f4f4f4 !important;
|
||||
color: #1a1a1a !important;
|
||||
border-left-color: #888888 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] code,
|
||||
[data-md-color-scheme="default"] .md-typeset code {
|
||||
background: #eeeeee !important;
|
||||
color: #1a1a1a !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] .md-header {
|
||||
background-color: #1a1a1a;
|
||||
color: #e8e8e8;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] .md-header__button,
|
||||
[data-md-color-scheme="default"] .md-header__topic,
|
||||
[data-md-color-scheme="default"] .md-header__title,
|
||||
[data-md-color-scheme="default"] .md-header__ellipsis {
|
||||
color: #e8e8e8 !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] .md-header__topic::before {
|
||||
color: #a8d8a8;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] .md-tabs {
|
||||
background-color: #2a2a2a;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] .md-tabs__link {
|
||||
color: #cccccc !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] .md-tabs__link--active,
|
||||
[data-md-color-scheme="default"] .md-tabs__link:hover {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] .md-tabs__link--active::after {
|
||||
background-color: #a8d8a8;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] .md-search__input {
|
||||
background-color: rgba(255,255,255,0.12);
|
||||
color: #e8e8e8;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] .md-search__input::placeholder {
|
||||
color: rgba(232,232,232,0.55);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] .md-source {
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
|
||||
[data-md-color-scheme="default"] .admonition {
|
||||
background: #f8f8f8 !important;
|
||||
border-left-color: var(--accent-green) !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] .admonition-title {
|
||||
background: #eeeeee !important;
|
||||
color: #1a1a1a !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] th {
|
||||
background-color: #eeeeee !important;
|
||||
border-bottom-color: var(--crt-amber) !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] td,
|
||||
[data-md-color-scheme="default"] tr {
|
||||
border-color: #cccccc !important;
|
||||
color: #1a1a1a !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
background:
|
||||
linear-gradient(rgba(18, 32, 12, 0) 50%, rgba(0, 20, 0, 0.10) 50%),
|
||||
linear-gradient(90deg, rgba(12, 36, 18, 0) 50%, rgba(0, 16, 0, 0.08) 50%);
|
||||
background-size: 100% 4px, 6px 100%;
|
||||
animation: scanline 4ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes scanline {
|
||||
0% { background-position: 0 0, 0 0; }
|
||||
100% { background-position: 0 4px, 6px 100%; }
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] body::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9997;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
transparent 50%,
|
||||
rgba(0, 0, 0, 0.30) 100%
|
||||
);
|
||||
animation: phosphorDecay 8s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes phosphorDecay {
|
||||
0%, 100% { opacity: 0.4; }
|
||||
50% { opacity: 0.2; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
[data-md-color-scheme="slate"] body::before {
|
||||
background-size: 100% 2px, 3px 100%;
|
||||
animation-duration: 6ms;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
[data-md-color-scheme="slate"] body::before,
|
||||
[data-md-color-scheme="slate"] body::after {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] img,
|
||||
[data-md-color-scheme="slate"] .md-typeset img {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
.command-palette {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 0.5em;
|
||||
border-top: 1px dashed var(--terminal-line-color);
|
||||
border-bottom: 1px dashed var(--terminal-line-color);
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(0, 255, 0, 0.03) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.command-palette::before,
|
||||
.command-palette::after {
|
||||
content: "|";
|
||||
color: var(--accent-green);
|
||||
animation: cursorBlink 1s step-end infinite;
|
||||
}
|
||||
|
||||
.headerlink {
|
||||
color: var(--crt-green);
|
||||
font-size: 0.8rem;
|
||||
margin-left: 0.5em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
pre.highlight {
|
||||
box-shadow: inset 0 0 20px rgba(20, 5, 30, 0.6);
|
||||
}
|
||||
|
||||
pre.highlight .hll {
|
||||
background-color: rgba(0, 255, 0, 0.05);
|
||||
}
|
||||
|
||||
.palette-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.palette-item {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 1.5em;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.palette-item:hover {
|
||||
border-color: var(--accent-green);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 255, 0, 0.2);
|
||||
}
|
||||
|
||||
.palette-item .icon-label {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.palette-item a {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.system-notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: var(--bg-color);
|
||||
border-left: 4px solid var(--accent-green);
|
||||
padding: 1em 1.5em;
|
||||
border-radius: 4px;
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.8rem;
|
||||
box-shadow: 0 8px 24px rgba(0, 255, 0, 0.15);
|
||||
z-index: 10000;
|
||||
min-width: 300px;
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.command-palette::before,
|
||||
.command-palette::after,
|
||||
.system-notification {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.donate-goals-block {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: 4px solid var(--crt-amber);
|
||||
border-radius: 6px;
|
||||
padding: 1.5em 2em;
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
.donate-goals-title {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--crt-amber);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
.donate-goals-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.donate-goal {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.92rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6em;
|
||||
padding: 0.35em 0;
|
||||
}
|
||||
|
||||
.donate-goal-icon {
|
||||
font-size: 1rem;
|
||||
width: 1.2em;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.donate-goal--done {
|
||||
color: var(--status-success);
|
||||
text-decoration: line-through;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.donate-goal--open {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.donate-goal--open .donate-goal-icon {
|
||||
color: var(--crt-amber);
|
||||
}
|
||||
|
||||
.donate-stat-row {
|
||||
display: flex;
|
||||
gap: 1.5em;
|
||||
flex-wrap: wrap;
|
||||
margin: 1em 0 1.5em;
|
||||
}
|
||||
|
||||
.donate-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2em;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 0.75em 1.25em;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.donate-stat-label {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.donate-stat-value {
|
||||
font-family: var(--code-font);
|
||||
font-size: 1.15rem;
|
||||
font-weight: bold;
|
||||
color: var(--crt-amber);
|
||||
}
|
||||
|
||||
.donate-address-block {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 1.5em;
|
||||
margin: 1em 0 2em;
|
||||
}
|
||||
|
||||
.donate-address-label {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
|
||||
.donate-address {
|
||||
display: block;
|
||||
word-break: break-all;
|
||||
font-size: 0.78rem;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 0.6em 0.9em;
|
||||
color: var(--accent-green);
|
||||
user-select: all;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.donate-qr {
|
||||
margin-top: 1.25em;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.donate-qr img {
|
||||
max-width: 150px;
|
||||
height: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.donate-qr--row {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.donate-qr figure {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.4em;
|
||||
}
|
||||
|
||||
.donate-qr figcaption {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.donate-stat-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
.donate-address {
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
.donate-qr--row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.pgp-fingerprint {
|
||||
display: block;
|
||||
margin-top: 0.75em;
|
||||
word-break: break-all;
|
||||
font-size: 0.78rem;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 0.5em 0.75em;
|
||||
color: var(--crt-green);
|
||||
user-select: all;
|
||||
cursor: text;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
@keyframes cursorBlink {
|
||||
0%, 49% { opacity: 1; }
|
||||
50%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.md-header__topic::before {
|
||||
content: "λ ";
|
||||
color: var(--crt-amber);
|
||||
font-family: var(--code-font);
|
||||
margin-right: 0.25em;
|
||||
animation: cursorBlink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes terminalLoad {
|
||||
from { opacity: 0; transform: translateY(25px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.md-typeset h1 {
|
||||
animation: terminalLoad 200ms ease-out forwards;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--crt-amber);
|
||||
font-family: var(--font-main);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
margin-right: 0.02em;
|
||||
}
|
||||
|
||||
strong,
|
||||
p strong {
|
||||
text-shadow: 0 0 1px var(--border-color);
|
||||
}
|
||||
|
||||
pre {
|
||||
background: rgba(30, 10, 40, 0.85);
|
||||
border-left: 3px solid rgba(125, 70, 152, 0.55);
|
||||
box-shadow: inset 0 0 0 1px rgba(125, 70, 152, 0.12);
|
||||
}
|
||||
|
||||
pre[role="presentation"] [data-line] {
|
||||
color: var(--text-muted);
|
||||
font-family: var(--code-font);
|
||||
user-select: none;
|
||||
min-width: 3em;
|
||||
text-align: right;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
pre code .token-keyword { color: var(--crt-blue); font-weight: bold; }
|
||||
pre code .token-string { color: var(--status-success); }
|
||||
pre code .token-comment { color: var(--text-muted); font-style: italic; }
|
||||
|
||||
kbd,
|
||||
.kbd,
|
||||
[data-type="kbd"] {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 0.2em 0.5em;
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.85em;
|
||||
color: var(--accent-green);
|
||||
text-shadow: 0 1px 2px rgba(0, 255, 0, 0.10);
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
kbd:hover {
|
||||
border-color: var(--accent-green);
|
||||
box-shadow: 0 2px 8px rgba(0, 255, 0, 0.20);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.admonition {
|
||||
border-left: 4px solid var(--accent-green);
|
||||
background: linear-gradient(180deg, transparent 0%, rgba(0, 255, 0, 0.03) 100%);
|
||||
}
|
||||
|
||||
.admonition-title {
|
||||
font-family: var(--code-font);
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85em;
|
||||
color: var(--accent-green);
|
||||
background: var(--terminal-green-glow);
|
||||
padding: 0.4em 0.8em;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
.md-typeset details.admonition {
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-primary);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.md-typeset table:not([class]) {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.md-typeset__table th {
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 2px solid var(--crt-amber);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.md-typeset__table tr:hover {
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid var(--accent-green);
|
||||
margin: 1.5em 0;
|
||||
padding-left: 1.5em;
|
||||
background: linear-gradient(90deg, rgba(0, 255, 0, 0.05) 0%, transparent 100%);
|
||||
}
|
||||
|
||||
blockquote p:first-of-type::before {
|
||||
content: "› ";
|
||||
color: var(--accent-green);
|
||||
font-family: var(--code-font);
|
||||
}
|
||||
|
||||
.md-typeset mark,
|
||||
.highlight-mark {
|
||||
background-color: rgba(255, 235, 0, 0.30);
|
||||
color: inherit;
|
||||
padding: 0.1em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.85em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
padding: 0.25em 0.5em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-success { color: var(--status-success); background: rgba(74, 222, 128, 0.10); border: 1px solid rgba(74, 222, 128, 0.30); }
|
||||
.status-warning { color: var(--status-warning); background: rgba(251, 191, 36, 0.10); border: 1px solid rgba(251, 191, 36, 0.30); }
|
||||
.status-error { color: var(--status-error); background: rgba(239, 68, 68, 0.10); border: 1px solid rgba(239, 68, 68, 0.30); }
|
||||
.status-info { color: var(--status-info); background: rgba(96, 165, 250, 0.10); border: 1px solid rgba(96, 165, 250, 0.30); }
|
||||
|
||||
.terminal-window {
|
||||
background: linear-gradient(180deg, var(--bg-primary) 0%, var(--bg-card) 100%);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 1.5em;
|
||||
position: relative;
|
||||
box-shadow: 0 8px 32px rgba(0, 255, 0, 0.10);
|
||||
}
|
||||
|
||||
.terminal-window::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, transparent, var(--accent-green), transparent);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-track { background: var(--bg-primary); }
|
||||
::-webkit-scrollbar-thumb { background: var(--terminal-green-glow); border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--accent-green-dim); }
|
||||
|
||||
.btn {
|
||||
font-family: var(--code-font);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-size: 0.85em;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
[data-md-color-scheme="slate"] body::before {
|
||||
background-size: 100% 2px, 3px 100%;
|
||||
animation-duration: 6ms;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
:root {
|
||||
--accent-green: #fff;
|
||||
--bg-primary: #000;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.md-header__topic::before,
|
||||
.md-typeset h1 {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
.md-sidebar {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.md-sidebar__scrollwrap {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.md-header__topic {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: -0.02em;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.md-header__title {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.md-typeset h1::before {
|
||||
content: "λ ";
|
||||
color: var(--text-primary);
|
||||
font-family: var(--code-font);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.md-nav__link {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.6rem;
|
||||
letter-spacing: 0.01em;
|
||||
color: var(--crt-amber);
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.md-nav--primary > .md-nav__list > .md-nav__item > .md-nav__link {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.md-nav__link--active,
|
||||
.md-nav__link[aria-current] {
|
||||
color: var(--accent-green) !important;
|
||||
font-weight: 600;
|
||||
border-left: 2px solid var(--accent-green);
|
||||
padding-left: 0.5em;
|
||||
background-color: rgba(0, 255, 0, 0.06);
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
.md-nav__link:hover {
|
||||
color: var(--crt-green);
|
||||
}
|
||||
|
||||
.md-nav__link:focus-visible {
|
||||
outline: 1px solid var(--accent-green);
|
||||
outline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] {
|
||||
--md-default-bg-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.md-header__button.md-icon {
|
||||
padding: 0.5em;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.md-typeset {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.md-typeset a {
|
||||
color: var(--crt-green);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.md-typeset h1,
|
||||
.md-typeset h2,
|
||||
.md-typeset h3 {
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
.md-footer-meta {
|
||||
background-color: var(--bg-primary);
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 76.25em) {
|
||||
.md-main__inner {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
@media print {
|
||||
|
||||
/* Reset all custom properties to light values */
|
||||
:root,
|
||||
[data-md-color-scheme],
|
||||
[data-md-color-scheme="slate"] {
|
||||
--bg-color: #ffffff;
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8f8f8;
|
||||
--bg-card: #f4f4f4;
|
||||
|
||||
--text-primary: #1a1a1a;
|
||||
--text-secondary: #2c2c2c;
|
||||
--text-muted: #555555;
|
||||
|
||||
--accent-green: #005c2e;
|
||||
--accent-green-dim: #007a3d;
|
||||
|
||||
--crt-green: #005c2e;
|
||||
--crt-amber: #5c4000;
|
||||
--crt-blue: #00487a;
|
||||
--crt-red: #8b0000;
|
||||
|
||||
--border-color: #cccccc;
|
||||
--terminal-green-glow: transparent;
|
||||
--terminal-line-color: #aaaaaa;
|
||||
--terminal-border-faint: #dddddd;
|
||||
|
||||
--status-success: #1a5c2e;
|
||||
--status-warning: #7a4f00;
|
||||
--status-error: #8b0000;
|
||||
--status-info: #003d6b;
|
||||
|
||||
--md-default-bg-color: #ffffff;
|
||||
--md-default-fg-color: #1a1a1a;
|
||||
--md-primary-fg-color: #1a1a1a;
|
||||
--md-typeset-color: #1a1a1a;
|
||||
--md-typeset-a-color: #00487a;
|
||||
}
|
||||
|
||||
html, body {
|
||||
background: #ffffff !important;
|
||||
color: #1a1a1a !important;
|
||||
font-family: 'EB Garamond', Georgia, 'Times New Roman', serif !important;
|
||||
font-size: 11pt !important;
|
||||
line-height: 1.65 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
body::before,
|
||||
body::after {
|
||||
display: none !important;
|
||||
content: none !important;
|
||||
background: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 25mm 28mm 28mm 28mm;
|
||||
}
|
||||
|
||||
@page :first {
|
||||
margin-top: 30mm;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
text-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Hide footnotes and inline reference superscripts in PDF output */
|
||||
.footnote,
|
||||
.md-typeset .footnote,
|
||||
section.footnotes,
|
||||
.md-typeset section.footnotes,
|
||||
[id^="fn:"],
|
||||
.md-typeset [id^="fn:"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide inline footnote back-reference superscripts [^N] */
|
||||
sup.footnote-ref,
|
||||
.md-typeset sup.footnote-ref,
|
||||
a.footnote-backref,
|
||||
.md-typeset a.footnote-backref,
|
||||
sup > a[href^="#fn"],
|
||||
.md-typeset sup > a[href^="#fn"],
|
||||
sup > a[href^="#fnref"],
|
||||
.md-typeset sup > a[href^="#fnref"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.md-header,
|
||||
.md-footer,
|
||||
.md-sidebar,
|
||||
.md-nav,
|
||||
.md-search,
|
||||
.md-tabs,
|
||||
.md-social,
|
||||
.md-top,
|
||||
[data-md-component="header"],
|
||||
[data-md-component="sidebar"],
|
||||
[data-md-component="footer"],
|
||||
[data-md-component="toc"],
|
||||
.headerlink,
|
||||
.md-content__button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.md-main,
|
||||
.md-main__inner,
|
||||
.md-content {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
}
|
||||
|
||||
.md-content__inner {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 0 0 12mm !important;
|
||||
float: none !important;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
.md-typeset h1,
|
||||
.md-typeset h2,
|
||||
.md-typeset h3,
|
||||
.md-typeset h4,
|
||||
.md-typeset h5,
|
||||
.md-typeset h6 {
|
||||
color: #1a1a1a !important;
|
||||
font-family: 'EB Garamond', Georgia, serif !important;
|
||||
font-weight: 700 !important;
|
||||
page-break-after: avoid;
|
||||
break-after: avoid;
|
||||
}
|
||||
|
||||
.md-typeset h1::before,
|
||||
.md-typeset h2::before {
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
h1, .md-typeset h1 { font-size: 20pt !important; margin: 18pt 0 6pt !important; }
|
||||
h2, .md-typeset h2 { font-size: 15pt !important; margin: 14pt 0 5pt !important; }
|
||||
h3, .md-typeset h3 { font-size: 12pt !important; margin: 10pt 0 4pt !important; }
|
||||
h4, .md-typeset h4 { font-size: 11pt !important; margin: 8pt 0 3pt !important; }
|
||||
h5, h6,
|
||||
.md-typeset h5,
|
||||
.md-typeset h6 { font-size: 10pt !important; margin: 6pt 0 2pt !important; }
|
||||
|
||||
p, li, dt, dd, blockquote p,
|
||||
.md-typeset p,
|
||||
.md-typeset li,
|
||||
.md-typeset dt,
|
||||
.md-typeset dd {
|
||||
color: #1a1a1a !important;
|
||||
font-family: 'EB Garamond', Georgia, serif !important;
|
||||
font-size: 11pt !important;
|
||||
line-height: 1.65 !important;
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
|
||||
a, .md-typeset a {
|
||||
color: #00487a !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
code, .md-typeset code {
|
||||
font-family: 'Fira Code VF', 'Fira Code', 'Courier New', monospace !important;
|
||||
font-size: 9pt !important;
|
||||
background: #f4f4f4 !important;
|
||||
color: #1a1a1a !important;
|
||||
border: 1px solid #cccccc !important;
|
||||
padding: 0.1em 0.3em !important;
|
||||
border-radius: 2px !important;
|
||||
}
|
||||
|
||||
pre, .md-typeset pre {
|
||||
font-family: 'Fira Code VF', 'Fira Code', 'Courier New', monospace !important;
|
||||
font-size: 8.5pt !important;
|
||||
background: #f8f8f8 !important;
|
||||
color: #1a1a1a !important;
|
||||
border: 1px solid #cccccc !important;
|
||||
border-left: 3px solid #555555 !important;
|
||||
padding: 0.75em 1em !important;
|
||||
white-space: pre-wrap !important;
|
||||
word-break: break-all !important;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
pre code, .md-typeset pre code {
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
font-size: inherit !important;
|
||||
}
|
||||
|
||||
table, .md-typeset table {
|
||||
border-collapse: collapse !important;
|
||||
width: 100% !important;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
font-size: 10pt !important;
|
||||
}
|
||||
|
||||
th, .md-typeset th {
|
||||
background: #eeeeee !important;
|
||||
color: #1a1a1a !important;
|
||||
border: 1px solid #aaaaaa !important;
|
||||
padding: 5pt 8pt !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
td, .md-typeset td {
|
||||
color: #1a1a1a !important;
|
||||
border: 1px solid #cccccc !important;
|
||||
padding: 4pt 8pt !important;
|
||||
}
|
||||
|
||||
tr:nth-child(even) td {
|
||||
background: #f9f9f9 !important;
|
||||
}
|
||||
|
||||
.admonition,
|
||||
.md-typeset .admonition,
|
||||
details.admonition {
|
||||
background: #f8f8f8 !important;
|
||||
border-left: 4px solid #555555 !important;
|
||||
border: 1px solid #cccccc !important;
|
||||
color: #1a1a1a !important;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
.admonition-title,
|
||||
.md-typeset .admonition-title {
|
||||
background: #eeeeee !important;
|
||||
color: #1a1a1a !important;
|
||||
}
|
||||
|
||||
blockquote, .md-typeset blockquote {
|
||||
border-left: 3px solid #999999 !important;
|
||||
background: none !important;
|
||||
color: #333333 !important;
|
||||
padding-left: 1em !important;
|
||||
margin: 0.75em 0 !important;
|
||||
}
|
||||
|
||||
blockquote p::before,
|
||||
.md-typeset blockquote p:first-of-type::before {
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
img,
|
||||
.md-typeset img,
|
||||
figure img,
|
||||
[data-md-color-scheme="slate"] img,
|
||||
[data-md-color-scheme="slate"] .md-typeset img {
|
||||
display: block !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
filter: none !important;
|
||||
-webkit-filter: none !important;
|
||||
opacity: 1 !important;
|
||||
mix-blend-mode: normal !important;
|
||||
image-rendering: auto !important;
|
||||
-webkit-print-color-adjust: exact !important;
|
||||
print-color-adjust: exact !important;
|
||||
}
|
||||
|
||||
html::before, html::after,
|
||||
body::before, body::after,
|
||||
[data-md-color-scheme="slate"] body::before,
|
||||
[data-md-color-scheme="slate"] body::after,
|
||||
.md-header::before, .md-header::after {
|
||||
display: none !important;
|
||||
content: none !important;
|
||||
background: none !important;
|
||||
animation: none !important;
|
||||
opacity: 0 !important;
|
||||
position: static !important;
|
||||
}
|
||||
|
||||
kbd, .md-typeset kbd {
|
||||
background: #eeeeee !important;
|
||||
border: 1px solid #aaaaaa !important;
|
||||
border-bottom: 2px solid #888888 !important;
|
||||
color: #1a1a1a !important;
|
||||
font-family: 'Fira Code VF', 'Fira Code', monospace !important;
|
||||
text-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
title: Notice
|
||||
---
|
||||
# Anonymous Planet has left Twitter
|
||||
|
||||
Anonymous Planet has moved to Mastodon. This was largely due to Twitter being owned by Elon Musk, the $8 fees, and recent security issues that have come to light. We do not regret this decision, as it has only gotten worse for Twitter users since.
|
||||
|
||||
- [Twitter $8 Fee Exploited by Cybercriminals](https://heimdalsecurity.com/blog/twitter-8-fee-exploited-by-cybercriminals/)
|
||||
|
||||
Our mastodon can be found at [@anonymousplanet@mastodon.social](https://mastodon.social/@anonymousplanet)
|
||||
+107
-87
@@ -1,126 +1,146 @@
|
||||
---
|
||||
title: "Verify"
|
||||
description: How to verify the authenticity of our files and check virus scans
|
||||
description: "Verify the authenticity and integrity of Anonymous Planet releases."
|
||||
hide:
|
||||
- toc
|
||||
schema:
|
||||
"@context": https://schema.org
|
||||
"@type": Organization
|
||||
"@id": https://anonymousplanet.net/
|
||||
name: Anonymous Planet
|
||||
url: https://anonymousplanet.net/verify/
|
||||
logo: ../media/profile.png
|
||||
---
|
||||
|
||||
# PDF Verification Guide
|
||||
<div class="hero-block">
|
||||
<div class="hero-eyebrow">Never blindly trust anything you download.</div>
|
||||
<h1 class="hero-title">Verify Releases<span class="hero-subtitle">Signatures. Hashes. Trust nothing blindly.</span></h1>
|
||||
<p class="hero-tagline">
|
||||
Every release is GPG-signed and hashed. Verify before you read.
|
||||
</p>
|
||||
<div class="hero-cta-row">
|
||||
<a href="#quick-verification" class="hero-cta hero-cta--primary">Quick Verification</a>
|
||||
<a href="../pgp/" class="hero-cta hero-cta--secondary">Import Our Keys</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Files Provided
|
||||
---
|
||||
|
||||
For each PDF release, you'll receive:
|
||||
## What We Publish { #artifacts }
|
||||
|
||||
- **PDF file** (`thgtoa.pdf` or `thgtoa-dark.pdf`) - The actual document
|
||||
- **Signature file** (`.sig`) - GPG detached signature for authenticity verification
|
||||
- **Hash file** (`.sha256`) - SHA256 checksum for integrity verification
|
||||
<div class="index-grid">
|
||||
|
||||
## Quick Verification
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">PDF Guide</h3>
|
||||
<p class="index-card__body"><code>thgtoa.pdf</code> and <code>thgtoa-dark.pdf</code> — the full guide in light and dark mode. The only canonical single-file export.</p>
|
||||
<a href="https://github.com/Anon-Planet/thgtoa/releases" class="index-card__link">Latest release</a>
|
||||
</div>
|
||||
|
||||
### Using Python Script (Recommended)
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Detached Signatures</h3>
|
||||
<p class="index-card__body"><code>.asc</code> files for every PDF and hash file, signed with the Release Signing Key (RSK). Verify with <code>gpg --verify</code>.</p>
|
||||
<a href="../pgp/" class="index-card__link">Our keys</a>
|
||||
</div>
|
||||
|
||||
```bash
|
||||
# Verify everything (hashes, signatures, and optionally VirusTotal)
|
||||
<div class="index-card">
|
||||
<h3 class="index-card__title">Hash Files</h3>
|
||||
<p class="index-card__body"><code>sha256sums.txt</code> and <code>b2sums.txt</code> for integrity. Both are also signed. Check with <code>sha256sum -c</code> or <code>b2sum -c</code>.</p>
|
||||
<a href="#manual-verification" class="index-card__link">Manual steps</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Quick Verification { #quick-verification }
|
||||
|
||||
### Using the Python Script (Recommended)
|
||||
|
||||
```sh
|
||||
# Verify everything — hashes, signatures, and optionally VirusTotal
|
||||
python scripts/verify_pdf.py --all
|
||||
|
||||
# Only verify hashes
|
||||
# Hashes only
|
||||
python scripts/verify_pdf.py --hashes
|
||||
|
||||
# Only verify GPG signatures
|
||||
# GPG signatures only
|
||||
python scripts/verify_pdf.py --signatures
|
||||
|
||||
# Check VirusTotal scan status (requires VT_API_KEY environment variable)
|
||||
# VirusTotal scan status (requires VT_API_KEY env var)
|
||||
python scripts/verify_pdf.py --vt
|
||||
```
|
||||
|
||||
### Manual Verification
|
||||
---
|
||||
|
||||
#### 1. Verify SHA256 Hash
|
||||
## Manual Verification { #manual-verification }
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
cd /path/to/repo
|
||||
sha256sum -c sha256sum-light.txt
|
||||
### 1. Import the key
|
||||
|
||||
```sh
|
||||
gpg --import pgp/anonymousplanet.asc
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
Get-FileHash -Algorithm SHA256 export\thgtoa.pdf | Select-Object Hash
|
||||
# Compare with the hash in thgtoa.pdf.sha256
|
||||
Verify the fingerprint against our [PGP page](../pgp/index.md) and [GitHub releases](https://github.com/Anon-Planet/thgtoa/releases) before trusting it.
|
||||
|
||||
### 2. Verify the PDFs
|
||||
|
||||
```sh
|
||||
gpg --verify export/thgtoa.pdf.asc export/thgtoa.pdf
|
||||
gpg --verify export/thgtoa-dark.pdf.asc export/thgtoa-dark.pdf
|
||||
```
|
||||
|
||||
#### 2. Verify GPG Signature
|
||||
Expected output:
|
||||
|
||||
First, import the public key:
|
||||
```bash
|
||||
gpg --import pgp/anonymousplanet-master.asc
|
||||
```text
|
||||
gpg: Signature made Sun 31 May 2026 03:23:26 AM EDT
|
||||
gpg: using EDDSA key C3023DBEA3FB38C438BA1EEDCEC60AEDE8B992A2
|
||||
gpg: Good signature from "Anonymous Planet Release Signing Key" [ultimate]
|
||||
Primary key fingerprint: C302 3DBE A3FB 38C4 38BA 1EED CEC6 0AED E8B9 92A2
|
||||
```
|
||||
|
||||
Then verify the signature:
|
||||
```bash
|
||||
gpg --verify export/thgtoa.pdf.sig export/thgtoa.pdf
|
||||
gpg --verify export/thgtoa-dark.pdf.sig export/thgtoa-dark.pdf
|
||||
```
|
||||
!!! note "About the WARNING"
|
||||
`WARNING: This key is not certified with a trusted signature` is expected. It means the key has not been co-signed by another key in your web of trust — not that the signature is invalid.
|
||||
|
||||
Expected output for successful verification:
|
||||
```
|
||||
gpg: Signature made Mon 20 Apr 2026 01:46:40 AM EDT
|
||||
gpg: using EDDSA key 9FA5436D0EE360985157382517ECA05F768DEDF6
|
||||
gpg: Good signature from "Anonymous Planet Master Signing Key" [unknown]
|
||||
gpg: WARNING: This key is not certified with a trusted signature!
|
||||
gpg: There is no indication that the signature belongs to the owner.
|
||||
Primary key fingerprint: 9FA5 436D 0EE3 6098 5157 3825 17EC A05F 768D EDF6
|
||||
```
|
||||
### 3. Check hashes
|
||||
|
||||
#### 3. Check VirusTotal Status
|
||||
=== "Linux / macOS"
|
||||
|
||||
Visit the VirusTotal report links (automatically generated in release notes):
|
||||
- Light mode: `https://www.virustotal.com/gui/file/[hash]`
|
||||
- Dark mode: `https://www.virustotal.com/gui/file/[hash]`
|
||||
```sh
|
||||
sha256sum -c sha256sums.txt
|
||||
b2sum -c b2sums.txt
|
||||
```
|
||||
|
||||
Or use the Python script with API key:
|
||||
```bash
|
||||
=== "Windows (PowerShell)"
|
||||
|
||||
```powershell
|
||||
Get-FileHash -Algorithm SHA256 export\thgtoa.pdf | Select-Object Hash
|
||||
# Compare with the value in thgtoa.pdf.sha256
|
||||
```
|
||||
|
||||
### 4. VirusTotal (optional)
|
||||
|
||||
```sh
|
||||
export VT_API_KEY=your_vt_api_key
|
||||
python scripts/verify_pdf.py --vt
|
||||
```
|
||||
|
||||
## Automated Verification in CI/CD
|
||||
|
||||
The GitHub Actions workflows automatically:
|
||||
|
||||
1. **Build PDFs** from MkDocs source
|
||||
2. **Generate SHA256 hashes** and save to root directory
|
||||
3. **Sign files with GPG** using the repository's private key
|
||||
4. **Scan with VirusTotal** and update release notes
|
||||
5. **Create releases** with all verification artifacts
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Always verify signatures** before opening PDFs from untrusted sources
|
||||
2. **Check hashes** to ensure files weren't corrupted during download
|
||||
3. **Review VirusTotal results** for any suspicious detections
|
||||
4. **Import keys securely** - verify key fingerprints with the project maintainers
|
||||
5. **Keep verification scripts updated** to match current security standards
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Good signature" but wrong owner?
|
||||
- Ensure you imported the correct public key
|
||||
- Check the key fingerprint matches the official one from the repository
|
||||
|
||||
### Hash mismatch?
|
||||
- Re-download the file (corruption during transfer)
|
||||
- Verify you're checking against the correct hash file
|
||||
- Check for disk errors on your system
|
||||
|
||||
### GPG not found?
|
||||
- Install GPG: `sudo apt install gnupg` (Debian/Ubuntu) or `brew install gnupg` (macOS)
|
||||
- On Windows, use [Gpg4win](https://www.gpg4win.org/)
|
||||
|
||||
## Key Information
|
||||
|
||||
**Signing Key:** Anonymous Planet Master Signing Key ("MSK")
|
||||
**Key ID:** See `pgp/anonymousplanet-master.asc` for details
|
||||
**Fingerprint:** Verify from the repository's official documentation
|
||||
Or open the VirusTotal report URLs listed in the release notes directly.
|
||||
|
||||
---
|
||||
|
||||
*For questions or issues with verification, please open an issue on GitHub.*
|
||||
## Troubleshooting { #troubleshooting }
|
||||
|
||||
**"Good signature" but wrong owner?**
|
||||
Ensure you imported the correct key from [`pgp/`](../pgp/index.md). Check the fingerprint matches the RSK: `C302 3DBE A3FB 38C4 38BA 1EED CEC6 0AED E8B9 92A2`.
|
||||
|
||||
**Hash mismatch?**
|
||||
Re-download the file. Verify you are using the correct hash file for the edition (light vs dark). Check for disk errors.
|
||||
|
||||
**GPG not installed?**
|
||||
|
||||
| Platform | Command |
|
||||
|---|---|
|
||||
| Debian / Ubuntu | `sudo apt install gnupg` |
|
||||
| RHEL / Fedora | `sudo dnf install gnupg2` |
|
||||
| macOS | `brew install gnupg` |
|
||||
| Windows | [Gpg4win](https://gpg4win.org) |
|
||||
|
||||
Binary file not shown.
@@ -1,8 +0,0 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iJEEABYKADkWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaeXaqxsUgAAAAAAEAA5t
|
||||
YW51MiwyLjUrMS4xMiwyLDIACgkQF+ygX3aN7fY6QAD/YCGJqs9HiRllFrF9EluE
|
||||
Ga4XUEQ/R6Q2zc+X6lX856sBAJIpxeMxUmMUXyr3xBAHxUf5eV+nQYkQQMKI81L1
|
||||
x8gL
|
||||
=VX6l
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -1 +0,0 @@
|
||||
f212d0425b38d5cd10da6dc804b60f143da23d4b07051aae31d0966082519b300af0e1c423683e0223738b33b138c687232b1c8bd68cf643777bbc5b588152bd ./export/thgtoa-dark.pdf
|
||||
@@ -1,8 +0,0 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iJEEABYKADkWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaeXaqxsUgAAAAAAEAA5t
|
||||
YW51MiwyLjUrMS4xMiwyLDIACgkQF+ygX3aN7fbdDgEAoSslLR47ydW/3r1wJOPY
|
||||
X/waLkVbkGZpHqwd4RjywwcA/3B7Ci+jUg+yP5TRsuChagEhwyO5vw2DxSlUGoB4
|
||||
+ksH
|
||||
=2ja9
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -1,8 +0,0 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iJEEABYKADkWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaeXaqxsUgAAAAAAEAA5t
|
||||
YW51MiwyLjUrMS4xMiwyLDIACgkQF+ygX3aN7faErgD/Svj1G+B7gmrZQ6AsLZ5J
|
||||
HfeldxjmrXE99dig1iHtl5IBAMndZZb+95TO03IZ9eLGfYuyTz4GCUanmftsY9yv
|
||||
LAIN
|
||||
=MEd0
|
||||
-----END PGP SIGNATURE-----
|
||||
Binary file not shown.
@@ -1,8 +0,0 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iJEEABYKADkWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaeXaqxsUgAAAAAAEAA5t
|
||||
YW51MiwyLjUrMS4xMiwyLDIACgkQF+ygX3aN7favvgEAvFFSB5NrsrKMYvGG5ZYB
|
||||
iLIyt8Sn1rZmlVkibssMPq0BAImpZe8S7hWNkbukyEC4sLbKiOYvjbVipQHnrIUV
|
||||
xPMH
|
||||
=0hnj
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -1 +0,0 @@
|
||||
436ed0df78c299f95b8d5ff94f43f26ec2e7825d92d843fc15419630d55ed5e0c98485e738c12715a2b6242633faae38e8a98935b361d44ddde97a1692cb01a1 ./export/thgtoa.pdf
|
||||
@@ -1,8 +0,0 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iJEEABYKADkWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaeXaqxsUgAAAAAAEAA5t
|
||||
YW51MiwyLjUrMS4xMiwyLDIACgkQF+ygX3aN7fatsgEAixDzH+zTnKYMEx3sikWp
|
||||
dsNTiHTU6wJY/brVJIU879UBAJntBIq72vqwKtMb/ZlVvomdDvKVllZw8ZsYBz1n
|
||||
aTkM
|
||||
=vkgy
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -1,8 +0,0 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iJEEABYKADkWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaeXaqxsUgAAAAAAEAA5t
|
||||
YW51MiwyLjUrMS4xMiwyLDIACgkQF+ygX3aN7faAGQEAyEhVKrRoXIsV3E5f1FZg
|
||||
8fcsmbxCnKBqxichCkf0dWYBAIvbI146mQLHaNqLDaTIqCUQbkq1aE/YMFDGykUG
|
||||
ngsJ
|
||||
=/0RY
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -1,16 +0,0 @@
|
||||
## VirusTotal Scan Results
|
||||
|
||||
**Scan Date:** 2026-04-19 01:48 UTC
|
||||
|
||||
---
|
||||
|
||||
### thgtoa.pdf
|
||||
- **SHA256 Hash:** `f82f6f53319315568fc2524b4eaf01126fe52356a20363cd358ad5977388ba28`
|
||||
- **VirusTotal Report:** VT_API_KEY not configured, scan skipped
|
||||
|
||||
### thgtoa-dark.pdf
|
||||
- **SHA256 Hash:** `94a0c8e3b81b0aeeb921029a41713d81b836da893a9bc9f905ca7296e82bd70f`
|
||||
- **VirusTotal Report:** VT_API_KEY not configured, scan skipped
|
||||
|
||||
---
|
||||
*Scan performed automatically by GitHub Actions*
|
||||
@@ -1,8 +0,0 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iJEEABYKADkWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaeXaqxsUgAAAAAAEAA5t
|
||||
YW51MiwyLjUrMS4xMiwyLDIACgkQF+ygX3aN7fYpCgEA209U3QewChp7mdrrFjH1
|
||||
CaBMIk2sCHwRMCcmbMDkNTAA/RIchAKex13ZjZWC9xsJpZEktvBENFsQLsNPReqR
|
||||
UZ8C
|
||||
=TYsa
|
||||
-----END PGP SIGNATURE-----
|
||||
+54
-46
@@ -1,12 +1,12 @@
|
||||
site_name: Hitchhiker's Guide
|
||||
site_name: The Hitchhiker's Guide
|
||||
site_author: Anonymous Planet
|
||||
site_description: "The comprehensive guide for online anonymity and OpSec."
|
||||
site_dir: '/site/'
|
||||
docs_dir: 'docs/'
|
||||
site_url: "https://www.anonymousplanet.org/"
|
||||
site_description: "The comprehensive guide for online #anonymity and #opsec."
|
||||
site_dir: "site"
|
||||
docs_dir: "docs"
|
||||
site_url: "https://anonymousplanet.net/"
|
||||
repo_url: "https://github.com/Anon-Planet/thgtoa"
|
||||
repo_name: ""
|
||||
#edit_uri: ""
|
||||
#edit_uri: ""blob/main/docs/{path}?plain=1""
|
||||
theme:
|
||||
name: material
|
||||
locale: en
|
||||
@@ -14,14 +14,13 @@ theme:
|
||||
icon:
|
||||
logo: material/bird
|
||||
repo: simple/github
|
||||
font:
|
||||
text: Public Sans
|
||||
code: Liberation Mono
|
||||
font: false # all fonts self-hosted in docs/fonts/
|
||||
features:
|
||||
- navigation.instant
|
||||
- navigation.instant.prefetch
|
||||
- navigation.tabs
|
||||
- navigation.sections
|
||||
- navigation.path
|
||||
- toc.integrate
|
||||
- navigation.top
|
||||
- search.suggest
|
||||
@@ -31,43 +30,58 @@ theme:
|
||||
- content.code.copy
|
||||
language: en
|
||||
palette:
|
||||
- scheme: default
|
||||
toggle:
|
||||
icon: material/eye-outline
|
||||
name: Switch to dark mode
|
||||
primary: green
|
||||
accent: blue
|
||||
- scheme: slate
|
||||
primary: hacker
|
||||
accent: green
|
||||
toggle:
|
||||
icon: material/eye
|
||||
name: Switch to light mode
|
||||
primary: grey
|
||||
accent: teal
|
||||
icon: material/monitor-off
|
||||
name: Printing Press
|
||||
- scheme: default
|
||||
primary: custom
|
||||
accent: custom
|
||||
toggle:
|
||||
icon: material/monitor
|
||||
name: Monitor
|
||||
|
||||
plugins:
|
||||
- social: {}
|
||||
# - with-pdf:
|
||||
- search:
|
||||
separator: '[\s\u200b\-_,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
|
||||
# - macros: {}
|
||||
# - meta: {}
|
||||
# - git-latest-tag: {}
|
||||
# - git-authors: {}
|
||||
# - git-latest-release: {}
|
||||
# - meta: {}
|
||||
|
||||
# Soon this will all be minified (It's a personal WIP that I already have stashed in git)
|
||||
extra_css:
|
||||
- stylesheets/extra.css
|
||||
- stylesheets/hacker.css
|
||||
- stylesheets/hacker-extra.css
|
||||
- stylesheets/going-dark.css
|
||||
- stylesheets/navigation.css
|
||||
- stylesheets/footer.css
|
||||
- stylesheets/accessibility.css
|
||||
# print-light.css is NOT included here - it's only injected during PDF builds via build_guide_pdf.py
|
||||
|
||||
# extra_css:
|
||||
# - stylesheets/hacker-core.css
|
||||
# - stylesheets/components.css
|
||||
# - stylesheets/accessibility.css
|
||||
# - stylesheets/print-and-utilities.css
|
||||
|
||||
copyright: >-
|
||||
© 2023-2026 Anonymous Planet & contributors —
|
||||
licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="license">CC BY-SA 4.0</a>.
|
||||
|
||||
extra:
|
||||
social:
|
||||
- icon: simple/mastodon
|
||||
link: https://mastodon.social/@anonymousplanet
|
||||
link: https://ioc.exchange/@anonymousplanet
|
||||
name: Mastodon
|
||||
- icon: simple/x
|
||||
link: https://x.com/anonplanetorg
|
||||
name: X
|
||||
- icon: simple/gitlab
|
||||
link: http://wmj5kiic7b6kjplpbvwadnht2nh2qnkbnqtcv3dyvpqtz7ssbssftxid.onion/
|
||||
name: "0xacab"
|
||||
- icon: simple/gitea
|
||||
link: http://it7otdanqu7ktntxzm427cba6i53w6wlanlh23v5i3siqmos47pzhvyd.onion/anonymousplanetorg
|
||||
link: http://it7otdanqu7ktntxzm427cba6i53w6wlanlh23v5i3siqmos47pzhvyd.onion/anonypla
|
||||
name: Darktea
|
||||
- icon: simple/github
|
||||
link: https://github.com/anon-planet
|
||||
@@ -79,7 +93,7 @@ extra:
|
||||
link: https://codeberg.org/anonymousplanet
|
||||
name: Codeberg
|
||||
# - icon: simple/torbrowser
|
||||
# link: http://thgtoa3jzy3doku7hkna32htpghjijefscwvh4dyjgfydbbjkeiohgid.onion/
|
||||
# link: http://thgtoa3jzy3doku7hkna32htpghjijefscwvh4dyjgfydbbjkeiohgid.onion/ (defunct)
|
||||
# name: Hidden service
|
||||
|
||||
markdown_extensions:
|
||||
@@ -121,23 +135,17 @@ markdown_extensions:
|
||||
- tables: {}
|
||||
- footnotes: {}
|
||||
- toc:
|
||||
permalink: true
|
||||
permalink: "λ"
|
||||
toc_depth: 3
|
||||
|
||||
nav:
|
||||
- Welcome: index.md
|
||||
- About: about/index.md
|
||||
- Verify: verify/index.md
|
||||
- Guide:
|
||||
- guide/index.md
|
||||
- Code:
|
||||
- code/index.md
|
||||
- Develop: code/develop.md
|
||||
- Contribute: contribute/index.md
|
||||
- Constitution: constitution/index.md
|
||||
- Mirrors: mirrors/index.md
|
||||
- Twitter: twitter/index.md
|
||||
- Releases: changelog/index.md
|
||||
|
||||
copyright: |
|
||||
<a href="https://anonymousplanet.org/">The Hitchhiker's Guide</a> ©2023-2026 by <a href="https://psa.anonymousplanet.org/">Anonymous Planet</a> is licensed under <a href="https://creativecommons.org/licenses/by-nc/4.0/">CC BY-NC 4.0</a><img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" style="max-width: 1em;max-height:1em;margin-left: .2em;"><img src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" style="max-width: 1em;max-height:1em;margin-left: .2em;"><img src="https://mirrors.creativecommons.org/presskit/icons/nc.svg" alt="" style="max-width: 1em;max-height:1em;margin-left: .2em;"></a>
|
||||
- "Home": index.md
|
||||
- "Verify": verify/index.md
|
||||
- "Guide": guide/index.md
|
||||
- "Code": code/index.md
|
||||
- "Donations": contribute/index.md
|
||||
- "A Constitution": constitution/index.md
|
||||
- "Mirrors": mirrors/index.md
|
||||
- "Changelog": changelog/index.md
|
||||
- "Anonymous Planet": about/index.md
|
||||
- "PGP": pgp/index.md
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBGNnDKYBEADEwpJcPVDJLJHlaRtBtVVJ2p2SaNwbZKgeI2zfpiLu4rTmpxUp
|
||||
cbyW5S3mI++kGt4ljcKTzQM0+upr2hcdZi/rpwliHLOxsC32cvTy4YtPmoKdOalo
|
||||
blJ9+llDbl0lBBvnqQcqFhnDMPXQPsaewWmCpGjwCwnQpxXLWmKhTYMxoQtzzZ8U
|
||||
oagorLwASkb6+NZoha96ayDlE41KNErI51U8qiVxMR+8iN8pcJ1l3XA9bfMKBz45
|
||||
TnlaoJ391CvJUgJ9535FjifmOyWTB0OYgJptMPz+n0K5jTOE7mvoqT6a/hqbAGDp
|
||||
5i5LgSYVPfJqZsdrkQBMwO5pW9XymH7hNHPhaX6nPkDB8RLKexqso9pzLapG8WNC
|
||||
sk+jxTC77TOFh9CniGks7UZoa0pRdhA5sGD0Wjh8eWgDRqdgYEmqviuulWnJDti0
|
||||
dIQNixzh+TylEO8YNJyz49KUIr/ckapHfPI1BZWUyZZLpcvNvT/2IzcEeT3Tgmfr
|
||||
IZsk2U91kA9z+BKEx8mJ7V5KZo7ku0uVgAtQn5oyluSIptUGwYu5DqhnZAqKXZok
|
||||
S7i2NMghrPMM/Wf048VXuxO1Dx7CwP7Q1LCNhwL0jsLWtXIJVm7NtTt+1Vj/M4EH
|
||||
Fl4g0B7iK6JiZEPYEp5YGSWpyhpSTKQaOOCHHKSCIjVx6VLm+/Xbaf6/TwARAQAB
|
||||
tEtBbm9ueW1vdXMgUGxhbmV0IEVtYWlsIEVuY3J5cHRpb24vU2lnbmluZyBLZXkg
|
||||
PGNvbnRhY3RAYW5vbnltb3VzcGxhbmV0Lm9yZz6JAk4EEwEKADgWIQS20XV2MqKA
|
||||
+Z8ty/25q52Tr/BbnAUCY2cMpgIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAK
|
||||
CRC5q52Tr/BbnOA7EACeevkcNYbacvNJx+E8cAHyVRS7kuSWDfV0EvCeiCsZ3+sq
|
||||
q9CFADbBn4wXuELdFcPSME5UGOMpZ3MVwWocHyRrU+axseO/OCgbl15nxgk3lsSK
|
||||
Tew/1YHnjTIfpDkSOw5kT86yxea9/bpIWVzb1aCkKxVogr1cXzvBdYRWV5qC3BP4
|
||||
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
|
||||
J0nsuzwoKTRfhIDlYN7e
|
||||
=Mkw3
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -1,14 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEY2cLxRYJKwYBBAHaRw8BAQdA1wWVN04/7B2thXG3Ppm9nj9BXOosgFUCq+6m
|
||||
7q7jDUG0QUFub255bW91cyBQbGFuZXQgTWFzdGVyIFNpZ25pbmcgS2V5IChodHRw
|
||||
czovL2Fub255bW91c3BsYW5ldC5vcmcpiJAEExYKADgWIQSeqYJ4Y58c2FPglsv/
|
||||
lFB1h6apuQUCY2cLxQIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRD/lFB1
|
||||
h6apuVvhAP0UTSY/QchH8LfHaw1inGaViik9rALbjdBeVRWofwyRSQD8DH2LRX3v
|
||||
f/DgBOK7Li6OL05s9wsEYwoF+8B1qWJinQu4OARjZwvFEgorBgEEAZdVAQUBAQdA
|
||||
xO3KbSonM28D2uTNHpXFRneFL3LqUO+8JW14eULOdxoDAQgHiHgEGBYKACAWIQSe
|
||||
qYJ4Y58c2FPglsv/lFB1h6apuQUCY2cLxQIbDAAKCRD/lFB1h6apuZ32AQCiiR0d
|
||||
bD29xEmQYf4b9F77jAdFFr2DoEGjeZBPoTrJywEA8m1dD5ZOS0qn1Yz3WkTgBflL
|
||||
/0VkU6m06r/KxLL4fg0=
|
||||
=4NMF
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -1,16 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEY2cNGBYJKwYBBAHaRw8BAQdAbKn/ExAQ+aq6/o2yc04B9jx5PMloaxux1eoT
|
||||
iKwQgX60JEFub255bW91cyBQbGFuZXQgUmVsZWFzZSBTaWduaW5nIEtleYiQBBMW
|
||||
CgA4FiEEg6bPnvV6wltcf10pKF5gSKEjIbIFAmNnDRgCGwMFCwkIBwMFFQoJCAsF
|
||||
FgIDAQACHgECF4AACgkQKF5gSKEjIbI5+QD/YSQ5E+LW4YJEAQQ+D3LFsGtGGRf3
|
||||
qQRD5plsUvTtBfsA/15EJaIjzSwrsf/3wsW48zSYKCer/nrhGY9y5yd0m2gBiHUE
|
||||
EBYKAB0WIQSeqYJ4Y58c2FPglsv/lFB1h6apuQUCY2cNxAAKCRD/lFB1h6apuXun
|
||||
AQCSNwZBNybUZzN/K4Zl1j6uhCqqnvbUlO80wvbHDMXpywD/dpabqjmpfxfJC20n
|
||||
t3OFxKSeIbfJ0VHvoHKpwcaGuwC4OARjZw0YEgorBgEEAZdVAQUBAQdAE7WMDHTx
|
||||
zWp542lXGLxSsiE4gtMvVxkEneKmZWwzbDcDAQgHiHgEGBYKACAWIQSDps+e9XrC
|
||||
W1x/XSkoXmBIoSMhsgUCY2cNGAIbDAAKCRAoXmBIoSMhsowLAP42HbiJIsIodWwn
|
||||
C3yBzwGrd1xRtf/91MpQUgFpCx7xuAD9G0F3l04hKkjxiHK+wJ27LnYcigaTVdje
|
||||
6d7bt7TerwE=
|
||||
=Hgos
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -0,0 +1,89 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEZc0QYxYJKwYBBAHaRw8BAQdAm8mOR8/0qWrm9Tqzfl9Ks5rjtIbQZLAR/qxH
|
||||
HVGJsxi0LUFub255bW91cyBQbGFuZXQgRW1haWwgRW5jcnlwdGlvbi9TaWduaW5n
|
||||
IEtleYiTBBMWCgA7FiEE/L0sq979H7ounnWRoags0t0s+JAFAmXNEGMCGwMFCwkI
|
||||
BwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQoags0t0s+JDbRAEAuZlBmMGgZ3bh
|
||||
12Js9jjDcu+jhKqL4fJrJG5z9+KFkQwA/An1StA6EhcM7qlzZ5bzm2SZAbP9hQRZ
|
||||
GmfaeU2P5KgHiHgEMBYKACAWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaiCYTgId
|
||||
AAAKCRAX7KBfdo3t9gNUAP9/SyGBYJ7s9YeqLHOJ+veQZjZYHvFGQ7yPn0Fetx0Z
|
||||
LAD/UOQ8rP2QaldCMyVSG8SqfPd7n++SEAXWAl2gAo9mhg6IdQQQFgoAHRYhBJ+l
|
||||
Q20O42CYUVc4JRfsoF92je32BQJmpEUQAAoJEBfsoF92je32tD8A/ir9hE8UjrJE
|
||||
psG+PNfxYAwAagKUGbAMDUxQp3z+t81+AP45hYT4aR89zSQaankHLs3Lh7Cp5ael
|
||||
NBe/BtfR9hCLAYkCMwQQAQgAHRYhBF7WeRgs0hkTAMDm6kyyELegkVLWBQJqIJiM
|
||||
AAoJEEyyELegkVLWjZYP/j1k5vl+r0NDQXmE8hS9IKhaQPggP72iXc5RWeMQHuIv
|
||||
b1laQZm64xerJNdAh0uk1bwfmJnVGfyxBUrlCgAIeVGRSlni2Rig4azaQ1IS0pqF
|
||||
4sC1KzKEhEaNdkh3pJyGtP1cikcSjWeU2oYQou3/7VN3vNyW+n8OAVF+2fsC5d78
|
||||
EvdpZgal+komb+J8Bt552uDbCCVI4TFIPBZmHWoXjaP6L+730YphbV7Aw0L5J6OO
|
||||
ob0nzHn4X0dIvGE7Phdp2e1yNRUOSRLh8B/D5OiE9k7CaeYmJNPv5qOw/R+NgrrA
|
||||
ZFnoOuwHo0D+aL9WT9q4aM/cDCEIbvhQ4l5ZhVGqZuQ9wxNCgPi3ZiZRTfk1PW4v
|
||||
uMw1xGwXBKy7jDO12xWIWWv9MiwIQLw0OxSxKbr76rgucq7e7JrWr64rItu5Wm7F
|
||||
8qxg2cwmDat6tFSRVWlEDy8oNkRMJNjdQJDu3ez9YOfJNnApAz94Of1XU7CUuYjY
|
||||
PV88BaHdUBVtANEzy0iSDCcSj6auzLfv9dBN8cOdUxlVcrPf2jjK6JR/6qe6VWNp
|
||||
wRg9VQW2fe8HJTMUt0o9qQBJUsF68KOHtIdoE4az9AyyBNKl67dKqLB9HoIItLzD
|
||||
MJRcbS2p6plCTNagwPVvgtPRChll9JP3jLPVhRL2BixYVkbHUoJxsEfscTUl6Azt
|
||||
tEtBbm9ueW1vdXMgUGxhbmV0IEVtYWlsIEVuY3J5cHRpb24vU2lnbmluZyBLZXkg
|
||||
PGFub255bW91c3BsYW5ldEBkaXNyb290Lm9yZz6IkwQTFgoAOxYhBPy9LKve/R+6
|
||||
Lp51kaGoLNLdLPiQBQJmhqXqAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheA
|
||||
AAoJEKGoLNLdLPiQJ34A/RJT9Hyj7hT/0D1BbDU6s6YzD+/x7Pyq2+9kNSI0L77W
|
||||
AQAAG+CfDrKDXJtBNKZVNFZpld3wUeoIOcAqLl7KpsVGCYh1BBAWCgAdFiEEn6VD
|
||||
bQ7jYJhRVzglF+ygX3aN7fYFAmakRRAACgkQF+ygX3aN7fbypgEAnEg0IbWnpaLj
|
||||
/4wU179vUZZu/Y0DE63GbJuZjj72hKUA/0xyzIgSvXByjoOkEwCn5w1+RPYXKw7Z
|
||||
syERsDCUAAMIiQIzBBABCAAdFiEEXtZ5GCzSGRMAwObqTLIQt6CRUtYFAmogmIwA
|
||||
CgkQTLIQt6CRUtafPQ/+LTWFU84tDZAM0Hp7bWB0dw8nP0JvNQ2WtZf0flh+r1tF
|
||||
cmVnc9szZBh+zzSpY25iK5+Waa6+l1POYSQpkS67VR0Jrv9nL94YrRhqalSRWsjW
|
||||
MQJO+Obu4LIRIqiMZLJlAd9Bg9FshYagbQDVDOI8v9mxqCzIVm3tBx1Jp57ATHgm
|
||||
sMDWn7l1BI0SkLlG49LYxVDQ6QAx4XLCQw+JzdiJs+yExa5ymYmV61evVVbDV5UF
|
||||
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
|
||||
Eztpvc4cyyGAmI42SLM/jKbqO2yWqwVoAg==
|
||||
=ww/S
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
+23
-23
@@ -1,23 +1,23 @@
|
||||
# Import
|
||||
|
||||
```bash
|
||||
$ gpg --import pgp/core-devs/*
|
||||
```
|
||||
|
||||
# Verify
|
||||
|
||||
```bash
|
||||
$ gpg --verify pgp/core-devs/than/than-crypto.txt
|
||||
|
||||
|
||||
gpg: Signature made Sat 19 Jul 2025 02:04:10 AM EDT
|
||||
gpg: using EDDSA key 8B3A74890536BAD50D9376EBF1CB32F67E3302A1
|
||||
gpg: Good signature from "nopenothinghere@proton.me <nopenothinghere@proton.me>" [ultimate]
|
||||
gpg: aka "Nope Nothing (Anonymous Planet Contact) <no@anonymousplanet.org>" [ultimate]
|
||||
gpg: aka "Nope Nothing (Systems Administrator) <admin@itsnothing.net>" [ultimate]
|
||||
Primary key fingerprint: 8B3A 7489 0536 BAD5 0D93 76EB F1CB 32F6 7E33 02A1
|
||||
```
|
||||
|
||||
## All signing keys are signed by the Master Signing Key
|
||||
|
||||
TODO
|
||||
# Import
|
||||
|
||||
```sh
|
||||
$ gpg --import pgp/core-devs/*
|
||||
```
|
||||
|
||||
# Verify
|
||||
|
||||
```sh
|
||||
$ gpg --verify pgp/core-devs/than/than-crypto.txt
|
||||
|
||||
|
||||
gpg: Signature made Sat 19 Jul 2025 02:04:10 AM EDT
|
||||
gpg: using EDDSA key 8B3A74890536BAD50D9376EBF1CB32F67E3302A1
|
||||
gpg: Good signature from "nopenothinghere@proton.me <nopenothinghere@proton.me>" [ultimate]
|
||||
gpg: aka "Nope Nothing (Anonymous Planet Contact) <no@anonymousplanet.net>" [ultimate]
|
||||
gpg: aka "Nope Nothing (Systems Administrator) <admin@itsnothing.net>" [ultimate]
|
||||
Primary key fingerprint: 8B3A 7489 0536 BAD5 0D93 76EB F1CB 32F6 7E33 02A1
|
||||
```
|
||||
|
||||
## All signing keys are signed by the Master Signing Key
|
||||
|
||||
TODO
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# scripts/archived
|
||||
|
||||
Scripts kept for reference but no longer part of the active pipeline.
|
||||
|
||||
| Script | Why archived |
|
||||
|--------|-------------|
|
||||
| `tag_release.py` | Created GPG-signed `vX.Y.Z` annotated tags. Superseded by the `release-YYYYMMDD-<sha>` timestamp tagging built into `03-release.yml`. Re-enable if semver release tagging is reintroduced. |
|
||||
@@ -38,7 +38,7 @@ from pathlib import Path
|
||||
|
||||
# Default release signing key fingerprint.
|
||||
# Maintainers with a different key can pass --key on the CLI.
|
||||
DEFAULT_SIGNING_KEY = "9FA5436D0EE360985157382517ECA05F768DEDF6"
|
||||
DEFAULT_SIGNING_KEY = "C3023DBEA3FB38C438BA1EEDCEC60AEDE8B992A2"
|
||||
|
||||
CHANGELOG = Path(__file__).resolve().parent.parent / "docs" / "changelog" / "index.md"
|
||||
|
||||
@@ -396,7 +396,7 @@ def main() -> int:
|
||||
print("\n" + "=" * 70)
|
||||
print(" ✓ All done. Push the tag with:")
|
||||
print(f"\n git push origin {version}\n")
|
||||
print(" The release.yml workflow can then be triggered manually from")
|
||||
print(" The 03-release.yml workflow can then be triggered manually from")
|
||||
print(" GitHub Actions to publish the GitHub Release for this tag.")
|
||||
print("=" * 70 + "\n")
|
||||
|
||||
+306
-46
@@ -2,7 +2,7 @@
|
||||
"""Build light-mode PDF with MkDocs + Chromium, then produce dark-mode PDF via convert.py.
|
||||
|
||||
Usage:
|
||||
python scripts/build_guide_pdf.py # Light PDF only
|
||||
python scripts/build_guide_pdf.py # Light PDF only (clean light theme)
|
||||
python scripts/build_guide_pdf.py --dark # Dark PDF only (requires light PDF to exist)
|
||||
python scripts/build_guide_pdf.py --both # Light PDF, then dark PDF
|
||||
|
||||
@@ -14,10 +14,12 @@ Examples:
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import html as _html_mod
|
||||
import os
|
||||
import shutil
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
@@ -27,37 +29,51 @@ def repo_root() -> Path:
|
||||
|
||||
|
||||
def find_chromium_executable() -> Path | None:
|
||||
"""Find a Chromium-based browser on the system (prioritizes WSL/Linux paths).
|
||||
|
||||
On WSL Windows, checks WSL tools first, then falls back to Windows paths.
|
||||
"""
|
||||
import os as _os
|
||||
import shutil
|
||||
|
||||
wsl_paths = [
|
||||
"/usr/bin/google-chrome",
|
||||
"/usr/bin/google-chrome-stable",
|
||||
"/usr/bin/chromium-browser",
|
||||
"/usr/bin/chromium",
|
||||
"/usr/bin/microsoft-edge",
|
||||
"/usr/bin/microsoft-edge-stable",
|
||||
"/usr/bin/microsoft-edge-dev",
|
||||
"/snap/bin/chromium",
|
||||
"/usr/local/bin/chrome",
|
||||
"/opt/google-chrome/",
|
||||
]
|
||||
|
||||
for p in wsl_paths:
|
||||
if os.path.isfile(p):
|
||||
return Path(p)
|
||||
|
||||
for name in ("google-chrome-stable", "google-chrome", "chromium-browser", "chromium",
|
||||
"microsoft-edge-stable", "microsoft-edge", "msedge", "chrome"):
|
||||
w = shutil.which(name)
|
||||
if w:
|
||||
return Path(w)
|
||||
|
||||
if sys.platform == "win32":
|
||||
paths = [
|
||||
Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Microsoft/Edge/Application/msedge.exe",
|
||||
Path(os.environ.get("LOCALAPPDATA", "")) / "Microsoft/Edge/Application/msedge.exe",
|
||||
Path(os.environ.get("PROGRAMFILES", "")) / "Google/Chrome/Application/chrome.exe",
|
||||
Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Google/Chrome/Application/chrome.exe",
|
||||
Path(os.environ.get("LOCALAPPDATA", "")) / "Google/Chrome/Application/chrome.exe",
|
||||
Path(_os.environ.get("PROGRAMFILES(X86)", "")) / "Microsoft/Edge/Application/msedge.exe",
|
||||
Path(_os.environ.get("LOCALAPPDATA", "")) / "Microsoft/Edge/Application/msedge.exe",
|
||||
Path(_os.environ.get("PROGRAMFILES", "")) / "Google/Chrome/Application/chrome.exe",
|
||||
]
|
||||
for p in paths:
|
||||
if p.is_file():
|
||||
return p
|
||||
for name in ("chrome", "msedge"):
|
||||
w = shutil.which(name)
|
||||
if w:
|
||||
return Path(w)
|
||||
elif sys.platform == "darwin":
|
||||
for p in (
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
):
|
||||
if os.path.isfile(p):
|
||||
return Path(p)
|
||||
for name in ("google-chrome-stable", "google-chrome", "chromium-browser", "chromium", "chrome"):
|
||||
w = shutil.which(name)
|
||||
if w:
|
||||
return Path(w)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def run_mkdocs(site_dir: Path) -> None:
|
||||
"""Build MkDocs site."""
|
||||
site_dir.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "mkdocs", "build", "-d", str(site_dir)],
|
||||
@@ -66,29 +82,56 @@ def run_mkdocs(site_dir: Path) -> None:
|
||||
)
|
||||
|
||||
|
||||
def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path) -> Path:
|
||||
"""Render html_file to pdf_out via headless Chromium.
|
||||
def inject_print_light_css(html_file: Path) -> Path:
|
||||
"""Inject print-light.css inline into the built HTML for light PDF builds."""
|
||||
html_path = html_file.resolve()
|
||||
print_light_css = repo_root() / "docs" / "stylesheets" / "print-light.css"
|
||||
|
||||
Uses a temp file first so an open guide.pdf on Windows does not block the
|
||||
build; if the final path is still locked, writes guide-new.pdf instead.
|
||||
"""
|
||||
if not print_light_css.is_file():
|
||||
print(f"Warning: {print_light_css} not found, skipping CSS injection.", file=sys.stderr)
|
||||
return html_path
|
||||
|
||||
css_content = print_light_css.read_text(encoding="utf-8")
|
||||
html_content = html_path.read_text(encoding="utf-8")
|
||||
|
||||
head_match = re.search(r"<head(?:[^>]*)>", html_content, re.IGNORECASE)
|
||||
if not head_match:
|
||||
print("Warning: Could not find <head> tag in HTML, skipping CSS injection.", file=sys.stderr)
|
||||
return html_path
|
||||
|
||||
insert_pos = head_match.end()
|
||||
style_block = (
|
||||
"\n<!-- Light PDF theme: injected inline so file:// URI resolves correctly -->\n"
|
||||
"<style>\n"
|
||||
+ css_content
|
||||
+ "\n</style>\n"
|
||||
)
|
||||
|
||||
new_html = html_content[:insert_pos] + style_block + html_content[insert_pos:]
|
||||
|
||||
output_file = html_path.with_stem(html_path.stem + "-light-pdf")
|
||||
output_file.write_text(new_html, encoding="utf-8")
|
||||
return output_file
|
||||
|
||||
|
||||
def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path) -> Path:
|
||||
"""Render html_file to pdf_out via headless Chromium."""
|
||||
pdf_out.parent.mkdir(parents=True, exist_ok=True)
|
||||
partial = pdf_out.parent / f".{pdf_out.name}.writing"
|
||||
partial.unlink(missing_ok=True)
|
||||
|
||||
html_file = inject_print_light_css(html_file)
|
||||
uri = html_file.resolve().as_uri()
|
||||
|
||||
cmd = [str(browser)]
|
||||
if os.environ.get("CI"):
|
||||
cmd += [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
]
|
||||
cmd += ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]
|
||||
cmd += [
|
||||
"--headless=new",
|
||||
"--disable-gpu",
|
||||
"--no-pdf-header-footer",
|
||||
"--print-background",
|
||||
"--no-margins",
|
||||
f"--print-to-pdf={partial.resolve()}",
|
||||
uri,
|
||||
]
|
||||
@@ -116,11 +159,228 @@ def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path) -> Path:
|
||||
partial.replace(pdf_out)
|
||||
return pdf_out
|
||||
|
||||
# Use scripts/convert.py in place of broken dark-mode hack
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Front matter: ToC parsing + HTML generation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def parse_toc_headings(guide_md: Path) -> list[tuple[int, str]]:
|
||||
"""Extract H2 and H3 headings from the guide markdown source.
|
||||
|
||||
Returns a list of (level, title) tuples in document order.
|
||||
Strips MkDocs anchor suffixes like { #some-id } and common inline markup.
|
||||
Skips headings inside fenced code blocks.
|
||||
"""
|
||||
headings: list[tuple[int, str]] = []
|
||||
in_fence = False
|
||||
fence_re = re.compile(r"^\s*```")
|
||||
heading_re = re.compile(r"^(#{2,3})\s+(.+?)(?:\s*\{[^}]*\})?\s*$")
|
||||
inline_re = re.compile(
|
||||
r"\*{1,2}([^*]+)\*{1,2}" # **bold** / *italic*
|
||||
r"|`[^`]+`" # `code`
|
||||
r"|\[([^\]]+)\]\([^)]*\)" # [text](url)
|
||||
)
|
||||
|
||||
for line in guide_md.read_text(encoding="utf-8").splitlines():
|
||||
if fence_re.match(line):
|
||||
in_fence = not in_fence
|
||||
continue
|
||||
if in_fence:
|
||||
continue
|
||||
m = heading_re.match(line)
|
||||
if m:
|
||||
level = len(m.group(1))
|
||||
raw = m.group(2).strip()
|
||||
title = inline_re.sub(
|
||||
lambda x: x.group(1) or x.group(2) or "", raw
|
||||
).strip()
|
||||
headings.append((level, title))
|
||||
return headings
|
||||
|
||||
|
||||
def create_toc_html(tmp_dir: str, headings: list[tuple[int, str]], is_dark: bool) -> str:
|
||||
"""Write a research-style Table of Contents HTML file and return its path.
|
||||
|
||||
H2 entries: full-width row with dot leader.
|
||||
H3 entries: indented, italic, muted colour.
|
||||
Layout matches the cover page (A4, same font stack, no Chromium margins).
|
||||
"""
|
||||
if is_dark:
|
||||
bg = "#1f1f31"
|
||||
fg = "#e0e0e0"
|
||||
muted = "#a0a0c0"
|
||||
rule = "#4a4a6a"
|
||||
h3color = "#b8b8d8"
|
||||
else:
|
||||
bg = "#ffffff"
|
||||
fg = "#1a1a1a"
|
||||
muted = "#555555"
|
||||
rule = "#cccccc"
|
||||
h3color = "#444466"
|
||||
|
||||
rows: list[str] = []
|
||||
for level, title in headings:
|
||||
safe = _html_mod.escape(title)
|
||||
if level == 2:
|
||||
rows.append(
|
||||
f'<div class="toc-h2">'
|
||||
f'<span class="toc-title">{safe}</span>'
|
||||
f'<span class="toc-dots"></span>'
|
||||
f"</div>"
|
||||
)
|
||||
else:
|
||||
rows.append(f'<div class="toc-h3">{safe}</div>')
|
||||
|
||||
body = "\n".join(rows)
|
||||
|
||||
css = (
|
||||
"* { box-sizing: border-box; margin: 0; padding: 0; }\n"
|
||||
"@page { size: A4; margin: 0; }\n"
|
||||
"html, body {\n"
|
||||
f" width: 210mm;\n"
|
||||
f" background: {bg};\n"
|
||||
f" color: {fg};\n"
|
||||
" font-family: 'EB Garamond', Georgia, 'Times New Roman', serif;\n"
|
||||
"}\n"
|
||||
".page {\n"
|
||||
" width: 210mm;\n"
|
||||
" min-height: 297mm;\n"
|
||||
" padding: 25mm 28mm 28mm 28mm;\n"
|
||||
"}\n"
|
||||
".toc-heading {\n"
|
||||
" font-size: 18pt;\n"
|
||||
" font-weight: normal;\n"
|
||||
" letter-spacing: 0.04em;\n"
|
||||
" margin-bottom: 8mm;\n"
|
||||
" padding-bottom: 3mm;\n"
|
||||
f" border-bottom: 1px solid {rule};\n"
|
||||
f" color: {fg};\n"
|
||||
"}\n"
|
||||
".toc-h2 {\n"
|
||||
" display: flex;\n"
|
||||
" align-items: baseline;\n"
|
||||
" font-size: 10.5pt;\n"
|
||||
" font-weight: normal;\n"
|
||||
f" color: {fg};\n"
|
||||
" margin-top: 3.5pt;\n"
|
||||
" margin-bottom: 1.5pt;\n"
|
||||
"}\n"
|
||||
".toc-title {\n"
|
||||
" white-space: nowrap;\n"
|
||||
" overflow: hidden;\n"
|
||||
" flex-shrink: 0;\n"
|
||||
" max-width: 85%;\n"
|
||||
"}\n"
|
||||
".toc-dots {\n"
|
||||
" flex: 1;\n"
|
||||
f" border-bottom: 1px dotted {muted};\n"
|
||||
" margin: 0 4pt;\n"
|
||||
" position: relative;\n"
|
||||
" top: -2pt;\n"
|
||||
" min-width: 8pt;\n"
|
||||
"}\n"
|
||||
".toc-h3 {\n"
|
||||
" font-size: 9pt;\n"
|
||||
f" color: {h3color};\n"
|
||||
" padding-left: 10mm;\n"
|
||||
" margin-top: 1pt;\n"
|
||||
" margin-bottom: 1pt;\n"
|
||||
" font-style: italic;\n"
|
||||
"}\n"
|
||||
)
|
||||
|
||||
html = (
|
||||
'<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n'
|
||||
f"<style>\n{css}</style>\n</head>\n<body>\n"
|
||||
'<div class="page">\n'
|
||||
' <p class="toc-heading">Table of Contents</p>\n'
|
||||
f"{body}\n"
|
||||
"</div>\n</body>\n</html>\n"
|
||||
)
|
||||
|
||||
html_path = os.path.join(tmp_dir, "toc.html")
|
||||
with open(html_path, "w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
return html_path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared Chromium render helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _render_html_to_pdf(
|
||||
browser: Path, html_path: str, pdf_path: str, ci: bool = False
|
||||
) -> None:
|
||||
"""Render a local HTML file to PDF via headless Chromium."""
|
||||
cmd = [
|
||||
str(browser),
|
||||
"--headless=new",
|
||||
"--disable-gpu",
|
||||
"--no-pdf-header-footer",
|
||||
"--print-background",
|
||||
"--no-margins",
|
||||
f"--print-to-pdf={pdf_path}",
|
||||
Path(html_path).resolve().as_uri(),
|
||||
]
|
||||
if ci:
|
||||
cmd[1:1] = ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]
|
||||
subprocess.run(cmd, check=True, capture_output=True, timeout=120)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Front matter assembly: cover + ToC prepended to body PDF
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def prepend_front_matter(browser: Path, pdf_path: Path, is_dark: bool) -> Path:
|
||||
"""Render cover + ToC and prepend them to pdf_path in-place.
|
||||
|
||||
- Cover HTML is sourced from convert.py (single definition).
|
||||
- ToC headings are parsed from docs/guide/index.md.
|
||||
- Merged order: cover -> toc -> body, via a single qpdf call.
|
||||
"""
|
||||
sys.path.insert(0, str(repo_root() / "scripts"))
|
||||
from convert import create_cover_page # noqa: PLC0415
|
||||
|
||||
guide_md = repo_root() / "docs" / "guide" / "index.md"
|
||||
ci = bool(os.environ.get("CI"))
|
||||
|
||||
print(" Parsing ToC headings...", flush=True)
|
||||
headings = parse_toc_headings(guide_md)
|
||||
n_h2 = sum(1 for l, _ in headings if l == 2)
|
||||
n_h3 = sum(1 for l, _ in headings if l == 3)
|
||||
print(f" Found {len(headings)} headings ({n_h2} H2, {n_h3} H3).", flush=True)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
print(" Rendering cover page...", flush=True)
|
||||
cover_html = create_cover_page(tmp, is_dark=is_dark)
|
||||
cover_pdf = os.path.join(tmp, "cover.pdf")
|
||||
_render_html_to_pdf(browser, cover_html, cover_pdf, ci=ci)
|
||||
|
||||
print(" Rendering ToC page...", flush=True)
|
||||
toc_html = create_toc_html(tmp, headings, is_dark=is_dark)
|
||||
toc_pdf = os.path.join(tmp, "toc.pdf")
|
||||
_render_html_to_pdf(browser, toc_html, toc_pdf, ci=ci)
|
||||
|
||||
merged = str(pdf_path.parent / f".{pdf_path.name}.with-front")
|
||||
subprocess.run(
|
||||
["qpdf", "--empty", "--pages",
|
||||
cover_pdf, toc_pdf, str(pdf_path),
|
||||
"--", merged],
|
||||
check=True,
|
||||
)
|
||||
|
||||
Path(merged).replace(pdf_path)
|
||||
return pdf_path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dark PDF build (delegates to convert.py which adds its own front matter)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_dark_pdf(light_pdf: Path, dark_pdf: Path) -> Path:
|
||||
"""Convert the light PDF to dark mode using scripts/convert.py."""
|
||||
convert_script = repo_root() / "scripts" / "convert.py"
|
||||
print(f"Converting {light_pdf.name} → {dark_pdf.name} (dark mode)…")
|
||||
print(f"Converting {light_pdf.name} -> {dark_pdf.name} (dark mode)...")
|
||||
subprocess.run(
|
||||
[sys.executable, str(convert_script), str(light_pdf), str(dark_pdf)],
|
||||
check=True,
|
||||
@@ -128,6 +388,10 @@ def build_dark_pdf(light_pdf: Path, dark_pdf: Path) -> Path:
|
||||
return dark_pdf
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
ap = argparse.ArgumentParser(
|
||||
@@ -158,15 +422,16 @@ def main() -> int:
|
||||
)
|
||||
|
||||
mode = ap.add_mutually_exclusive_group()
|
||||
mode.add_argument("--dark", action="store_true", help="Dark PDF only (light PDF must already exist)")
|
||||
mode.add_argument("--both", action="store_true", help="Build light PDF, then dark PDF")
|
||||
# default (no flag) = light only
|
||||
mode.add_argument("--dark", action="store_true",
|
||||
help="Dark PDF only (light PDF must already exist)")
|
||||
mode.add_argument("--both", action="store_true",
|
||||
help="Build light PDF, then dark PDF")
|
||||
args = ap.parse_args()
|
||||
|
||||
build_light = not args.dark
|
||||
build_dark = args.dark or args.both
|
||||
|
||||
# --- Light PDF (Chromium) ---
|
||||
# --- Light PDF ---
|
||||
if build_light:
|
||||
guide_html = args.site_dir / "guide" / "index.html"
|
||||
|
||||
@@ -187,16 +452,11 @@ def main() -> int:
|
||||
return 1
|
||||
|
||||
out_light = print_to_pdf(browser, guide_html, args.pdf_light)
|
||||
out_light = prepend_front_matter(browser, out_light, is_dark=False)
|
||||
size_kb = out_light.stat().st_size // 1024
|
||||
print(f"Wrote {out_light.resolve()} ({size_kb} KiB) [Light Mode]")
|
||||
if out_light.resolve() != args.pdf_light.resolve():
|
||||
print(
|
||||
f"Note: {args.pdf_light.name} was in use; "
|
||||
"close it and rename or replace with the file above.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
# --- Dark PDF (pixel converter) ---
|
||||
# --- Dark PDF ---
|
||||
if build_dark:
|
||||
if not args.pdf_light.exists():
|
||||
print(
|
||||
|
||||
+303
-41
@@ -15,6 +15,8 @@ Usage:
|
||||
Examples:
|
||||
python scripts/convert.py export/thgtoa.pdf export/thgtoa-dark.pdf
|
||||
python scripts/convert.py export/thgtoa.pdf --dpi 150 --bg 0d1117
|
||||
|
||||
Note: Adds a cover page at the start with title/subtitle/version info.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -54,61 +56,98 @@ def apply_dark_theme(
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Remap a white-background page image to a dark theme.
|
||||
- Near-white pixels → bg color
|
||||
- Dark pixels (ink/text) → text color
|
||||
- Blue-ish pixels → link color
|
||||
|
||||
Strategy:
|
||||
- Near-white pixels (page background) → bg color
|
||||
- Dark, low-saturation pixels (text) → text color
|
||||
- Blue-dominant dark pixels (links) → link color
|
||||
- High-saturation pixels (photos/images) → preserved exactly as-is
|
||||
(no dark remapping; images stay at natural colors)
|
||||
|
||||
The key fix vs. the old version: output is initialized from the *original*
|
||||
pixels, so any pixel that doesn't match a remap mask keeps its source color.
|
||||
This prevents the "black blobs" bug where image regions fell through to the
|
||||
zero-initialized buffer.
|
||||
"""
|
||||
arr = np.array(img.convert('RGB'), dtype=np.float32)
|
||||
orig = arr.copy()
|
||||
norm = arr / 255.0
|
||||
|
||||
lightness = (
|
||||
0.299 * norm[:, :, 0]
|
||||
+ 0.587 * norm[:, :, 1]
|
||||
+ 0.114 * norm[:, :, 2]
|
||||
# Luminance (Rec. 601)
|
||||
lum = (
|
||||
0.299 * norm[:, :, 0] +
|
||||
0.587 * norm[:, :, 1] +
|
||||
0.114 * norm[:, :, 2]
|
||||
)
|
||||
|
||||
r, g, b = orig[:, :, 0], orig[:, :, 1], orig[:, :, 2]
|
||||
link_mask = (
|
||||
(b > 100)
|
||||
& (b > r * 1.3)
|
||||
& (b > g * 0.9)
|
||||
& (lightness < 0.85)
|
||||
# Saturation (HSV model, vectorised)
|
||||
ch_min = np.min(norm, axis=2)
|
||||
ch_max = np.max(norm, axis=2)
|
||||
sat = np.where(ch_max > 0.001, (ch_max - ch_min) / ch_max, 0.0).astype(np.float32)
|
||||
|
||||
# --- Masks ---
|
||||
# High-saturation = image/photo content — leave untouched
|
||||
is_image = sat > 0.20
|
||||
|
||||
# Near-white page background
|
||||
is_bg = (lum > 0.88) & ~is_image
|
||||
|
||||
# Blue-ish hyperlinks: blue channel dominant, dark, not an image
|
||||
is_link = (
|
||||
~is_image &
|
||||
~is_bg &
|
||||
(norm[:, :, 2] > 0.30) &
|
||||
(norm[:, :, 2] > norm[:, :, 0] * 1.20) &
|
||||
(lum < 0.75)
|
||||
)
|
||||
content_mask = (lightness < 0.85) & ~link_mask
|
||||
blend = ((1.0 - lightness) / 0.85).clip(0, 1)
|
||||
|
||||
bg_f = [c / 255.0 for c in bg]
|
||||
text_f = [c / 255.0 for c in text]
|
||||
link_f = [c / 255.0 for c in link]
|
||||
# Dark ink (text, borders, rules): not image, not bg, not link
|
||||
is_text = ~is_image & ~is_bg & ~is_link & (lum < 0.85)
|
||||
|
||||
out = np.zeros_like(norm)
|
||||
for i, (b_c, t, lc) in enumerate(zip(bg_f, text_f, link_f)):
|
||||
channel = np.full(lightness.shape, b_c)
|
||||
channel = np.where(content_mask, b_c + blend * (t - b_c), channel)
|
||||
channel = np.where(link_mask, b_c + blend * (lc - b_c), channel)
|
||||
out[:, :, i] = channel
|
||||
# --- Build output from original pixels ---
|
||||
# Start from a copy so anything not matched keeps source color.
|
||||
out = arr.copy()
|
||||
|
||||
return Image.fromarray((out * 255).clip(0, 255).astype('uint8'))
|
||||
bg_f = np.array(bg, dtype=np.float32)
|
||||
text_f = np.array(text, dtype=np.float32)
|
||||
link_f = np.array(link, dtype=np.float32)
|
||||
|
||||
# Full-strength remap for text: anything that was dark ink becomes text_color
|
||||
# at full brightness. No partial blend — partial blends leave mid-gray elements
|
||||
# (captions, borders, muted labels) at unreadable intermediate values.
|
||||
bg_mask3 = is_bg[..., np.newaxis]
|
||||
text_mask3 = is_text[..., np.newaxis]
|
||||
link_mask3 = is_link[..., np.newaxis]
|
||||
|
||||
out = np.where(bg_mask3, bg_f, out)
|
||||
out = np.where(text_mask3, text_f, out)
|
||||
out = np.where(link_mask3, link_f, out)
|
||||
|
||||
return Image.fromarray(out.clip(0, 255).astype(np.uint8))
|
||||
|
||||
|
||||
def _save_images_as_pdf(images: list, output_path: str) -> None:
|
||||
"""Save a list of RGB PIL images as a PDF using PNG compression via qpdf.
|
||||
"""Save a list of RGB PIL images as a PDF without requiring libjpeg.
|
||||
|
||||
Pillow's built-in PDF writer defaults to JPEG encoding for RGB images,
|
||||
which fails when libjpeg is not available in the environment. Instead we
|
||||
write each page as a lossless PNG to a temp directory and assemble them
|
||||
with qpdf, which embeds the PNGs directly without re-encoding.
|
||||
Pillow's PDF writer defaults to JPEG encoding for RGB images, which
|
||||
fails when libjpeg is absent in the environment. Fix: quantize each
|
||||
image to palette mode (256 colours, FASTOCTREE) so Pillow uses
|
||||
zlib/deflate instead of JPEG, save each as an individual single-page
|
||||
PDF, then merge the page PDFs with qpdf.
|
||||
|
||||
Colour fidelity is preserved — the hacker theme uses only a handful
|
||||
of distinct colours so 256-colour quantization is visually lossless.
|
||||
"""
|
||||
import tempfile as _tempfile
|
||||
with _tempfile.TemporaryDirectory() as staging:
|
||||
png_paths = []
|
||||
page_pdfs = []
|
||||
for i, img in enumerate(images):
|
||||
p = os.path.join(staging, f'p{i:05d}.png')
|
||||
img.save(p, format='PNG')
|
||||
png_paths.append(p)
|
||||
page_path = os.path.join(staging, f'p{i:05d}.pdf')
|
||||
img.quantize(colors=256, method=Image.Quantize.FASTOCTREE).save(
|
||||
page_path, format='PDF'
|
||||
)
|
||||
page_pdfs.append(page_path)
|
||||
subprocess.run(
|
||||
['qpdf', '--empty', '--pages'] + png_paths + ['--', output_path],
|
||||
['qpdf', '--empty', '--pages'] + page_pdfs + ['--', output_path],
|
||||
check=True,
|
||||
)
|
||||
|
||||
@@ -119,6 +158,125 @@ def _check_qpdf() -> bool:
|
||||
).returncode == 0
|
||||
|
||||
|
||||
def _check_dependencies() -> None:
|
||||
"""Verify required system tools are available before doing any work."""
|
||||
missing = []
|
||||
for tool in ('pdftoppm', 'qpdf'):
|
||||
if subprocess.run(['which', tool], capture_output=True).returncode != 0:
|
||||
missing.append(tool)
|
||||
if missing:
|
||||
tools = ', '.join(missing)
|
||||
instructions = (
|
||||
f"Install with:\n"
|
||||
f" Linux/WSL: sudo apt install poppler-utils qpdf\n"
|
||||
f" macOS: brew install poppler qpdf\n"
|
||||
f" Windows: see docs/code/develop.md"
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"Missing required system tool(s): {tools}\n{instructions}"
|
||||
)
|
||||
|
||||
|
||||
def create_cover_page(tmp_dir: str, is_dark: bool) -> str:
|
||||
"""Create a research-style text-only cover page for Chromium rendering."""
|
||||
if is_dark:
|
||||
bg_color = '#1f1f31'
|
||||
text_color = '#e0e0e0'
|
||||
rule_color = '#4a4a6a'
|
||||
meta_color = '#a0a0c0'
|
||||
else:
|
||||
bg_color = '#ffffff'
|
||||
text_color = '#1a1a1a'
|
||||
rule_color = '#cccccc'
|
||||
meta_color = '#555555'
|
||||
|
||||
html_path = os.path.join(tmp_dir, 'cover.html')
|
||||
with open(html_path, 'w', encoding='utf-8') as f:
|
||||
f.write(f"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* {{
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}}
|
||||
@page {{
|
||||
size: A4;
|
||||
margin: 0;
|
||||
}}
|
||||
html, body {{
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
background: {bg_color};
|
||||
color: {text_color};
|
||||
font-family: 'EB Garamond', Georgia, 'Times New Roman', serif;
|
||||
}}
|
||||
.page {{
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 30mm 25mm;
|
||||
text-align: center;
|
||||
}}
|
||||
.title {{
|
||||
font-size: 28pt;
|
||||
font-weight: normal;
|
||||
line-height: 1.25;
|
||||
letter-spacing: 0.01em;
|
||||
margin-bottom: 10mm;
|
||||
}}
|
||||
.rule {{
|
||||
width: 80mm;
|
||||
height: 1px;
|
||||
background: {rule_color};
|
||||
margin: 0 auto 10mm auto;
|
||||
}}
|
||||
.subtitle {{
|
||||
font-size: 13pt;
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
color: {meta_color};
|
||||
margin-bottom: 14mm;
|
||||
}}
|
||||
.meta {{
|
||||
font-size: 11pt;
|
||||
color: {meta_color};
|
||||
line-height: 1.9;
|
||||
}}
|
||||
.meta strong {{
|
||||
color: {text_color};
|
||||
font-weight: normal;
|
||||
}}
|
||||
.version {{
|
||||
font-size: 11pt;
|
||||
color: {meta_color};
|
||||
margin-top: 12mm;
|
||||
letter-spacing: 0.05em;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<p class="title">The Hitchhiker’s Guide<br>to Online Anonymity</p>
|
||||
<div class="rule"></div>
|
||||
<p class="subtitle">The comprehensive guide for online anonymity and OpSec.</p>
|
||||
<div class="meta">
|
||||
<p><strong>Author</strong> Anonymous Planet</p>
|
||||
<p><strong>License</strong> Creative Commons BY-SA 4.0</p>
|
||||
<p><strong>Source</strong> https://anonymousplanet.net</p>
|
||||
</div>
|
||||
<p class="version">v1.2.5 — June 2026</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>""")
|
||||
return html_path
|
||||
|
||||
|
||||
def convert_pdf_to_dark(
|
||||
input_path: str | Path,
|
||||
output_path: str | Path,
|
||||
@@ -134,10 +292,14 @@ def convert_pdf_to_dark(
|
||||
For large documents, pages are processed in batches of `batch_size` to
|
||||
avoid OOM, then merged with qpdf. Falls back to single-pass Pillow save
|
||||
if qpdf is not available (fine for small documents).
|
||||
|
||||
Adds a cover page at the start with title/subtitle/version info.
|
||||
"""
|
||||
input_path = str(input_path)
|
||||
output_path = str(output_path)
|
||||
|
||||
_check_dependencies()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
# 1. Rasterize all pages
|
||||
prefix = os.path.join(tmp, 'page')
|
||||
@@ -164,7 +326,51 @@ def convert_pdf_to_dark(
|
||||
if out_dir:
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
# 2. Process in batches
|
||||
# Determine theme from filename for cover page
|
||||
pdf_output = Path(output_path)
|
||||
pdf_name = pdf_output.stem.lower()
|
||||
is_dark = 'dark' in pdf_name
|
||||
|
||||
print(f" Theme: {'Going dark' if is_dark else 'Light mode'}")
|
||||
|
||||
# 2. Build front matter: cover + ToC
|
||||
# Import ToC helpers from build_guide_pdf.py (single source of truth).
|
||||
scripts_dir = str(Path(__file__).resolve().parent)
|
||||
if scripts_dir not in sys.path:
|
||||
sys.path.insert(0, scripts_dir)
|
||||
from build_guide_pdf import parse_toc_headings, create_toc_html # noqa: PLC0415
|
||||
|
||||
cover_html_path = create_cover_page(tmp, is_dark)
|
||||
browser = find_chromium_executable()
|
||||
|
||||
if not browser:
|
||||
raise RuntimeError(
|
||||
"No Chromium-based browser found; needed for cover page rendering.\n"
|
||||
"Install Chrome, Edge, or add Chromium to PATH."
|
||||
)
|
||||
|
||||
cover_pdf = os.path.join(tmp, 'cover.pdf')
|
||||
cmd = [str(browser), "--headless=new", "--disable-gpu",
|
||||
"--no-pdf-header-footer", "--print-background", "--no-margins",
|
||||
f"--print-to-pdf={cover_pdf}", cover_html_path]
|
||||
subprocess.run(cmd, check=True, capture_output=True)
|
||||
|
||||
guide_md = Path(__file__).resolve().parent.parent / 'docs' / 'guide' / 'index.md'
|
||||
if guide_md.is_file():
|
||||
print(" Building ToC...", flush=True)
|
||||
headings = parse_toc_headings(guide_md)
|
||||
toc_html_path = create_toc_html(tmp, headings, is_dark)
|
||||
toc_pdf = os.path.join(tmp, 'toc.pdf')
|
||||
cmd_toc = [str(browser), "--headless=new", "--disable-gpu",
|
||||
"--no-pdf-header-footer", "--print-background", "--no-margins",
|
||||
f"--print-to-pdf={toc_pdf}", toc_html_path]
|
||||
subprocess.run(cmd_toc, check=True, capture_output=True)
|
||||
front_pages = [cover_pdf, toc_pdf]
|
||||
else:
|
||||
print(" Warning: guide/index.md not found, skipping ToC.", file=sys.stderr)
|
||||
front_pages = [cover_pdf]
|
||||
|
||||
# 3. Process pages with theme remapping
|
||||
use_batches = total > batch_size and _check_qpdf()
|
||||
|
||||
if use_batches:
|
||||
@@ -186,12 +392,11 @@ def convert_pdf_to_dark(
|
||||
dark = [apply_dark_theme(Image.open(p), bg, text, link) for p in batch]
|
||||
_save_images_as_pdf(dark, batch_path)
|
||||
batch_files.append(batch_path)
|
||||
del dark
|
||||
|
||||
# 3. Merge batches with qpdf
|
||||
print(" Merging batches…", flush=True)
|
||||
# Merge batches with front matter using qpdf
|
||||
print(" Merging batches and front matter...", flush=True)
|
||||
subprocess.run(
|
||||
['qpdf', '--empty', '--pages'] + batch_files + ['--', output_path],
|
||||
['qpdf', '--empty', '--pages'] + front_pages + batch_files + ['--', output_path],
|
||||
check=True,
|
||||
)
|
||||
|
||||
@@ -205,10 +410,67 @@ def convert_pdf_to_dark(
|
||||
|
||||
_save_images_as_pdf(dark_pages, output_path)
|
||||
|
||||
# Prepend front matter to the single-pass output
|
||||
if not use_batches:
|
||||
tmp_body = os.path.join(tmp, 'body_only.pdf')
|
||||
os.rename(output_path, tmp_body)
|
||||
subprocess.run(
|
||||
['qpdf', '--empty', '--pages'] + front_pages + [tmp_body] + ['--', output_path],
|
||||
check=True,
|
||||
)
|
||||
|
||||
size_mb = os.path.getsize(output_path) / 1024 / 1024
|
||||
print(f" Saved → {output_path} ({size_mb:.1f} MB)")
|
||||
|
||||
|
||||
def find_chromium_executable() -> Path | None:
|
||||
"""Find a Chromium-based browser on the system (prioritizes WSL/Linux paths).
|
||||
|
||||
On WSL Windows, checks WSL tools first, then falls back to Windows paths.
|
||||
"""
|
||||
import os as _os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
# First, check WSL/Linux locations (common for WSL Windows)
|
||||
wsl_paths = [
|
||||
"/usr/bin/google-chrome",
|
||||
"/usr/bin/google-chrome-stable",
|
||||
"/usr/bin/chromium-browser",
|
||||
"/usr/bin/chromium",
|
||||
"/usr/bin/microsoft-edge",
|
||||
"/usr/bin/microsoft-edge-stable",
|
||||
"/usr/bin/microsoft-edge-dev",
|
||||
"/snap/bin/chromium",
|
||||
"/usr/local/bin/chrome",
|
||||
"/opt/google-chrome/",
|
||||
]
|
||||
|
||||
for p in wsl_paths:
|
||||
if _os.path.isfile(p):
|
||||
return Path(p)
|
||||
|
||||
# Then check shutil.which (standard PATH, includes WSL paths)
|
||||
for name in ("google-chrome-stable", "google-chrome", "chromium-browser", "chromium",
|
||||
"microsoft-edge-stable", "microsoft-edge", "msedge", "chrome"):
|
||||
w = shutil.which(name)
|
||||
if w:
|
||||
return Path(w)
|
||||
|
||||
# Finally, Windows-specific paths (if running natively on Windows)
|
||||
if sys.platform == "win32":
|
||||
paths = [
|
||||
Path(_os.environ.get("PROGRAMFILES(X86)", "")) / "Microsoft/Edge/Application/msedge.exe",
|
||||
Path(_os.environ.get("LOCALAPPDATA", "")) / "Microsoft/Edge/Application/msedge.exe",
|
||||
Path(_os.environ.get("PROGRAMFILES", "")) / "Google/Chrome/Application/chrome.exe",
|
||||
]
|
||||
for p in paths:
|
||||
if p.is_file():
|
||||
return p
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# CLI
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
COMMIT_MSG_FILE="$1"
|
||||
|
||||
if [ -z "$COMMIT_MSG_FILE" ]; then
|
||||
echo "require-commit-body: no commit message file supplied" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
stripped=$(grep -v '^\s*#' "$COMMIT_MSG_FILE" | sed 's/[[:space:]]*$//')
|
||||
|
||||
subject=$(echo "$stripped" | sed -n '1p')
|
||||
separator=$(echo "$stripped" | sed -n '2p')
|
||||
body=$(echo "$stripped" | tail -n +3 | grep -v '^\s*$')
|
||||
|
||||
if [ -z "$subject" ]; then
|
||||
echo ""
|
||||
echo " COMMIT REJECTED: subject line is empty." >&2
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$separator" ] && [ -z "$body" ]; then
|
||||
echo ""
|
||||
echo " COMMIT REJECTED: commit has no body." >&2
|
||||
echo ""
|
||||
echo " Every commit must explain *why*, not just *what*." >&2
|
||||
echo " Format:" >&2
|
||||
echo ""
|
||||
echo " type(scope): short subject" >&2
|
||||
echo ""
|
||||
echo " Explain the motivation for this change. What problem does" >&2
|
||||
echo " it solve? Why is this the right approach? Reference any" >&2
|
||||
echo " relevant issues, prior art, or context." >&2
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$separator" ]; then
|
||||
echo ""
|
||||
echo " COMMIT REJECTED: no blank line between subject and body." >&2
|
||||
echo ""
|
||||
echo " The second line of a commit message must be blank." >&2
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$body" ]; then
|
||||
echo ""
|
||||
echo " COMMIT REJECTED: commit body is empty." >&2
|
||||
echo ""
|
||||
echo " Add at least one line after the blank separator explaining" >&2
|
||||
echo " why this change was made." >&2
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Download self-hosted fonts for the thgtoa MkDocs site.
|
||||
Run once from the repo root. Requires network access (uses npm registry
|
||||
via a temporary npm pack, or direct URLs from fontsource CDN as fallback).
|
||||
|
||||
Usage:
|
||||
python scripts/install_fonts.py
|
||||
|
||||
Fonts installed:
|
||||
docs/fonts/eb-garamond/ - EB Garamond 400/700 normal+italic (latin, latin-ext)
|
||||
docs/fonts/fira-code/ - Fira Code variable (latin, latin-ext)
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
FONTS_DIR = os.path.join(REPO_ROOT, "docs", "fonts")
|
||||
|
||||
EB_GARAMOND_DIR = os.path.join(FONTS_DIR, "eb-garamond")
|
||||
FIRA_CODE_DIR = os.path.join(FONTS_DIR, "fira-code")
|
||||
|
||||
EB_GARAMOND_FILES = [
|
||||
"eb-garamond-latin-400-normal.woff2",
|
||||
"eb-garamond-latin-400-italic.woff2",
|
||||
"eb-garamond-latin-700-normal.woff2",
|
||||
"eb-garamond-latin-700-italic.woff2",
|
||||
"eb-garamond-latin-ext-400-normal.woff2",
|
||||
"eb-garamond-latin-ext-400-italic.woff2",
|
||||
"eb-garamond-latin-ext-700-normal.woff2",
|
||||
"eb-garamond-latin-ext-700-italic.woff2",
|
||||
]
|
||||
|
||||
FIRA_CODE_FILES = [
|
||||
"fira-code-latin-wght-normal.woff2",
|
||||
"fira-code-latin-ext-wght-normal.woff2",
|
||||
]
|
||||
|
||||
|
||||
def npm_pack(package: str, tmpdir: str) -> str:
|
||||
"""Run npm pack and return the path to the extracted package dir."""
|
||||
result = subprocess.run(
|
||||
["npm", "pack", package, "--silent"],
|
||||
cwd=tmpdir, capture_output=True, text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"npm pack failed for {package}:\n{result.stderr}")
|
||||
tgz = os.path.join(tmpdir, result.stdout.strip())
|
||||
with tarfile.open(tgz, "r:gz") as tf:
|
||||
tf.extractall(tmpdir)
|
||||
return os.path.join(tmpdir, "package")
|
||||
|
||||
|
||||
def install_package(package: str, files: list[str], dest: str) -> None:
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
print(f" Fetching {package} ...")
|
||||
pkg_dir = npm_pack(package, tmpdir)
|
||||
files_dir = os.path.join(pkg_dir, "files")
|
||||
for fname in files:
|
||||
src = os.path.join(files_dir, fname)
|
||||
if not os.path.exists(src):
|
||||
print(f" WARNING: {fname} not found in package", file=sys.stderr)
|
||||
continue
|
||||
dst = os.path.join(dest, fname)
|
||||
shutil.copy2(src, dst)
|
||||
size = os.path.getsize(dst)
|
||||
print(f" + {fname} ({size // 1024}K)")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("Installing self-hosted fonts for thgtoa...\n")
|
||||
|
||||
if shutil.which("npm") is None:
|
||||
print("ERROR: npm not found. Install Node.js and re-run.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("EB Garamond (fontsource):")
|
||||
install_package("@fontsource/eb-garamond", EB_GARAMOND_FILES, EB_GARAMOND_DIR)
|
||||
|
||||
print("\nFira Code Variable (fontsource):")
|
||||
install_package("@fontsource-variable/fira-code", FIRA_CODE_FILES, FIRA_CODE_DIR)
|
||||
|
||||
print("\nDone. Font files written to docs/fonts/")
|
||||
print("Commit docs/fonts/ to the repository so it is served with the site.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+57
-33
@@ -1,15 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Auto-generate and prepend a changelog entry to docs/changelog/index.md.
|
||||
|
||||
Called by .github/workflows/changelog.yml. Reads git log since the last
|
||||
changelog version tag, categorises commits by conventional-commit prefix,
|
||||
Called by .github/workflows/04-changelog.yml. Reads git log since the last
|
||||
changelog version, categorises commits by conventional-commit prefix,
|
||||
and prepends a new ## [vX.Y.Z] section in the MkDocs admonition format used
|
||||
by the rest of the file.
|
||||
|
||||
Environment variables (all optional — reasonable defaults apply):
|
||||
MANUAL_VERSION Override the auto-incremented version string.
|
||||
Environment variables:
|
||||
MANUAL_VERSION Version string to record (required when run from CI).
|
||||
Falls back to auto-increment from the changelog for local runs.
|
||||
TRIGGERING_SHA The commit SHA that triggered this run (used as range end).
|
||||
DRY_RUN If "true", print the entry and exit without writing.
|
||||
|
||||
Note: version is sourced from the changelog file, not from git tags. Git tags
|
||||
are no longer used as the version authority. The changelog is the source of truth.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -17,7 +21,6 @@ from __future__ import annotations
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
@@ -52,15 +55,6 @@ def run(cmd: list[str]) -> str:
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def latest_version_tag() -> str | None:
|
||||
"""Return the most recent vX.Y.Z tag reachable from HEAD, or None."""
|
||||
out = run(["git", "tag", "--sort=-version:refname", "--list", "v*"])
|
||||
for line in out.splitlines():
|
||||
if re.match(r"^v\d+\.\d+\.\d+", line):
|
||||
return line
|
||||
return None
|
||||
|
||||
|
||||
def auto_increment_version(current: str | None) -> str:
|
||||
"""Bump the patch number of the current version, or default to v1.0.0."""
|
||||
if not current:
|
||||
@@ -73,7 +67,11 @@ def auto_increment_version(current: str | None) -> str:
|
||||
|
||||
|
||||
def version_from_changelog() -> str | None:
|
||||
"""Parse the most recent ## [vX.Y.Z] heading from the changelog file."""
|
||||
"""Parse the most recent ## [vX.Y.Z] heading from the changelog file.
|
||||
|
||||
This is the primary version source — the changelog is the authority,
|
||||
not git tags.
|
||||
"""
|
||||
if not CHANGELOG.exists():
|
||||
return None
|
||||
for line in CHANGELOG.read_text(encoding="utf-8").splitlines():
|
||||
@@ -84,11 +82,20 @@ def version_from_changelog() -> str | None:
|
||||
|
||||
|
||||
def commits_since(ref: str | None, until: str) -> list[str]:
|
||||
"""Return one-line commit messages between ref and until (exclusive/inclusive)."""
|
||||
"""Return one-line commit messages between ref and until (exclusive/inclusive).
|
||||
|
||||
When no ref is given we fall back to the merge-base between HEAD and
|
||||
origin/main to avoid dumping the entire history into the changelog.
|
||||
"""
|
||||
if ref:
|
||||
log_range = f"{ref}..{until}"
|
||||
else:
|
||||
log_range = until
|
||||
merge_base = run(["git", "merge-base", "HEAD", "origin/main"])
|
||||
if merge_base:
|
||||
log_range = f"{merge_base}..{until}"
|
||||
else:
|
||||
# Truly brand new repo with no remote — limit to last 50 commits
|
||||
log_range = f"-50 {until}"
|
||||
out = run(["git", "log", "--pretty=format:%s", log_range])
|
||||
return [line.strip() for line in out.splitlines() if line.strip()]
|
||||
|
||||
@@ -97,12 +104,30 @@ def categorise(messages: list[str]) -> dict[str, list[str]]:
|
||||
"""Sort commit messages into Added / Changed / Fixed buckets."""
|
||||
buckets: dict[str, list[str]] = {b: [] for b in BUCKET_ORDER}
|
||||
|
||||
NOISE = re.compile(
|
||||
r"""
|
||||
\[skip\ ci\] # CI skip marker
|
||||
| ^Merge\ (pull\ request|branch) # merge commits
|
||||
| ^chore:\ bump # version bump chores
|
||||
| update\ changelog # self-referential
|
||||
| ^\d+/\d+ # numbered commit series (e.g. 3/8)
|
||||
| ^Tweaking # vague WIP messages
|
||||
| ^Moving\ some # vague WIP messages
|
||||
| \ pt\d+$ # "...pt2", "...pt3" suffixes
|
||||
| ^Fix\ (workflow|path|README)$ # one-word infrastructure fixes
|
||||
| ^Still\ broken # embarrassing mid-fix notes
|
||||
| ^WIP\b # work in progress
|
||||
| ^Forgot\ to # oops commits
|
||||
| ^Revert\ " # reverts (surface the original instead)
|
||||
| ^One\ job\ to\ rule # joke commit messages
|
||||
""",
|
||||
re.VERBOSE | re.IGNORECASE,
|
||||
)
|
||||
|
||||
for msg in messages:
|
||||
# Skip automated / noise commits
|
||||
if re.search(r"\[skip ci\]|^Merge |^chore: bump|update changelog", msg, re.I):
|
||||
if NOISE.search(msg):
|
||||
continue
|
||||
|
||||
# Strip conventional-commit prefix to get the plain description
|
||||
m = re.match(r"^(\w+)(?:\([^)]+\))?!?:\s*(.+)$", msg)
|
||||
if m:
|
||||
prefix = m.group(1).lower()
|
||||
@@ -112,7 +137,6 @@ def categorise(messages: list[str]) -> dict[str, list[str]]:
|
||||
description = msg
|
||||
bucket = "Changed"
|
||||
|
||||
# Capitalise first letter
|
||||
description = description[0].upper() + description[1:] if description else description
|
||||
buckets[bucket].append(description)
|
||||
|
||||
@@ -130,7 +154,7 @@ def format_admonition(bucket: str, items: list[str]) -> str:
|
||||
def build_entry(version: str, buckets: dict[str, list[str]], sha: str) -> str:
|
||||
date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||
lines = [f"## [{version}]", ""]
|
||||
lines.append(f'!!! Note "Meta"')
|
||||
lines.append('!!! Note "Meta"')
|
||||
lines.append("")
|
||||
lines.append(f" - Released {date} from [`{sha[:7]}`](https://github.com/Anon-Planet/thgtoa/commit/{sha})")
|
||||
lines.append("")
|
||||
@@ -139,7 +163,6 @@ def build_entry(version: str, buckets: dict[str, list[str]], sha: str) -> str:
|
||||
if buckets.get(bucket):
|
||||
lines.append(format_admonition(bucket, buckets[bucket]))
|
||||
|
||||
# If no commits were categorised, add a placeholder
|
||||
if not any(buckets[b] for b in BUCKET_ORDER):
|
||||
lines.append('!!! Note "Changed"')
|
||||
lines.append("")
|
||||
@@ -153,10 +176,8 @@ def prepend_entry(entry: str) -> None:
|
||||
"""Insert the new entry after the # Release Notes heading."""
|
||||
content = CHANGELOG.read_text(encoding="utf-8")
|
||||
|
||||
# Find the first ## heading and insert before it
|
||||
insert_at = content.find("\n## ")
|
||||
if insert_at == -1:
|
||||
# No existing version section — append after the header block
|
||||
content = content.rstrip() + "\n\n" + entry + "\n"
|
||||
else:
|
||||
content = content[: insert_at + 1] + entry + "\n" + content[insert_at + 1 :]
|
||||
@@ -175,13 +196,11 @@ def main() -> int:
|
||||
manual_version = os.environ.get("MANUAL_VERSION", "").strip()
|
||||
triggering_sha = os.environ.get("TRIGGERING_SHA", "HEAD").strip() or "HEAD"
|
||||
|
||||
# Determine version
|
||||
last_tag = latest_version_tag()
|
||||
last_cl_ver = version_from_changelog()
|
||||
base_version = last_tag or last_cl_ver
|
||||
new_version = manual_version or auto_increment_version(base_version)
|
||||
# Version authority: MANUAL_VERSION (required in CI) → changelog → auto-increment.
|
||||
# Git tags are intentionally not consulted.
|
||||
last_cl_ver = version_from_changelog()
|
||||
new_version = manual_version or auto_increment_version(last_cl_ver)
|
||||
|
||||
print(f"Last tag: {last_tag or '(none)'}")
|
||||
print(f"Last CL version: {last_cl_ver or '(none)'}")
|
||||
print(f"New version: {new_version}")
|
||||
print(f"Triggering SHA: {triggering_sha}")
|
||||
@@ -190,8 +209,13 @@ def main() -> int:
|
||||
print(f"Changelog already contains {new_version} — nothing to do.")
|
||||
return 0
|
||||
|
||||
# Collect commits since the last tag (or all commits if no tag)
|
||||
messages = commits_since(last_tag, triggering_sha)
|
||||
if already_has_version(new_version) and manual_version:
|
||||
print(f"::error::Changelog already contains {new_version}. Choose a different version.")
|
||||
return 1
|
||||
|
||||
# Collect commits since the last changelog version (using it as a git ref
|
||||
# is a best-effort — if the tag doesn't exist, commits_since handles it gracefully).
|
||||
messages = commits_since(last_cl_ver, triggering_sha)
|
||||
print(f"Commits found: {len(messages)}")
|
||||
for m in messages:
|
||||
print(f" {m}")
|
||||
|
||||
+199
-153
@@ -1,214 +1,260 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verification script for PDF files.
|
||||
"""Verification script for thgtoa PDF releases.
|
||||
|
||||
This script verifies:
|
||||
1. SHA256 hash integrity of PDF files
|
||||
2. GPG signature authenticity
|
||||
3. VirusTotal scan status (optional)
|
||||
Verifies SHA-256 hashes, BLAKE2b hashes, and GPG signatures (.asc) for
|
||||
the light and dark PDFs. Optionally checks VirusTotal scan status.
|
||||
|
||||
Usage:
|
||||
python scripts/verify_pdf.py --all # Verify everything
|
||||
python scripts/verify_pdf.py --hashes # Only verify hashes
|
||||
python scripts/verify_pdf.py --signatures # Only verify signatures
|
||||
python scripts/verify_pdf.py --vt # Check VT status (requires API key)
|
||||
|
||||
Examples:
|
||||
python scripts/verify_pdf.py --all
|
||||
python scripts/verify_pdf.py --hashes --file export/thgtoa.pdf
|
||||
python scripts/verify_pdf.py
|
||||
python scripts/verify_pdf.py --hashes
|
||||
python scripts/verify_pdf.py --signatures
|
||||
python scripts/verify_pdf.py --vt
|
||||
python scripts/verify_pdf.py --file export/thgtoa.pdf --hashes
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
def repo_root() -> Path:
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
def calculate_sha256(file_path: Path) -> str:
|
||||
"""Calculate SHA256 hash of a file."""
|
||||
sha256_hash = hashlib.sha256()
|
||||
with open(file_path, "rb") as f:
|
||||
for byte_block in iter(lambda: f.read(4096), b""):
|
||||
sha256_hash.update(byte_block)
|
||||
return sha256_hash.hexdigest()
|
||||
|
||||
def verify_hash(file_path: Path, expected_hash: str) -> bool:
|
||||
"""Verify file hash against expected value."""
|
||||
actual_hash = calculate_sha256(file_path)
|
||||
is_valid = actual_hash == expected_hash
|
||||
status = "✓ PASS" if is_valid else "✗ FAIL"
|
||||
print(f"{status}: {file_path.name}")
|
||||
print(f" Expected: {expected_hash}")
|
||||
print(f" Actual: {actual_hash}")
|
||||
return is_valid
|
||||
def _read_bare_hash(hash_file: Path) -> str | None:
|
||||
"""Read a bare hex digest from a single-value hash file."""
|
||||
try:
|
||||
return hash_file.read_text(encoding="utf-8").strip().split()[0]
|
||||
except (OSError, IndexError):
|
||||
return None
|
||||
|
||||
def verify_signature(file_path: Path, sig_file: Path) -> bool:
|
||||
"""Verify GPG signature of a file."""
|
||||
if not sig_file.exists():
|
||||
print(f"✗ FAIL: Signature file not found: {sig_file}")
|
||||
return False
|
||||
|
||||
def _read_hash_from_sumfile(sum_file: Path, pdf_path: Path) -> str | None:
|
||||
"""Read a hash from a two-column sumfile (sha256sum / b2sum format).
|
||||
|
||||
Matches on the filename only (not the full path) so the file can be used
|
||||
regardless of where the PDFs sit on disk.
|
||||
"""
|
||||
if not sum_file.exists():
|
||||
return None
|
||||
target = pdf_path.name
|
||||
try:
|
||||
for line in sum_file.read_text(encoding="utf-8").splitlines():
|
||||
parts = line.strip().split(None, 1)
|
||||
if len(parts) == 2 and Path(parts[1].lstrip("*")).name == target:
|
||||
return parts[0]
|
||||
except OSError:
|
||||
return None
|
||||
return None
|
||||
|
||||
# Hash verification
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
with path.open("rb") as fh:
|
||||
for chunk in iter(lambda: fh.read(65536), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def _blake2b(path: Path) -> str:
|
||||
h = hashlib.blake2b()
|
||||
with path.open("rb") as fh:
|
||||
for chunk in iter(lambda: fh.read(65536), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def verify_hashes(pdf: Path, export_dir: Path) -> bool:
|
||||
"""Verify all available hash files for a PDF. Returns True if all pass."""
|
||||
stem = pdf.name # e.g. "thgtoa.pdf" or "thgtoa-dark.pdf"
|
||||
results: list[bool] = []
|
||||
|
||||
checks = [
|
||||
("SHA-256", _sha256, export_dir / f"{stem}.sha256", export_dir / "sha256sums.txt"),
|
||||
("BLAKE2b", _blake2b, export_dir / f"{stem}.b2sum", export_dir / "b2sums.txt"),
|
||||
]
|
||||
|
||||
for algo, fn, bare_file, sum_file in checks:
|
||||
# Resolve expected hash — prefer bare file, fall back to sumfile
|
||||
expected = _read_bare_hash(bare_file) if bare_file.exists() else None
|
||||
if expected is None:
|
||||
expected = _read_hash_from_sumfile(sum_file, pdf)
|
||||
if expected is None:
|
||||
print(f" ⚠ {algo}: no hash file found (checked {bare_file.name}, {sum_file.name})")
|
||||
continue
|
||||
|
||||
actual = fn(pdf)
|
||||
ok = actual == expected
|
||||
results.append(ok)
|
||||
mark = "✓" if ok else "✗"
|
||||
print(f" {mark} {algo}")
|
||||
if not ok:
|
||||
print(f" expected: {expected}")
|
||||
print(f" actual: {actual}")
|
||||
|
||||
return all(results) if results else False
|
||||
|
||||
# Signature verification
|
||||
|
||||
def verify_signature(pdf: Path) -> bool | None:
|
||||
"""Verify the .asc detached signature for a PDF.
|
||||
|
||||
Returns True on success, False on failure, None if GPG is not installed
|
||||
or the signature file is missing.
|
||||
"""
|
||||
sig = pdf.with_suffix(pdf.suffix + ".asc")
|
||||
if not sig.exists():
|
||||
print(f" ⚠ Signature file not found: {sig.name}")
|
||||
return None
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gpg", "--verify", str(sig_file), str(file_path)],
|
||||
["gpg", "--verify", str(sig), str(pdf)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"✓ PASS: {file_path.name} signature verified")
|
||||
# Extract key info from GPG output
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'Good signature' in line or 'key ID' in line.lower():
|
||||
print(f" {line.strip()}")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ FAIL: {file_path.name} signature verification failed")
|
||||
print(f" Error: {result.stderr}")
|
||||
return False
|
||||
|
||||
except FileNotFoundError:
|
||||
print("⚠ WARNING: GPG not installed. Skipping signature verification.")
|
||||
print(" ⚠ GPG not installed — skipping signature verification")
|
||||
return None
|
||||
|
||||
def verify_from_hash_file(file_path: Path, hash_file: Path) -> bool:
|
||||
"""Verify file hash from a hash file."""
|
||||
if not hash_file.exists():
|
||||
print(f"✗ FAIL: Hash file not found: {hash_file}")
|
||||
if result.returncode == 0:
|
||||
print(f" ✓ GPG signature valid")
|
||||
# Surface the key info line from stderr (that's where gpg writes it)
|
||||
for line in result.stderr.splitlines():
|
||||
if any(kw in line for kw in ("Good signature", "key ID", "fingerprint", "using")):
|
||||
print(f" {line.strip()}")
|
||||
return True
|
||||
else:
|
||||
print(f" ✗ GPG signature INVALID")
|
||||
for line in result.stderr.splitlines():
|
||||
if line.strip():
|
||||
print(f" {line.strip()}")
|
||||
return False
|
||||
|
||||
expected_hash = None
|
||||
with open(hash_file, 'r') as f:
|
||||
for line in f:
|
||||
parts = line.strip().split()
|
||||
if len(parts) >= 2 and parts[1] == str(file_path):
|
||||
expected_hash = parts[0]
|
||||
break
|
||||
# VirusTotal
|
||||
|
||||
if not expected_hash:
|
||||
print(f"✗ FAIL: Hash not found in {hash_file.name} for {file_path.name}")
|
||||
return False
|
||||
|
||||
return verify_hash(file_path, expected_hash)
|
||||
|
||||
def check_virustotal(file_hash: str, api_key: str | None = None) -> dict | None:
|
||||
"""Check VirusTotal scan status for a file hash."""
|
||||
if not api_key:
|
||||
print("⚠ WARNING: VT_API_KEY not set. Skipping VirusTotal check.")
|
||||
return None
|
||||
def check_virustotal(pdf: Path, api_key: str) -> bool:
|
||||
"""Query VirusTotal for the SHA-256 of a PDF. Returns True if clean."""
|
||||
file_hash = _sha256(pdf)
|
||||
url = f"https://www.virustotal.com/api/v3/files/{file_hash}"
|
||||
req = urllib.request.Request(url, headers={"x-apikey": api_key})
|
||||
|
||||
try:
|
||||
import urllib.request
|
||||
import json
|
||||
|
||||
url = f"https://www.virustotal.com/api/v3/files/{file_hash}"
|
||||
request = urllib.request.Request(url, headers={"x-apikey": api_key})
|
||||
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
|
||||
stats = data.get('data', {}).get('attributes', {}).get('last_analysis_stats', {})
|
||||
total = sum(stats.values()) if stats else 0
|
||||
|
||||
print(f"\n🦠 VirusTotal Results for {file_hash[:16]}...")
|
||||
print(f" Total scans: {total}")
|
||||
|
||||
if stats:
|
||||
print(f" Malicious: {stats.get('malicious', 0)}")
|
||||
print(f" Suspicious: {stats.get('suspicious', 0)}")
|
||||
print(f" Undetected: {stats.get('undetected', 0)}")
|
||||
print(f" Clean: {stats.get('harmless', 0)}")
|
||||
|
||||
return data
|
||||
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
data = json.loads(resp.read().decode())
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404:
|
||||
print(f" ⚠ Not yet scanned on VirusTotal (hash: {file_hash[:16]}…)")
|
||||
else:
|
||||
print(f" ⚠ VirusTotal HTTP error: {e.code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"⚠ ERROR checking VirusTotal: {e}")
|
||||
return None
|
||||
print(f" ⚠ VirusTotal error: {e}")
|
||||
return False
|
||||
|
||||
stats = data.get("data", {}).get("attributes", {}).get("last_analysis_stats", {})
|
||||
malicious = stats.get("malicious", 0)
|
||||
suspicious = stats.get("suspicious", 0)
|
||||
undetected = stats.get("undetected", 0)
|
||||
harmless = stats.get("harmless", 0)
|
||||
total = malicious + suspicious + undetected + harmless
|
||||
|
||||
clean = malicious == 0 and suspicious == 0
|
||||
mark = "✓" if clean else "✗"
|
||||
print(f" {mark} VirusTotal ({malicious} malicious, {suspicious} suspicious, "
|
||||
f"{harmless} clean / {total} engines)")
|
||||
print(f" https://www.virustotal.com/gui/file/{file_hash}")
|
||||
return clean
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
ap = argparse.ArgumentParser(description="Verify PDF files (hashes, signatures, VT).")
|
||||
export = root / "export"
|
||||
|
||||
# File paths
|
||||
ap.add_argument(
|
||||
"--light-pdf",
|
||||
type=Path,
|
||||
default=root / "export" / "thgtoa.pdf",
|
||||
help="Light mode PDF file",
|
||||
ap = argparse.ArgumentParser(
|
||||
description="Verify thgtoa PDF hashes, signatures, and VirusTotal status.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__,
|
||||
)
|
||||
ap.add_argument(
|
||||
"--dark-pdf",
|
||||
"--file",
|
||||
type=Path,
|
||||
default=root / "export" / "thgtoa-dark.pdf",
|
||||
help="Dark mode PDF file",
|
||||
default=None,
|
||||
metavar="PDF",
|
||||
help="Verify a single PDF instead of both light and dark",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--hash-file",
|
||||
"--export-dir",
|
||||
type=Path,
|
||||
default=root / "export" / "thgtoa.pdf.sha256",
|
||||
help="Hash file to verify against",
|
||||
default=export,
|
||||
metavar="DIR",
|
||||
help=f"Directory containing hash and signature files (default: {export})",
|
||||
)
|
||||
|
||||
# Verification modes
|
||||
group = ap.add_mutually_exclusive_group()
|
||||
group.add_argument("--all", action="store_true", help="Verify everything")
|
||||
group.add_argument("--hashes", action="store_true", help="Only verify hashes")
|
||||
group.add_argument("--signatures", action="store_true", help="Only verify signatures")
|
||||
ap.add_argument("--vt", action="store_true", help="Check VirusTotal status")
|
||||
|
||||
ap.add_argument("--hashes", action="store_true", help="Verify hashes only")
|
||||
ap.add_argument("--signatures", action="store_true", help="Verify signatures only")
|
||||
ap.add_argument("--vt", action="store_true", help="Check VirusTotal status")
|
||||
args = ap.parse_args()
|
||||
|
||||
# Determine what to verify
|
||||
if not any([args.all, args.hashes, args.signatures, args.vt]):
|
||||
args.all = True
|
||||
# Default: verify everything
|
||||
do_hashes = args.hashes or not any([args.hashes, args.signatures, args.vt])
|
||||
do_sigs = args.signatures or not any([args.hashes, args.signatures, args.vt])
|
||||
do_vt = args.vt or not any([args.hashes, args.signatures, args.vt])
|
||||
|
||||
all_passed = True
|
||||
# Resolve PDFs to check
|
||||
if args.file:
|
||||
pdfs = [args.file]
|
||||
else:
|
||||
pdfs = [export / "thgtoa.pdf", export / "thgtoa-dark.pdf"]
|
||||
|
||||
pdf_files = [
|
||||
("Light", args.light_pdf),
|
||||
("Dark", args.dark_pdf),
|
||||
]
|
||||
vt_api_key = os.environ.get("VT_API_KEY", "")
|
||||
|
||||
for mode_name, pdf_file in pdf_files:
|
||||
if not pdf_file.exists():
|
||||
print(f"⚠ WARNING: {pdf_file.name} not found. Skipping.")
|
||||
overall_pass = True
|
||||
|
||||
for pdf in pdfs:
|
||||
bar = "─" * 60
|
||||
print(f"\n{bar}")
|
||||
print(f" {pdf.name}")
|
||||
print(bar)
|
||||
|
||||
if not pdf.exists():
|
||||
print(f" ⚠ File not found: {pdf} — skipping")
|
||||
overall_pass = False
|
||||
continue
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Verifying {mode_name} PDF: {pdf_file.name}")
|
||||
print('='*60)
|
||||
if do_hashes:
|
||||
ok = verify_hashes(pdf, args.export_dir)
|
||||
if not ok:
|
||||
overall_pass = False
|
||||
|
||||
# Verify hash if requested
|
||||
if args.all or args.hashes:
|
||||
if not verify_from_hash_file(pdf_file, args.hash_file):
|
||||
all_passed = False
|
||||
if do_sigs:
|
||||
result = verify_signature(pdf)
|
||||
if result is False:
|
||||
overall_pass = False
|
||||
|
||||
# Verify signature if requested
|
||||
if args.all or args.signatures:
|
||||
sig_file = pdf_file.with_suffix(pdf_file.suffix + ".sig")
|
||||
result = verify_signature(pdf_file, sig_file)
|
||||
if result is False: # None means skipped (GPG not installed)
|
||||
all_passed = False
|
||||
if do_vt:
|
||||
if not vt_api_key:
|
||||
print(" ⚠ VT_API_KEY not set — skipping VirusTotal check")
|
||||
else:
|
||||
ok = check_virustotal(pdf, vt_api_key)
|
||||
if not ok:
|
||||
overall_pass = False
|
||||
|
||||
# Check VirusTotal if requested
|
||||
if args.all or args.vt:
|
||||
file_hash = calculate_sha256(pdf_file)
|
||||
api_key = os.environ.get("VT_API_KEY")
|
||||
check_virustotal(file_hash, api_key)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
if all_passed:
|
||||
print("✓ All verifications PASSED")
|
||||
return 0
|
||||
print(f"\n{'─' * 60}")
|
||||
if overall_pass:
|
||||
print(" ✓ All checks passed")
|
||||
else:
|
||||
print("✗ Some verifications FAILED")
|
||||
return 1
|
||||
print(" ✗ One or more checks failed")
|
||||
print()
|
||||
|
||||
return 0 if overall_pass else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
Reference in New Issue
Block a user