commit 4e18fe388a6e17692c87f857ac6cefe312ca94a7
parent 26ba6ecb5502b0c3459147f692c85aa8de94c174
Author: Amit Dutta <mail@amit.is-a.dev>
Date: Sat, 10 Oct 2026 11:15:37 +0530
Merge pull request #36 from notamitgamer/open-source-readiness
Prepare the project for open source (README, community files, CI)
Diffstat:
14 files changed, 588 insertions(+), 1 deletion(-)
diff --git a/.dockerignore b/.dockerignore
@@ -0,0 +1,13 @@
+.git
+.github
+.env
+.env.*
+!.env.example
+**/__pycache__
+*.py[cod]
+.venv
+venv
+.ruff_cache
+.pytest_cache
+android
+*.md
diff --git a/.env.example b/.env.example
@@ -0,0 +1,47 @@
+# Copy to .env and fill in. Never commit .env (it is git-ignored).
+# Load it with: set -a; . ./.env; set +a (or `docker run --env-file .env ...`)
+
+# --- Storage (required) -----------------------------------------------------
+# Hugging Face Storage Bucket that holds every file, as "owner/name".
+HF_BUCKET_ID=your-username/your-cdn-bucket
+# Hugging Face access token with WRITE permission to that bucket.
+HF_TOKEN=
+# Set to 1 to create the bucket as private if it doesn't exist yet.
+HF_BUCKET_PRIVATE=0
+
+# --- Public URLs ------------------------------------------------------------
+# Where the web UI is served, and the host that serves raw files.
+CDN_BASE_URL=http://localhost:8000
+RAW_DOMAIN=raw.localhost
+RAW_BASE_URL=http://raw.localhost:8000
+
+# --- GitHub Actions sync (/api/gh-sync) -------------------------------------
+# Must equal the audience your workflows pass to core.getIDToken(...).
+# Use your own CDN hostname when self-hosting.
+OIDC_AUDIENCE=cdn.example.com
+# Comma-separated GitHub owners that may sync without manual approval.
+GH_ALWAYS_ALLOW=your-github-username
+
+# --- Admin dashboard (/admin) -----------------------------------------------
+# Leave ADMIN_TOKEN unset to switch the dashboard, IP bans and the GitHub
+# allowlist off entirely.
+ADMIN_TOKEN=
+# Encrypts the saved admin state in the bucket. Set it separately so rotating
+# ADMIN_TOKEN doesn't lock you out of that state. Falls back to ADMIN_TOKEN.
+ADMIN_DATA_KEY=
+
+# --- Upload guard -----------------------------------------------------------
+# Refuse uploads from VPN/proxy IPs (1 = on).
+VPN_BLOCK=1
+# Allow the upload anyway if the VPN lookup itself fails (1 = allow).
+VPN_FAIL_OPEN=1
+# Optional proxycheck.io key for the VPN lookup.
+PROXYCHECK_KEY=
+# Comma-separated IPs that are never VPN-blocked.
+VPN_ALLOWLIST=
+
+# --- Client IP detection (rate limits and bans depend on this) --------------
+# Behind Cloudflare, set to 1 to trust the CF-Connecting-IP header.
+TRUST_CF_CONNECTING_IP=0
+# Number of trusted reverse proxies in front of the app (X-Forwarded-For hops).
+TRUSTED_PROXY_HOPS=1
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,45 @@
+name: Bug report
+description: Something isn't working as expected
+labels: ["bug"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for reporting! **Security problems should not be filed here** — see SECURITY.md.
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: What happened?
+ description: What did you do, what did you expect, and what happened instead?
+ validations:
+ required: true
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to reproduce
+ placeholder: |
+ 1. Go to ...
+ 2. Click ...
+ validations:
+ required: true
+ - type: dropdown
+ id: where
+ attributes:
+ label: Where did it happen?
+ options:
+ - The hosted instance (cdn.amit.is-a.dev)
+ - My own deployment
+ - Local development
+ validations:
+ required: true
+ - type: input
+ id: version
+ attributes:
+ label: Commit or version
+ description: Output of `git rev-parse --short HEAD`, if self-hosting.
+ - type: textarea
+ id: logs
+ attributes:
+ label: Logs or screenshots
+ description: Remove tokens, IP addresses and anything private first.
+ render: shell
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Report a security vulnerability
+ url: https://github.com/notamitgamer/cdnV2/security/advisories/new
+ about: Please report security issues privately, not as a public issue.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,27 @@
+name: Feature request
+description: Suggest an improvement or a new feature
+labels: ["enhancement"]
+body:
+ - type: textarea
+ id: problem
+ attributes:
+ label: What problem are you trying to solve?
+ validations:
+ required: true
+ - type: textarea
+ id: proposal
+ attributes:
+ label: What would you like to see?
+ description: Sketches, examples and links to similar tools are welcome.
+ validations:
+ required: true
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: Alternatives considered
+ - type: checkboxes
+ id: help
+ attributes:
+ label: Contribution
+ options:
+ - label: I'd be willing to open a pull request for this.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,17 @@
+## What does this change?
+
+<!-- A short description, and the issue it closes (e.g. "Closes #12"). -->
+
+## Why?
+
+## How was it tested?
+
+<!-- Commands run, pages checked, or "docs only". -->
+
+## Checklist
+
+- [ ] The change is focused on one thing
+- [ ] `ruff check --select E9,F63,F7,F82 app` and `python -m compileall -q app` pass
+- [ ] README / `.env.example` / in-app docs updated if behaviour or configuration changed
+- [ ] No secrets, tokens or `.env` files are included
+- [ ] Existing deployments keep working (or the change is marked as breaking)
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
@@ -0,0 +1,14 @@
+version: 2
+updates:
+ - package-ecosystem: "pip"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ - package-ecosystem: "docker"
+ directory: "/"
+ schedule:
+ interval: "monthly"
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "monthly"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
@@ -0,0 +1,56 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ checks:
+ name: Lint and smoke test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+ cache: pip
+
+ - name: Install dependencies
+ run: pip install -r requirements.txt ruff
+
+ - name: Lint (syntax errors and undefined names)
+ run: ruff check --select E9,F63,F7,F82 app
+
+ - name: Byte-compile
+ run: python -m compileall -q app
+
+ - name: Smoke test (app imports, static pages render)
+ env:
+ HF_BUCKET_ID: ci/placeholder
+ run: |
+ python - <<'PY'
+ from fastapi.testclient import TestClient
+ import app.main as m
+
+ async def no_stats(): # keep CI independent of the Hugging Face API
+ return None
+
+ m.repo_stats = no_stats
+ c = TestClient(m.app)
+ for path in ["/new", "/static/manifest.json"]:
+ r = c.get(path)
+ assert r.status_code == 200, (path, r.status_code)
+ print(path, r.status_code)
+ PY
+
+ docker:
+ name: Docker build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - run: docker build -t cdnv2 .
diff --git a/.gitignore b/.gitignore
@@ -12,3 +12,14 @@ android/app/
android/*.aab
android/build/
android/.gradle/
+
+# Local env files (the template .env.example is committed)
+.env.*
+!.env.example
+
+# Tooling / editors / OS
+.ruff_cache/
+.pytest_cache/
+.idea/
+.vscode/
+.DS_Store
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
@@ -0,0 +1,83 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at the maintainer, [@notamitgamer](https://github.com/notamitgamer), through a GitHub [private security advisory](https://github.com/notamitgamer/cdnV2/security/advisories/new) (marked as a conduct report) or a direct message on GitHub. All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
@@ -0,0 +1,67 @@
+# Contributing to cdnV2
+
+Thanks for helping out! Bug reports, fixes, docs and well-scoped features are all welcome.
+
+By participating you agree to follow the [Code of Conduct](CODE_OF_CONDUCT.md). To report a security
+problem, please use [SECURITY.md](SECURITY.md) rather than a public issue.
+
+## Before you start
+
+- **Bugs and small fixes:** go ahead and open a pull request.
+- **New features or larger changes:** open an issue first so we can agree on the approach before you
+ spend time on it.
+- **Looking for something to do?** Issues labelled `good first issue` are a good place to begin.
+
+## Development setup
+
+```bash
+git clone https://github.com/<you>/cdnV2.git
+cd cdnV2
+python -m venv .venv && source .venv/bin/activate
+pip install -r requirements.txt
+cp .env.example .env # fill in HF_BUCKET_ID and HF_TOKEN
+set -a; . ./.env; set +a
+uvicorn app.main:app --reload
+```
+
+Use a **throwaway Hugging Face bucket and token** for development, never production credentials.
+Run the app from the repository root — templates are loaded from `app/templates` relative to it.
+
+## Checks
+
+CI runs these on every pull request; please run them locally first:
+
+```bash
+pip install ruff
+ruff check --select E9,F63,F7,F82 app # syntax errors and undefined names
+python -m compileall -q app
+docker build -t cdnv2 . # the image must still build
+```
+
+There is no full test suite yet. If you add logic that can be tested without network access (parsing,
+validation, rate limiting), adding a test alongside it is very much appreciated.
+
+## Pull requests
+
+1. Branch from `main` with a descriptive name (`fix-zip-empty-folder`, `docs-self-hosting`).
+2. Keep each PR focused on one change; avoid unrelated reformatting.
+3. Match the surrounding code style. The code is plain, readable Python with short comments that
+ explain *why*, not *what*.
+4. Update the README, `.env.example` and the in-app documentation pages if behaviour or configuration
+ changes.
+5. Fill in the pull request template, including how you tested the change.
+
+## Ground rules
+
+- **Never commit secrets.** No tokens, keys or `.env` files. If you accidentally push one, revoke it
+ immediately and tell a maintainer.
+- **Keep configuration in environment variables**, with defaults that are safe for a new deployment.
+- **Preserve backwards compatibility** for existing deployments unless the change is clearly marked as
+ breaking.
+- **Be careful with anything that touches uploads, rate limits, admin or the GitHub OIDC check** — these
+ are the security boundary of the project. Explain your reasoning in the PR.
+
+## License of contributions
+
+This project is licensed under the [MIT License](LICENSE). By submitting a contribution you agree that
+it is licensed under the same terms, and that you have the right to submit it.
diff --git a/README.md b/README.md
@@ -0,0 +1,158 @@
+# cdnV2
+
+A small, self-hostable static file host. Browse folders in a clean web UI, upload files, grab direct
+links, shorten URLs, and mirror any GitHub repo to the CDN from GitHub Actions — with the files
+themselves stored in a [Hugging Face Storage Bucket](https://huggingface.co/docs/hub/storage-buckets),
+so the app server stays stateless and cheap to run.
+
+Live instance: <https://cdn.amit.is-a.dev> · License: [MIT](LICENSE)
+
+## Features
+
+- **File browser** — folder listings, per-folder filter (press `/`), breadcrumbs, a recent-uploads and
+ visited-files history, and a global search page (`/find`).
+- **Direct and raw links** — every file gets a CDN link, a download link, and a raw link served from a
+ separate raw domain with correct content types, CORS and long-lived caching.
+- **Uploads** — drag-and-drop or paste a URL to fetch. Multi-file uploads get a shareable batch page.
+- **Zip downloads** — download any folder or batch as a `.zip` (capped at 300 files / 500 MB).
+- **URL shortener** — `/shorten`, with short links served from `/mask/<id>`.
+- **GitHub sync** — a GitHub Actions workflow pushes a repo to `/<owner>/<repo>/` using GitHub's OIDC
+ token, so no long-lived secret is ever stored in the contributor's repo. See `/gh-sync` for the
+ workflow template.
+- **Admin dashboard** — `/admin`: upload log, IP bans, GitHub owner allowlist, and file deletion.
+- **Abuse protection** — per-IP rate limits, video-upload blocking (by extension and by file contents),
+ and optional VPN/proxy blocking.
+- **Stats** — `/stats` shows file counts and storage used.
+- **PWA** — installable, with a service worker that deliberately caches nothing, so listings are
+ never stale.
+
+## How it works
+
+```
+ browser / curl ──► FastAPI app (this repo) ──► Hugging Face Storage Bucket
+ │ UI, uploads, rate limits, (the actual files)
+ │ admin, GitHub OIDC check
+ └─ in-memory cache (60 s) for listings and file metadata
+```
+
+The app streams files from the bucket (with HTTP range support) rather than redirecting to it, so your
+own domain stays in every link. Hidden prefixes (`_batches`, `_shortened`, `_logs`, `_admin`) hold
+internal state and never appear in listings.
+
+## Quick start
+
+You need Python 3.12+ and a Hugging Face access token with **write** access.
+
+```bash
+git clone https://github.com/notamitgamer/cdnV2.git
+cd cdnV2
+python -m venv .venv && source .venv/bin/activate
+pip install -r requirements.txt
+
+cp .env.example .env # then edit it: at minimum HF_BUCKET_ID and HF_TOKEN
+set -a; . ./.env; set +a
+
+uvicorn app.main:app --reload # run from the repo root; open http://localhost:8000
+```
+
+The bucket is created automatically on first upload if it doesn't exist
+(private if `HF_BUCKET_PRIVATE=1`).
+
+The raw domain is matched by hostname. Locally, `raw.localhost` resolves to your machine in most
+browsers and in `curl`, which is what `.env.example` uses.
+
+### Docker
+
+```bash
+docker build -t cdnv2 .
+docker run --rm -p 8000:8000 --env-file .env cdnv2
+```
+
+### Deploy on Render
+
+`render.yaml` defines a Docker web service. Set `HF_TOKEN` (and optionally `ADMIN_TOKEN` /
+`ADMIN_DATA_KEY`) in the dashboard, and update `HF_BUCKET_ID`, `RAW_DOMAIN` and `RAW_BASE_URL` for your
+own deployment. Point two hostnames at the service: the main one and the raw one.
+
+## Configuration
+
+All configuration is through environment variables. See [`.env.example`](.env.example) for a commented
+template.
+
+| Variable | Default | Purpose |
+| --- | --- | --- |
+| `HF_TOKEN` | — | Hugging Face token with write access. **Required.** |
+| `HF_BUCKET_ID` | `notamitgamer/cdn` | Bucket that stores the files, as `owner/name`. **Set this.** |
+| `HF_BUCKET_PRIVATE` | `0` | Create the bucket as private if it does not exist. |
+| `CDN_BASE_URL` | `https://cdn.amit.is-a.dev` | Public URL of the web UI, used when building links. |
+| `RAW_DOMAIN` | `raw.cdn.amit.is-a.dev` | Hostname that serves raw files. |
+| `RAW_BASE_URL` | `https://$RAW_DOMAIN` | Public base URL for raw links. |
+| `OIDC_AUDIENCE` | `cdn.amit.is-a.dev` | Audience GitHub Actions workflows must request. Use your own hostname. |
+| `GH_ALWAYS_ALLOW` | `notamitgamer` | Comma-separated GitHub owners that may sync without approval. |
+| `ADMIN_TOKEN` | unset | Enables `/admin`. Leave unset to disable the dashboard, bans and allowlist. |
+| `ADMIN_DATA_KEY` | `ADMIN_TOKEN` | Key used to encrypt saved admin state in the bucket. |
+| `VPN_BLOCK` | `1` | Refuse uploads from VPN/proxy IPs. |
+| `VPN_FAIL_OPEN` | `1` | Allow the upload if the VPN lookup itself fails. |
+| `PROXYCHECK_KEY` | unset | Optional [proxycheck.io](https://proxycheck.io) key for VPN lookups. |
+| `VPN_ALLOWLIST` | unset | Comma-separated IPs exempt from VPN blocking. |
+| `TRUST_CF_CONNECTING_IP` | `0` | Trust `CF-Connecting-IP` (set to `1` behind Cloudflare). |
+| `TRUSTED_PROXY_HOPS` | `1` | Number of trusted reverse proxies in front of the app. |
+
+> **Self-hosting checklist:** the defaults above point at the original deployment. Before going live,
+> set `HF_BUCKET_ID`, `CDN_BASE_URL`, `RAW_DOMAIN`/`RAW_BASE_URL`, `OIDC_AUDIENCE` and `GH_ALWAYS_ALLOW`
+> to your own values, and review the hard-coded branding (`app/static/manifest.json`, the navbar and
+> the documentation templates).
+
+### Getting client IPs right
+
+Rate limits and bans are keyed on the client IP, so the app must see the real one. Behind Cloudflare set
+`TRUST_CF_CONNECTING_IP=1`; behind other proxies set `TRUSTED_PROXY_HOPS` to the number of proxies you
+trust. The admin dashboard shows how the server currently sees your connection.
+
+## HTTP API
+
+| Method & path | Description |
+| --- | --- |
+| `GET /{path}` | Folder listing or file page. |
+| `GET /api/get/{path}` | Download a file (range requests supported). |
+| `POST /api/put` | Upload files (`multipart/form-data`: `files`, `folder`). |
+| `POST /api/put-url` | Fetch a remote URL into the CDN. |
+| `GET /api/find?q=…` | Search all files. |
+| `GET /api/pack/{path}` | Zip a folder (`/api/pack-info/{path}` returns its size first). |
+| `POST /api/shorten` | Create a short link. |
+| `POST /api/gh-sync` | Sync a GitHub repo (GitHub Actions OIDC token + tar.gz archive). |
+| `GET /api/stats` | Storage statistics. |
+
+Uploads go to either `uploads/` or `third-party/`.
+
+Default limits per client IP: 200 MB/hour for file uploads, 50 MB/hour for URL uploads, and 10 requests
+per minute / 50 per hour for the shortener. GitHub sync has no rate limit but accepts at most a 200 MB
+compressed archive. Video files are refused.
+
+## Project layout
+
+```
+app/
+ main.py routes, uploads, zip packing, GitHub sync endpoint
+ storage.py Hugging Face bucket access, listing, caching, search
+ upload_guard.py upload checks: video blocking, VPN detection, client IP
+ admin.py /admin dashboard, bans, GitHub allowlist (encrypted state)
+ gh_oidc.py GitHub Actions OIDC token verification
+ shortener.py URL shortener
+ stats.py /stats
+ templates/ Jinja2 pages (index.html renders most page types)
+ static/ icons, manifest, service worker
+Dockerfile · render.yaml · start.sh
+```
+
+## Contributing
+
+Contributions are welcome — please read [CONTRIBUTING.md](CONTRIBUTING.md) first. Participation is
+governed by the [Code of Conduct](CODE_OF_CONDUCT.md). To report a vulnerability, follow
+[SECURITY.md](SECURITY.md) instead of opening a public issue.
+
+## License
+
+[MIT](LICENSE) © 2026 Amit Dutta. All runtime dependencies are under permissive licenses (MIT, BSD,
+Apache-2.0, Unlicense). The only third-party browser asset is [Turbo](https://github.com/hotwired/turbo) (MIT), loaded from
+jsDelivr.
diff --git a/SECURITY.md b/SECURITY.md
@@ -0,0 +1,41 @@
+# Security Policy
+
+## Supported versions
+
+Only the latest commit on `main` is supported. Fixes are not backported.
+
+## Reporting a vulnerability
+
+**Please do not open a public issue for security problems.**
+
+Report privately through GitHub:
+<https://github.com/notamitgamer/cdnV2/security/advisories/new>
+
+Please include:
+
+- a description of the issue and its impact,
+- steps to reproduce (a minimal request or proof of concept is ideal),
+- the commit or deployment you tested against.
+
+You can expect an acknowledgement within a few days. Please give us reasonable time to ship a fix
+before disclosing publicly; we are happy to credit you in the advisory if you would like.
+
+## Scope
+
+In scope: this repository's code and its default configuration — in particular the upload guard and
+rate limiting, the admin dashboard and its token handling, the GitHub OIDC verification
+(`/api/gh-sync`), the URL-fetching upload (SSRF), the zip packer (path traversal, zip bombs), and the
+file-serving routes.
+
+Out of scope: vulnerabilities in third-party services (Hugging Face, GitHub, Cloudflare, Render) or in
+dependencies with no impact on this project, denial of service by sheer traffic volume, and issues that
+require a misconfigured deployment (for example running without `TRUST_CF_CONNECTING_IP` behind a
+proxy and then spoofing IPs).
+
+## Hardening tips for operators
+
+- Set a strong, random `ADMIN_TOKEN`, and a separate `ADMIN_DATA_KEY`.
+- Use a Hugging Face token scoped to the one bucket, with the minimum permissions needed.
+- Configure `TRUST_CF_CONNECTING_IP` / `TRUSTED_PROXY_HOPS` correctly, otherwise rate limits and bans
+ can be bypassed or can hit the wrong people.
+- Keep dependencies up to date (Dependabot is configured in this repository).
diff --git a/app/gh_oidc.py b/app/gh_oidc.py
@@ -14,6 +14,7 @@ Why this instead of a PAT/App token per contributor:
Docs: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
"""
+import os
import time
import httpx
import jwt
@@ -22,7 +23,9 @@ from fastapi import HTTPException
GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com"
JWKS_URL = f"{GITHUB_OIDC_ISSUER}/.well-known/jwks"
-EXPECTED_AUDIENCE = "cdn.amit.is-a.dev"
+# The `audience` your workflows request in core.getIDToken(...). Self-hosters: set OIDC_AUDIENCE
+# to your own CDN hostname. The default keeps the original deployment working unchanged.
+EXPECTED_AUDIENCE = os.getenv("OIDC_AUDIENCE", "cdn.amit.is-a.dev")
_jwks_client = None
_jwks_client_ts = 0