A new crate and service, crates/directory, live at ipx-dir.sdf1.net. ipx servers send the public feeds their people subscribe to (POST /v1/snapshot, replacing what that token sent before); the directory fetches each feed's title, artwork, category, description and latest twenty items every six hours with ipx-feed, and lists them A to Z and by subscribers summed over every server. It holds metadata only, never a file or a file's address. Tokens are in its config file as SHA-256 hashes, each ro, rw or rwa, and the file is read again within a minute of a change, so adding and revoking one is an edit and nothing else; a revoked token takes its server's counts. At first they were to come from environment variables, which would have meant recreating the container to add one. A file that does not load keeps the tokens as they were. Every address sent is checked with ipx_feed::url_looks_private, the check ipx makes before sending, so one server's miss does not publish a paid feed to every other; an rwa token can remove and block what both miss. Deployed: ipx-directory in the ipx Arcane project, database ipx_directory (its own login) on the databases project's Postgres, which db-backup already covers, and a tunnel route with no Access in front. The store's tests run on Postgres as well as SQLite when IPX_TEST_DATABASE_URL is set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.6 KiB
The directory
ipx-directory is a catalogue of public podcasts and blogs that ipx servers share (#204). An ipx
server sends it the public feeds its people subscribe to, and how many of them do. The directory
fetches each feed's title, artwork, category, description and latest twenty items every six hours,
and any server holding a token can browse it. It never downloads a file, and it never stores
one's address: every ipx server still fetches its own feeds.
This server's is at https://ipx-dir.sdf1.net, the ipx-directory service of the Arcane project
ipx, with its database ipx_directory on the databases project's Postgres.
Tokens
Everything but /healthz needs a token, sent as Authorization: Bearer <token>. Each one has a
name and one of three levels of access:
| browse | send snapshots | remove, block, refresh feeds; drop a server's snapshot | |
|---|---|---|---|
ro |
yes | ||
rw |
yes | yes | |
rwa |
yes | yes | yes |
Tokens are in the config file and nowhere else, and no level of access can make or revoke one over the API. To make one:
docker exec ipx-directory ipx-directory token new <name> --access ro|rw|rwa
It prints the token, to give to whoever it is for, and the lines to add to the config file. The file
holds the token's SHA-256, never the token. The directory reads the file again within a minute of
a change, so adding a token and revoking one (deleting its entry) need no restart. A revoked token
also takes the counts its server sent. A file that does not load (a misspelled access, two tokens
with one name) leaves the tokens as they were, and the log says why.
The config file
/config/directory.toml in the container (DIRECTORY_CONFIG, or --config), on Tower
/mnt/fast/appdata/ipx-directory/directory.toml:
bind = "0.0.0.0:8100"
# The environment variable holding the database URL: postgres://... or sqlite://...
database_url_env = "DIRECTORY_DATABASE_URL"
[[tokens]]
name = "tower"
access = "rwa"
token_sha256 = "…"
DIRECTORY_LOG_FORMAT=json logs one JSON object a line, for Loki, as ipx's IPX_LOG_FORMAT does.
Each request is a line with its method, path, status, duration_ms and the token's name.
Running one
services:
ipx-directory:
image: ipx-directory # docker build -f Dockerfile.directory -t ipx-directory .
restart: unless-stopped
environment:
DIRECTORY_DATABASE_URL: "postgres://ipx_directory:password@db-host:5432/ipx_directory"
ports:
- "8100:8100"
volumes:
- ./directory:/config:ro # directory.toml
healthcheck:
test: ["CMD", "ipx-directory", "health"]
What it will not hold
A private feed is never meant to reach it. ipx leaves out a feed marked private, one with a
username or password, and one whose address ipx_feed::url_looks_private catches: a paid-feed
host, or a key in the query. The directory checks every address it is sent with the same function
and refuses what it catches, so one server's miss does not publish someone's paid feed to every
other. What both miss, an rwa token can remove: DELETE /v1/feeds/{id} removes the feed and
blocks it, so no snapshot brings it back.
API
| Route | Access | |
|---|---|---|
GET /healthz |
none | ok when it can read its database |
GET /v1/me |
ro | the token's name and access |
GET /v1/feeds |
ro | every feed A to Z: id, url, title, image, category, subcategory, podcast, subscribers |
GET /v1/popular?limit=10 |
ro | the most subscribed, at most 100 |
GET /v1/feeds/{id} |
ro | one feed, with description, failing (why its last check failed, in words) and its latest twenty items (guid, title, link, published, duration, description), never their files |
POST /v1/snapshot |
rw | [{"url", "subscribers"}], at most 20,000: replaces everything this token sent before; answers what it accepted and how many it refused as private, blocked or invalid |
DELETE /v1/feeds/{id} |
rwa | remove and block |
GET /v1/blocked, DELETE /v1/blocked/{id} |
rwa | what is blocked; unblock |
DELETE /v1/instances/{name} |
rwa | drop one token's snapshot, its counts with it |
POST /v1/feeds/{id}/refresh |
rwa | fetch the feed now, answering with its page |
A feed's id is the first 64 bits of the SHA-256 of its address, normalised (scheme and host in
lower case, no default port or fragment), so every server arrives at the same id. subscribers
is the sum over every token's latest snapshot. A feed no snapshot has any more goes. Descriptions
are the feed's own HTML, unsanitized: whoever shows them sanitizes them, as ipx does.