Git LFS routes
The Git LFS endpoint of every repository, for git-lfs with git-xet
registered, and for Xet-native clients such as jj-xet. It follows the Git
LFS batch API
(https://github.com/git-lfs/git-lfs/blob/main/docs/api/batch.md), with
the decisions of ADR 0003
and ADR 0005. This page is
written from the code in crates/hub/src/lfs; the tests are
crates/hub/tests/lfs.rs.
The LFS URL
{hub.public_url}/{ns}/{name}.git/info/lfs
This is what lfs.url points at, and what git-lfs derives from a Git
remote on this server. The same routes answer without .git, and under
the typed prefixes datasets/, spaces/ and kernels/, which see the
repository through that label.
The unprefixed URL serves a repository whatever its labels.
Credentials
Every route but the capability route takes the caller's OpenXet access
token, either as the password of HTTP Basic (the user name is ignored),
which is what git's credential helpers give git-lfs, or as
Bearer <token>. Rules decide what it may do.
- No credential, or one the server doesn't know:
401withWWW-AuthenticateandLFS-Authenticateset toBasic realm="openxet", sogit-lfsasks its credential helpers. - A repository the caller may not read answers exactly as one that
doesn't exist:
404 repo_not_found. - A write the caller may not make on a repository it may read:
403 forbidden.
Errors are JSON, {"error", "schema": "openxet.error.v1", "code", "message", "hint"}, with an X-Error-Message header and, where Hugging
Face defines one, X-Error-Code, as on the Hub API. No answer and no log line carries the caller's
credential. Every answer is Cache-Control: no-store.
Routes
| Method and path | Credential | Does |
|---|---|---|
POST {lfs-url}/objects/batch | read for downloads, write for uploads | The batch API |
POST {lfs-url}/objects/verify | write | Whether an upload completed |
GET {lfs-url}/xet-read-token | read | A CAS read token for the repository |
GET {lfs-url}/xet-write-token | write | A CAS write token for the repository |
GET /v1/capabilities/{repo id}/write?exp=…&sub=…&sig=… | the signed URL | Refreshes an upload's CAS token |
POST {lfs-url}/objects/batch
Request and answer are application/vnd.git-lfs+json. A batch names at
most 1,000 objects (git-lfs sends 100); hash_algo, if sent, must be
sha256, else 409 unsupported_hash_algorithm. An object is a SHA-256
and a size; the repository has it when a shard uploaded to it registered
a file with that SHA-256 and size (ADR 0005 item 7). An object whose oid
is not 64 lowercase hex characters, or whose size is negative, gets the
object error 422.
Uploads that offer xet answer "transfer": "xet". An object the
repository has gets no actions. Every other object gets:
actions.upload.href: an absolute capability URL, see belowactions.upload.header:X-Xet-Cas-Url,X-Xet-Access-Token,X-Xet-Token-Expiration(Unix seconds, as a string) andX-Xet-Session-Id, in exactly that casing. The token is a CAS write token for the repository livinghub.token_ttl, never past the capability.actions.upload.expires_in:hub.capability_ttlin secondsactions.verify.href:{lfs-url}/objects/verify, with no header:git-lfssends the credential it holds for the host.
No object carries authenticated, so git-lfs sends its credential for
the host with the verify (git-lfs 3.7.1 sends it without one when
authenticated is true). git-xet doesn't read the field.
All objects of one batch share one capability, token and session.
Uploads that don't offer xet (plain git-lfs) answer each object
the repository doesn't have with the object error 501 and a message
naming git-xet and git xet install. OpenXet stores LFS files through
Xet, and an href that would fail later is never handed out.
Downloads always answer "transfer": "basic", whether xet is
offered or not: git-xet registers for downloads but can't do them (ADR
0003 decision 3). An object the repository has gets
actions.download.href, the file gateway URL
{hub.public_url}/v1/transfer/files/{repo id}/{xet hash}, and
actions.download.header holding only X-Xet-Hash. There is no
authenticated field, so git-lfs sends its credential for the host to
the gateway, and the answer carries no token of any kind. An object the
repository doesn't have, including one registered only to another
repository, gets the object error 404.
The first push. git-lfs uploads before git pushes. An upload batch
to a repository that doesn't exist creates it, without a commit, when the
caller may create in the namespace, labelled as the URL it came through
(model for the unprefixed URL). A download batch, a token route or a
refused batch never creates one.
POST {lfs-url}/objects/verify
Body {"oid", "size"}. 200 {} when the repository has the object, else
404 object_not_uploaded, so an upload that never completed is not a
silent success.
GET {lfs-url}/xet-read-token, GET {lfs-url}/xet-write-token
For clients that chunk and upload before they know any oid (ADR 0005). The answer is the Hub token route's:
{"accessToken": "REDACTED", "exp": 1792736900, "casUrl": "https://openxet.example.com"}
with the same values in the X-Xet-Access-Token, X-Xet-Cas-Url and
X-Xet-Token-Expiration headers. exp is Unix seconds, hub.token_ttl
ahead. The token is scoped to the repository and the operation, never to
objects. To refresh, repeat the request.
GET /v1/capabilities/{repo id}/write?exp=…&sub=…&sig=…
The href of an upload action, which git-xet calls to refresh its CAS
token during a long upload. The signature is the whole authorization (ADR
0003 decision 2): the route needs no Authorization header, and one that
is sent is never used, logged or echoed, and never fails the request. The
log records only whether one was present.
The answer is the token routes' answer for the repository and pusher the
capability was issued to, with a token living hub.token_ttl but never
past the capability's exp. The href therefore outlives every token it
issues. A capability that is altered, presented for another repository,
operation or subject, or past its exp, is refused with 403 capability_refused; push again for a fresh one.
A capability doesn't re-check the rules: a writer whose
access is revoked can still refresh CAS tokens through a capability issued
before the revocation until it expires, so hub.capability_ttl is also
that window (ADR 0003 decision 2). Capabilities are signed
with transfer.key (config), so any
node sharing the key answers one another node issued.
Configuration
hub.token_ttl and hub.capability_ttl (config) set
the two lifetimes. capability_ttl must be at least token_ttl.
Client setup
With the repository hosted on this server, install git-xet, run
git xet install, and store the access token as git's password for the
host (git credential approve). With the Git repository hosted elsewhere and its LFS
objects here, set lfs.url to this server's LFS URL and, in each clone,
lfs.<endpoint git-xet derives from the remote>.access=none, so that
git-xet calls the capability href without a credential (ADR 0003
decision 4).