dhkatz fbf1e01314
CI / build (push) Successful in 9s
Release / release (push) Successful in 2s
CI / image (push) Successful in 21s
ci: release pull requests, and conventional-commit PR titles (#1)
Releases as pull requests, the same in all five app repos (hub, catalog, live, riftbound, karaoke). The Gitea side (PRs only into main, squash merges, required checks, `v*` tags only by release-bot, its `RELEASE_TOKEN` org secret) is dhkatz/infra `modules/app-repo`.

- **`.gitea/workflows/pull-request.yml`**: the PR title must be a conventional commit (`feat(api): …`, `fix: …`, `feat!: …`); it becomes the commit on main. `chore(release)` only from release-bot's `release` branch. Required to merge (`Pull request / title`).
- **`.gitea/default_merge_message/SQUASH_TEMPLATE.md`**: the squash commit is `<PR title> (#<n>)` plus the description.
- **`.gitea/workflows/release.yml`**, on every push to main, as release-bot (`RELEASE_TOKEN`):
  - if the commit is a merged `chore(release): vX.Y.Z`: tag it vX.Y.Z (lightweight), push the tag (CI builds the image, Komodo deploys prod), publish the release with git-cliff's notes;
  - otherwise, if anything releasable landed since the last tag: rewrite the `release` branch (CHANGELOG.md) and open or update `chore(release): vX.Y.Z`.
- **`cliff.toml`** (git-cliff 2.14.2, pinned by sha512): feat → minor, fix/perf/revert → patch, breaking → minor while 0.x; chore/ci/docs/test/style/build/refactor alone don't release. Only conventional commits count, so the history before this is left out.
- **`CHANGELOG.md`**: header only; release PRs fill it.
- Agent notes (CLAUDE.md / AGENTS.md): the PR flow and title format; never tag by hand.

After merging, `Release` runs on main and should report nothing to release (this PR is `ci:`); the first `feat:`/`fix:` PR merged after it opens the first release PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #1
Co-authored-by: dhkatz <[email protected]>
2026-11-03 08:40:38 +00:00

t5de-catalog

T5DE's catalog server: it stores community products' files by sha256, serves them, and registers uploads with T5DE's hub (git.t5de.dev/t5de/t5de-hub). Creators upload from the hub's website straight to a catalog, so large files never pass through the hub; T5DE's proxy then downloads the files from the catalogs the hub lists. Anyone can run a catalog and ask the hub to approve it.

The contract (tokens, this API, the hub's API for catalogs) is t5de-hub's docs/products.md; the design and the reasons are in its docs/design.md ("Products and catalogs", "Catalogs (t5de-catalog)").

Unofficial: not made by or affiliated with IMVU.

How it works

  • Uploads (POST /api/v1/uploads): the creator's browser sends the product as a zip (Create Mode's .chkn, files at the root) with an upload token from the hub. The catalog checks the token (signed by the hub, for this catalog), checks the files by t5de-api's pack rules (names, limits counted while reading, index.xml with ProductRating GA or AP), stores each file under its sha256, and registers the revision with the hub (POST /api/v1/catalog/revisions, Authorization: Catalog <id>:<key>). Only once the hub accepts it is the revision recorded as hosted; files of a refused upload stay on disk but aren't served.
  • Files (GET /api/v1/files/<sha256>): immutable, so cached for a year (ETag is the hash; ranges work). A file used only by adult revisions needs an access token from the hub with adult: true, and isn't cached by shared caches.
  • Tokens are EdDSA (Ed25519) JWTs; the hub's keys come from HUB_URL/.well-known/t5de-keys.json, cached and fetched again when a token names an unknown key (at most every 30 s).
  • Storage: CATALOG_DATA/catalog.db (SQLite: the revisions hosted and which files belong to them) and CATALOG_DATA/files/<first 2 hex>/<sha256>, written whole (temp file, then rename). CATALOG_DATA/tmp holds uploads being checked and is emptied at start.

API

GET /health {ok: true}
POST /api/v1/uploads Authorization: Bearer <t5de-upload token>; multipart: file (the zip), notes (required for a new revision of an existing product). 201 {product, revision}. Errors are {error, message}: 400 bad files or form (bad_files, notes_required, bad_request), 401 bad token (bad_token), 413 over 300 MiB (too_large), 502 the hub refused (its error code, e.g. unchanged) or couldn't be reached (hub_unavailable). CORS for WEB_ORIGIN.
GET /api/v1/files/<sha256> the file; 401/403 for adult-only files without an adult access token; 404 if not hosted
GET /api/v1/revisions/<product>/<revision> {product, revision, rating, files} for revisions hosted here

Running

Register the catalog on the hub's website (POST /api/v1/catalogs) to get its ID and key, and have a hub moderator approve it. Then, for a hub running locally:

HUB_URL=http://localhost:4101 \
CATALOG_ID=<id> CATALOG_KEY=<key> \
CATALOG_PUBLIC_URL=http://localhost:4102 \
WEB_ORIGIN=http://localhost:4100 \
CATALOG_DATA=./data \
go run ./cmd/t5de-catalog
Variable
CATALOG_ADDR listen address (default 127.0.0.1:4102)
CATALOG_DATA data folder (default ./data)
CATALOG_PUBLIC_URL the catalog's public URL, as registered on the hub (logged)
HUB_URL the hub's API; also the tokens' issuer (required)
CATALOG_ID, CATALOG_KEY from registering on the hub (required)
WEB_ORIGIN the hub website's origin, allowed to upload from browsers

It logs to standard error and stops cleanly on Ctrl+C or SIGTERM, finishing requests in progress. Put it behind a TLS-terminating reverse proxy (or a CDN for /api/v1/files/) in production.

With Docker:

docker build -t t5de-catalog .
docker run -p 4102:4102 -v catalog-data:/data -e HUB_URL=... -e CATALOG_ID=... -e CATALOG_KEY=... \
  -e CATALOG_PUBLIC_URL=... -e WEB_ORIGIN=... t5de-catalog

Building

go test ./...
CGO_ENABLED=0 go build -trimpath -o dist/t5de-catalog ./cmd/t5de-catalog

The SQLite driver is pure Go (modernc.org/sqlite), so builds are static. t5de-api comes from git.t5de.dev directly: set GOPRIVATE=git.t5de.dev.

S
Description
An open implementation of T5DE's catalog server.
Readme
72 KiB
0 Stars 2 Watchers 0 Forks
Languages
Go 98.5%
Dockerfile 1.5%