docs/The front door
The front door
Every human page has a machine counterpart, and the split is carried by the URL shape alone — no API keys, no content negotiation, plain HTTP built for curl.
The URL grammar
| Shape | Meaning |
|---|---|
/{publisher} | A publisher: a domain handle (graham.systems) or a DID (did:plc:…) |
/{publisher}/{project} | A project landing; nested paths welcome (/dist-town/cli) |
…/{project}@{selector} | A release: @1.2.0 names a version, @latest resolves a pointer |
…/{project}/~releases, …/~changelog | Human surfaces (tabs); ~-segments are routes, never names |
…@{selector}/{artifact} | Machine: 302 to the artifact bytes on the publisher’s storage |
…@{selector}/SHA256SUMS | Machine: checksums for every artifact, sha256sum -c format |
…@{selector}/{artifact}.sha256 | Machine: one artifact’s checksum, a single sha256sum -c line |
The split is a shape check, not a lookup: any path segment containing @ with at least one segment after it is a machine path. Everything else is a page, with three prefixes carved out as machine paths regardless of shape — /xrpc/, /.well-known/, and /disttown.schema.json — and one shape carved back in: a path ending in /__data.json is this site’s own client-side navigation, so it stays a page even after an @ segment.
Those carve-outs cost three artifact names. An artifact named SHA256SUMS, an artifact whose name ends in .sha256, and an artifact named __data.json have no front-door download URL: the first two are the checksum endpoints, the third is a page path. Their digests are still in the release record and still listed in SHA256SUMS; only the download URL is taken.
Downloads are 302s
dist.town serves zero artifact bytes. An artifact URL answers with a redirect to the publisher’s storage, plus an RFC 9530 Repr-Digest header carrying the expected sha256 — follow with curl -L and verify what arrived.
The redirect goes to the location the release record declares, and only once that location has been verified against the record’s digest. Locations that dist.town merely discovered are reported through the read plane, not redirected to.
Two storage kinds have no front-door download today: artifacts declared as hostStorage or ociStorage answer 404 saying the location is unresolvable. Their digests are still in the record, so SHA256SUMS and .sha256 answer for them as usual.
Checksums come from the signed record
SHA256SUMS and .sha256 responses are generated from the digests in the publisher’s signed release record — never read from the storage that serves the bytes. A compromised bucket can change what it serves, but it cannot change what the sums say the bytes should be.
curl -LO https://dist.town/{publisher}/{project}@{version}/SHA256SUMS
sha256sum -c SHA256SUMS --ignore-missingSHA256SUMS is one <hex> <name> line per sha256-digested artifact in the release — sha256sum -c format, two spaces between the digest and the name. <artifact>.sha256 is that same single line for one artifact, and carries Repr-Digest as well. An artifact whose digest uses another algorithm is omitted from SHA256SUMS, and its .sha256 answers 404.
Lifecycle on every response
X-Dist-Status carries lifecycle state on every response that got as far as a release: active, yanked, deprecated, archived, or retracted. It is absent when nothing was resolved — a 502 on the publisher, or a 404 for a project or selector that doesn’t exist — because there is no lifecycle to report. A 404 for an artifact name still carries it: the release resolved, the file just isn’t in it.
The yank rules:
- An explicit version (
@1.2.0) always serves — with its status attached, so your tooling can decide. - A pointer (
@latest) never resolves to a yanked release; the request fails with409and an explanation. Channels don’t hand out known-bad versions; pins keep working.
The response vocabulary
| Status | What it means |
|---|---|
302 | The location is verified; follow it. Repr-Digest carries the expected digest. |
409 | Either a pointer whose target is yanked, or a location whose bytes failed verification against the record. |
410 | The version was retracted by its publisher. X-Dist-Status: retracted, and the body is the testimony: retracted by the publisher <timestamp> (<kind>): <reason>. |
425 | The location has not been verified yet. Retry shortly, or verify the digest yourself from the .sha256 endpoint. |
503 | The location is suspended — the origin changed since verification — or a record-located artifact can’t be resolved right now. |
404 | No such project, no pointer or version matching the selector, no such artifact in the release, or an artifact whose storage has no resolvable location. |
502 | The publisher’s identity could not be resolved. |
Every one of these is plain text with the reason in the body. A 410 is not a 404 on purpose: a pin the publisher deliberately deleted deserves an answer.
The read plane
Public XRPC under /xrpc/town.dist.* is the same read plane this site consumes, with no private API behind it. If you’re building richer tooling than downloads, that’s your surface. Seven queries:
town.dist.resolveRelease—repo,project,selector: the release a pointer name or literal version resolves to, with lifecycle and locations.410 ReleaseRetractedfor a retracted version.town.dist.listReleases—repo,project: a project’s releases in creation order.limit(default 50, max 100) andcursorpage it.town.dist.getProject—repo,project: everything a project page renders, in one call.town.dist.listRecentReleases— newest-first feed. Unfiltered it spans every indexed publisher;reposcopes it to one,repoplusprojectto a subtree.limit(default 10, max 50) andcursor.town.dist.listProjects—repo: a publisher’s projects, rename aliases excluded.town.dist.listChangelogs—repo,project: changelog documents newest first, aggregating descendants under an umbrella.town.dist.getChangelog—repo,project,rkey: one document, the permalink’s machine twin.
Two more machine documents ride the same origin: /.well-known/did.json is the service identity (did:web:dist.town), and /disttown.schema.json is the CLI manifest’s JSON Schema — the URL every $schema line names.
The pages are built from this read plane, with one call outside it: a publisher’s avatar and display name come from the public Bluesky appview.
Moderation is legible too
dist.town’s moderation actions are published as signed labels under its service identity (did:web:dist.town): query them at com.atproto.label.queryLabels or follow the stream at com.atproto.label.subscribeLabels. A suppression emits a suppressed label on the subject; reversal emits a negation. Content reports go to com.atproto.moderation.createReport on this origin, rate-limited per source address.