~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

7
754 lines · 67.1 KB Raw History

HTTP and XRPC API

What knotless-serve answers over HTTP. Which flags turn each part on is in Commands; how the parts work is in knotless-serve.

An error answers its status with a plain-text message, but under /xrpc/, where it is JSON as XRPC has one: {"error", "message"}, error naming its kind (InvalidRequest, AuthenticationRequired, Forbidden, NotFound, Conflict, UpstreamFailure, InternalServerError) unless an entry names another. In a path, <authority> is a handle or DID and <repo> a repository's name; a trailing .git on the name is dropped.

StatusWhen
400a bad parameter, or a bad repository name
401a missing or bad service token
403a caller who may not do this
404<url>: no such repository, <url>: taken down, <name>: no one's handle (not one an account can have, or one a resolver answered that none has), or a part this server does not run (this server keeps no index, this server is not a gateway)
502the PDS or identity lookup failed

Who asks

A query under Appview or Web data may carry a service token, as atproto's appviews take one: Authorization: Bearer <jwt>, signed by the asker's PDS (com.atproto.server.getServiceAuth) with this server's did:web as aud and the query's NSID as lxm. A PDS proxying to the #knotless_appview service sends one itself. With none, a query answers as for anyone. A token that does not hold, or one sent to a server with no --public-url and so no DID, is 401 AuthenticationRequired. With no token, the cookie knotless_session of someone logged in to the pages names the asker; one that no longer holds asks as no one. Every query answer is Vary: authorization, cookie, and one with an asker is Cache-Control: private, no-cache; one that read a private repository for its asker, private, no-store.

A repository no one may read publicly is read as the asker, with the session they logged in to the pages with: a member of it reads it as a public one, and anyone else, asker or not, gets the 404 NotFound a repository never made gets (access.md). GET /<owner>/<name>/raw/<rev>/<path> reads as the cookie's account too, and varies by cookie; the cards, git and /api/ routes read as no one.

A query may instead prove its asker a member with a delegation token, for an app that signs people in to their own PDS rather than here: Atproto-Space-Delegation: <jwt>, from the asker's PDS (com.atproto.space.getDelegationToken) for the repository's space, at://<owner DID>/space/dev.knotless.space/<name>, one for each query. The server checks its signature against the account it names and spends it: one sent again is 401 AuthenticationRequired, as is one that is no delegation token, or that names another account than a service token or cookie sent with it. It reads that one repository as its member, as a session would, exchanging the token with the space's authority, which answers only a member; anything else the query reads, it reads as for anyone, or as the service token or cookie. A token for another space, or from someone who is not a member, reads as no token does. The queries answer a page on any other site (CORS, with Authorization and Atproto-Space-Delegation allowed), which a browser never sends the cookie with, and so does /.well-known/did.json, where such a page finds them (#knotless_appview).

Health

GET /healthz

Answers ok while the process runs.

GET /_status

Parameter
repo<handle or did>/<name>: also read that repository's mirror, as its code page does

Answers 200 when everything holds, else 503, with Cache-Control: no-store. One feed bringing events is enough for ok; with no feeds followed, feeds pass.

Field
okeverything holds
problemsstrings, below; dead feeds are listed even while ok is true
uptime_secondssince the process started
feeds[]name, url, ok, connected, silent_seconds, stale_after_seconds, events, seq
indexok, error; with --index
mirrorrepo, ok, error, commit (the default branch's), packs (.pack files in the mirror); with ?repo=

A feed never heard from is silent since it was first followed. Its stale_after_seconds is --stale, else 120.

Problems:

  • <feed>: not connected
  • <feed>: no event for <n>s
  • no feed is bringing events
  • index: <error>
  • mirror <repo>: <error>, including ?repo= takes <handle or did>/<name>

Git smart HTTP

Route
GET /<authority>/<repo>/info/refs?service=git-upload-packthe ref advertisement
POST /<authority>/<repo>/git-upload-packa fetch

Any other service is 403 read only: push with knotless:// instead. The Git-Protocol header is passed to git upload-pack, so protocol v0 and v2, --filter and --depth work. A gzip request body (Content-Encoding: gzip) is decompressed; bad gzip is 400. Answers are application/x-git-upload-pack-advertisement or -result, Cache-Control: no-cache, not compressed.

Object API

Under /api/<authority>/<repo>. A failed read of a tree, blob, log, diff or object is 404; a failure to list the refs is 500.

RouteParametersAnswer
GET /refs{refs: [{name, target}], head}
GET /treerev, path{rev, path, entries: [{name, mode, type, sha, size}]}
GET /blobrev, paththe raw bytes, application/octet-stream, ETag the blob's hash
GET /logrev, path, limit (default 50, at most 1000){rev, commits: [{sha, parents, author: {name, email, date}, message}]}
GET /diffbase, headgit diff base...head, text/x-diff; charset=utf-8
GET /object/<sha>the raw object, application/octet-stream, its type in X-Git-Type, ETag, immutable

rev, base and head default to HEAD. An empty one, one starting with -, or one holding a NUL, newline or space is 400 bad revision "<rev>". path is trimmed of slashes. <sha> is 40 or 64 hex characters, else 400 not an object id: <sha>. A tree or blob read at a 40-character hash is public, max-age=31536000, immutable, and otherwise no-cache.

Appview

XRPC queries under /xrpc/. All but getProfile, describeServer and getStats need --index, and without one answer 404 this server keeps no index.

dev.knotless.listRepos

ParameterDefault
qmatched against name and description
runnera DID: only repositories whose record names it in runners
triagera DID: only repositories whose record names it in triagers
topiconly repositories whose record names it in topics
limit50clamped to 1 to 500
cursorfrom the page before

Answers repos (did, name, description?, forkOf?, createdAt, owner), newest first, and cursor when the page was full. Taken-down owners and repositories are left out. Asked as an account, the first page with no runner, triager or topic starts with the private repositories matching q that it may read here, at most limit more, each private: its own, and those it was proven a member of, as it read one or as it was asked after in the background. The answer is then that account's alone.

dev.knotless.searchOwners

ParameterDefault
qrequired; matched against the handle, ignoring case
limit10clamped to 1 to 50

Answers owners, each an owner (did, handle, avatar?) and its repoCount: the accounts owning repositories listRepos lists, those whose handle starts with q first, then the one with the newest repository. Owners are matched by the handles the index keeps; an owner whose handle was never looked up is looked up first, once, and kept.

dev.knotless.describeServer

No parameters. Answers runners, each a did: the one --default-runner names, which a client names in a new public repository's record unless its owner names others. Empty when it names none. It needs no index.

dev.knotless.getForge

ParameterDefault
repothe repository's at:// URI; only its owner's DID is used
keythe repository's key
limit500clamped to 1 to 1000
cursorthe last URI of the page before

Answers entries (uri, author, value), every record naming the repository, sorted by URI, less what moderation removes, and cursor only when more remain. Records about the repository it was transferred from are included when both owners agree on the move.

dev.knotless.listChecks

ParameterDefault
repo, keyas for getForge
refonly checks of this ref
commitonly checks of this commit
pullonly checks of this pull's at:// URI
limit50clamped to 1 to 100
cursorfrom the page before

Answers checks (uri, runner, value), newest startedAt first, and cursor when there may be more. Only the runners the repository record names now count.

dev.knotless.getFeed

ParameterDefault
actoronly what this DID did
filtereveryonefollowing: what the account asking and those it follows did, and anything done on its repositories and those it starred
kindevery kindonly pushes (refs pushed and deleted), pulls or issues (opened and moved between states), comments, stars or repos (created and forked); a kind it does not know answers nothing
limit40clamped to 1 to 100
cursorfrom the page before

Answers events (createdAt, actor {did, handle, avatar?}, repo {did, handle, name}, action) and cursor. action.$type is one of:

$typeFields
dev.knotless.getFeed#createddescription?, forkOf?
dev.knotless.getFeed#pushedref, created, to?, forced?, count, new, commits (sha, message, merge?)
dev.knotless.getFeed#deletedref
dev.knotless.getFeed#openedsubject, title
dev.knotless.getFeed#commentedsubject, title, text, verdict?
dev.knotless.getFeed#changedsubject, title, state
dev.knotless.getFeed#starredsubject, the repository record

A push's actor is the account that pushed: a ref's claim or record that names a pusher the repository lists among its collaborators or runners is theirs, and any other is the owner's. actor and filter=following choose pushes by that account too. Pushes to one ref by one account within an hour of each other are one event. to is the commit the last of them left the ref at, which a page links to as the ref may since have been deleted. A push walks at most 100 commits and lists at most 10; count is what it brought, and new those no other ref had first. forced is a push that left commits behind, as a rebase or a rewrite does: its count and new leave out the commits it brought again, those by the same author at the same time with the same message as one it left behind, so a rebase alone counts none.

changed is a pull or issue whose state changed to open, closed or merged, and nothing else: labels, the merge queue and a pull pointed at another branch are not in the feed.

starred comes only with actor (the stars it gave) or filter=following (the stars of the asker and those it follows). Everyone's feed has none, since anyone can write as many stars as they like. filter=following with no one logged in or no service token answers 401. A commit with more than one parent says merge: true.

dev.knotless.getActorActivity

ParameterDefault
actorthe account's DID
sincea year before nowthe first day counted is the one this time falls on in tz
tzUTCthe IANA time zone whose days are counted, as America/New_York; one it does not know is UTC

Answers days, oldest first, each a date in tz (day, as YYYY-MM-DD) on which the account did something, with how many repositories it made (repos), refs it moved (pushes, one for each claim it is credited with as getFeed credits them, on its repositories or others'), and pulls, issues and comments it wrote; and repos, each of its repositories ever pushed to, by name, with when it last was (pushedAt) and its days (day, pushes, by anyone) since since. An account or repository taken down counts for nothing.

dev.knotless.getProfile

Parameter
actora handle or DID

Needs no index. Answers did, displayName?, description?, pronouns?, links?, avatar?, bot? (true when its dev.knotless.profile says it is a bot); an avatar served here is made absolute from X-Forwarded-Proto (default http) and Host. With an index, also badges?: each badge the account earned (kind, tier from 1 to 4, or 1 for a badge earned once, count, and next, the count the next tier starts at, absent at the top), in the order the pages show them (badges). An account that cannot be reached is 400 {"error": "ProfileNotFound", "message": ...}.

dev.knotless.getStats

No parameters, and no sign-in: what the /stats page shows. days, the last 30 (UTC), oldest first, each with visitors, views and, from the index, repos, pushes, pulls, issues, comments, checks and authors; and network?, as dev.knotless.getUsage has it, absent without an index. Days are zero without --usage. Cached for five minutes (Cache-Control: public, max-age=300). What only operators read, the requests, routes, events, slow statements, failures and crawlers, is dev.knotless.getUsage's (deploy).

Live

The pages follow what happens as it happens as server-sent events (serve). Each answers as the query for the same thing does for the same viewer, a 404 included, lasts ten minutes at most, and ends as the server stops; a browser then asks again by itself.

PathParametersEvents
GET /_live/reporepo: the repository's at:// URIchanged, its data null, a quarter of a second after the index takes in or lets go a record about the repository, once for those that came meanwhile
GET /_live/checkrepo and uri, as dev.knotless.getCheck; the Last-Event-ID header, where the output was had tooutput: what the run printed since, a JSON string, its id the byte where it ends in all the run printed; steps: the steps that ended, as the record has them, each time more do; going: the name of the step going, a JSON string, or null between steps; ended, its data null, once, as the run's steps are over

Moderation

These need --index and --public-url. The admin methods, and dev.knotless.getUsage, take a service token for the method, Authorization: Bearer, from an operator: an account KNOTLESS_ADMIN_DIDS names. With no token, they take the session cookie of an operator logged in to the pages, asking from the site's own origin, as the /admin page does. Without either, from another site, or with a token for another method or server, they answer 401; from anyone else, 403 <did> is not an operator here.

MethodAuthParametersAnswer
POST com.atproto.moderation.createReportservice token, lxm this methodsubject, reasonType, reason?the stored report: id, reasonType, subject, reportedBy, createdAt, reason?
GET dev.knotless.getLogsadmintrace, or any of message, level (that or above), minMs; limit (default 500 for a trace, else 100; clamped to 1 to 500); needs --logslines, oldest first, each {line}, the JSON line as logged; 400 asking for none of them, 404 keeping no logs
GET dev.knotless.listReportsadminlimit (default 50, clamped to 1 to 500), cursor (an id: reports before it)reports, newest first, and cursor when the page is full
POST com.atproto.admin.updateSubjectStatusadminsubject, takedown.appliedsubject, takedown
GET com.atproto.admin.getSubjectStatusadmindid or urisubject, takedown.applied
GET com.atproto.label.queryLabelsuriPatterns, repeatable: a URI, or a prefix ending in *labels
GET com.atproto.label.subscribeLabelscursora WebSocket of labels

Invites

A private repository's owner leaves its runners an invite, and a runner reads its own, each with a service token for the method from their own PDS: knotless-run's --discover finds private repositories so (runner). These need --index and --public-url, as the methods above do, and answer 401 without a token for the method.

MethodAuthParametersAnswer
POST dev.knotless.inviteRunnerservice token, lxm this methodrunner (a DID, else 400), name (a repository name, else 400): the caller's repository{}; an invite already there moves to the end
GET dev.knotless.listRunnerInvitesservice token, lxm this methodlimit (default 100, clamped to 1 to 500), cursor (one this method gave, else 400)the caller's invites after cursor, oldest first, each did, name, createdAt; and cursor, where the page ended, the one given when it is empty

Secrets

An owner leaves a repository's runners secrets, each sealed to one runner's dev.knotless.runnerKey, so this server holds only ciphertext (runner). A repository is named by its owner, the caller, and its key, never its name, so a rename keeps its secrets. Each method takes a service token for itself, from the caller's PDS, or the pages' login, as their writes take it (403 from another site), else 401, and needs --index. The pages seal each value in the browser, to the keys listRunners gives, so this server never sees one.

MethodAuthParametersAnswer
POST dev.knotless.putSecretservice token, lxm this method, or the pages' loginrepo (a key: 1 to 32 letters and digits), runner (a DID), name (a shell variable's name, not starting KNOTLESS_RUN), refs (at most 16 {pattern}, each 1 to 256 bytes), key (a did:key), sealed (1 to 16465 bytes); any else 400{}; one of the same repository, runner and name is replaced
POST dev.knotless.deleteSecretservice token, lxm this method, or the pages' loginrepo; name, runner, or both, to delete only thosedeleted, how many of the caller's went
GET dev.knotless.listSecretsservice token, lxm this method, or the pages' loginowner, reposecrets, by name then runner, each name, runner, refs, key, updatedAt: every one to the owner, with sealed only on those sealed to it; those sealed to the caller, with sealed, to anyone else

A subject is a com.atproto.admin.defs#repoRef or a com.atproto.repo.strongRef, else 400. getSubjectStatus with neither parameter is 400 com.atproto.admin.getSubjectStatus takes a did or a uri.

updateSubjectStatus with takedown.applied: true takes the subject down and emits a !takedown label; with false it restores it and emits the label with neg: true.

queryLabels answers this server's own standing takedowns, at most 250 per pattern, each with ver: 1, src the server's DID and cts the takedown's time. subscribeLabels sends #labels frames in CBOR (header {op: 1, t: "#labels"}, body {seq, labels: [label]}): with a cursor, the labels after it first, then live ones. Labels are unsigned.

Space notifications

A private repository's space's authority, the owner's PDS, tells the server of writes there once asked to (access). Each is signed as the space's owner for the server's #knotless_appview service and this method, else 401; a space not the signer's own, or not a private repository's, is 403. Both need a gateway DID and a server people log in to, else 404.

MethodAuthInputAnswer
POST com.atproto.space.notifyWriteservice token, aud the server's #knotless_appview, lxm this methodspace; the rest is not read{}; the space is synced now, and followed first as its owner if they are logged in here and it is not yet
POST com.atproto.space.notifySpaceDeletedthe samespace{}; the space is forgotten, with every record it gave

Runs as they go

A runner sends the server what its runs print as they print it, and asks it which of its private repositories' spaces were written to (runner), each with a service token for the method from its own PDS, else 401; a token is checked once a minute at most. These need --index and --public-url, as invites do.

MethodAuthInputAnswer
POST dev.knotless.putRunOutputservice token, lxm this methodcheck: a dev.knotless.check record of the caller's, else 400; startedAt?, the run's start, as its record says: another than the run kept started at starts it anew; offset; text?, at most 20,000 bytes; steps?; going?, the step running now, taken with steps; ended?{watched}: whether a page follows the run now; 429 while the server holds 1,024 runs, or 128 of the caller's
GET dev.knotless.awaitSpaceWritesservice token, lxm this methodcursor?cursor, and writes, each space: those written to after cursor of the spaces whose owners invited the caller, as soon as there is one, else none after 25 seconds; with no cursor, or one of another start of the server or past the 4,096 writes it remembers, at once, none, and missed: true

Gateway

These need --public-url, and otherwise answer 404 this server is not a gateway. Each XRPC call carries a service token with the gateway's DID as audience and the method as lxm. Gateway describes each.

RouteInputAnswer
GET /.well-known/did.jsonthe did:web document, with #knotless_gateway, #atproto_labeler and #knotless_appview services
GET /oauth-client-metadata.jsonthe gateway's OAuth client metadata
GET /oauth/callbackthe authorization server's redirecta page saying whether the gateway now writes for you
GET /oauth/bot-client-metadata.jsonthe bots' OAuth client metadata; with --bots
GET /oauth/botaccount, a handle or DID that KNOTLESS_BOT_DIDS namesa redirect to approve a bot's login at its PDS; with --bots
GET /oauth/bot-callbackthe authorization server's redirecta page saying whether the bot is logged in; with --bots
POST dev.knotless.putDelegation{url, state}
GET dev.knotless.getDelegationstate{done}
POST dev.knotless.pushRefsa JSON header line {repo, name, updates: [{dst, src?, force?}]}, then a pack; application/x-knotless-push, at most 512 MiB{results: [{name, error?}]}
POST dev.knotless.createCommit{repo, name, branch, message, author?: {name, email}, base?, changes: [{path, data?, executable?}]}{sha, parent?, changed, batched}

A change with no data deletes its path.

MCP over HTTP

Served with --public-url, to any account the index has not taken down. The server is its own OAuth authorization server, with PKCE.

Route
/mcpthe MCP tools, as streamable HTTP with JSON answers and no sessions; takes a bearer token
GET /.well-known/oauth-protected-resource, /.well-known/oauth-protected-resource/mcpprotected-resource metadata
GET /.well-known/oauth-authorization-serverauthorization-server metadata
POST /mcp/registerclient registration (RFC 7591)
GET, POST /mcp/authorizesign-in, with atproto OAuth at the user's PDS
GET /mcp/oauth/callbackwhere the user's PDS sends them back
POST /mcp/tokentokens
GET /mcp/oauth-client-metadata.jsonthe client metadata it signs users in with

Registrations and tokens are signed, not stored.

Lifetime
Access token1 hour
Refresh token90 days
An account's session, from its last token90 days
Client registration100 years
Sign-in15 minutes
Authorization code5 minutes
Limit
Tool calls running, per account8
Of those, with a wait2
Tool calls started a minute, per account120
Sign-ins waiting on PDSes, from everyone1000

A tool call past an account's limits answers as a tool error, and a sign-in past the server's as a 503 page.

Releases

With --releases <dir> or --releases knotless://<owner>/<name>:

Route
GET /install.shthe install script, text/x-shellscript; charset=utf-8, pointed at --public-url, else at X-Forwarded-Proto (default http) and Host (default 127.0.0.1:2590)
GET /releases/...the directory: latest, and <version>/knotless-<version>-<target>.tar.gz with its .sha256

From a repository, the same paths answer from its releases of the knotless package (packages.md), listed again each minute:

Route
GET /releases/latestthe latest version and a newline; 404 no release with none
GET /releases/<version>/<file>the file, application/octet-stream, read once from the owner's PDS, checked against its record, then kept under the cache directory's serve/releases/ by its SHA-256
GET /releases/<version>/<file>.sha256<sha256> <file> and a newline, from the record

A version or file the releases do not have is 404. A repository that cannot be read, or a file that cannot be read or kept, is 502 with the error.

Images

An OCI registry of the images repositories publish as releases, with knotless publish --oci or a push here (packages.md). An image is named <owner>/<repo>/<package>, or <owner>/<repo> for the package named as the repository, the owner by handle; a tag is a version, or latest, the highest without a pre-release part.

Route
GET /v2/with any bearer token, {} and Docker-Distribution-API-Version: registry/2.0; without one, 401 asking for one: WWW-Authenticate: Bearer realm="<this server>/v2/token",service="knotless"
GET /v2/tokena token: {token, access_token}, anonymous with no credentials, which pulls need; with Basic credentials, a handle (or DID) and an app password, one to push with for an hour, expires_in 3600, made by logging in to their PDS and kept only in memory; a password the PDS refuses is 401
GET, HEAD /v2/<name>/manifests/<tag or digest>the manifest: by tag, the one its release's index.json names; by digest, the file of that digest among the package's releases; as the media type it says (an OCI image manifest if none), with Docker-Content-Digest
GET, HEAD /v2/<name>/blobs/<digest>the blob, application/octet-stream, with Docker-Content-Digest and Content-Length, sent from where it is kept as it is read: a release's, or one the owner pushed and no release holds yet
GET /v2/<name>/tags/list{name, tags}: the versions that are images, highest first, and latest if there is one
POST /v2/<name>/blobs/uploads/begins an upload: 202 with Location and Docker-Upload-UUID; with ?digest=, the body is the whole blob, ended as a PUT ends one
PATCH /v2/<name>/blobs/uploads/<id>the body as the upload's next part: 202 with Range: 0-<last byte>
PUT /v2/<name>/blobs/uploads/<id>?digest=<digest>the body as its last part: checked against the digest, uploaded to the owner's PDS and kept; 201 with Location and Docker-Content-Digest
PUT /v2/<name>/manifests/<digest or version>a manifest, at most 4 MiB of JSON: by digest, kept as a blob; by a version, the release of it, with an index.json naming it and every blob it reaches, an index's manifests and theirs included

Each file is read from the owner's PDS once, checked against its record, and kept by its SHA-256, as the releases are; each package's releases are listed again each minute. Errors are the spec's, {"errors": [{"code", "message"}]}: 404 NAME_UNKNOWN for a name that is not <owner>/<repo>[/<package>], an owner that cannot be found, or a repository that cannot be read, NAME_INVALID for a repository's name knotless does not take, MANIFEST_UNKNOWN, BLOB_UNKNOWN and BLOB_UPLOAD_UNKNOWN; 400 DIGEST_INVALID for an upload ended with no digest or the wrong one (which ends it), or a manifest put by a digest not its own, TAG_INVALID for a tag that is not a version (latest among them), MANIFEST_INVALID and MANIFEST_BLOB_UNKNOWN for a blob or manifest it reaches that was not pushed; 401 UNAUTHORIZED for a push with no token, asking for one with scope="repository:<name>:push,pull"; 403 DENIED for a push by anyone but the owner, or of a version published already; 405 UNSUPPORTED for anything else posted, patched or put; 502 UNKNOWN for what cannot be read, uploaded or kept.

Web data

The pages read what they show from these XRPC queries, as atproto's appviews answer theirs: each has its lexicon under lexicons/dev/knotless, with the views they share in dev.knotless.defs; the pages' TypeScript types are made from them by @atproto/lex, into web/src/lib/lexicons (npm run lex). A repository is named by its record's at:// URI, repo, whose authority may be a handle; an old name, or an owner it moved from, answers under the name it has now.

Query
dev.knotless.listRepos?q=repositories matching q (above)
dev.knotless.searchOwners?q=the owners of repositories whose handle holds q (above)
dev.knotless.getActorRepos?actor=an account's public repositories, each with the language most of it is written in: from the index while a feed is followed, else as its PDS lists them; to the account itself, asking, its private ones too, each private, and to a member asking those it was proven a member of; asked as the account itself, apart, as shared, the private repositories of others it was proven a member of
dev.knotless.getRepo?repo=a repository: its owner, description, links (only http and https ones), topics, default branch, counts, languages, collaborators (the first twenty its record names, besides its owner) and collaboratorCount, private if it is, and archivedAt if its owner archived it; with an asker, viewer: its star, a transfer offered to it, and the labels it may put on the repository's pulls and issues
dev.knotless.listReleases?repo=&package=&limit=&cursor=its releases (packages.md), newest first, 30 a page by default, of one package if given: each with latest when it is its package's latest, image when its files are an OCI layout's, its notes as Markdown and as html, and its files' names, sizes and SHA-256; and every package it publishes, by name, with its latest version, how many releases it has and whether it is an image. None for a private repository or without an index
dev.knotless.getTree?repo=&ref=&path=a folder, its README and the newest checks of its last commit
dev.knotless.getLastCommits?repo=&ref=&path=the newest commit to change each entry of a folder, and at the root, how many commits the ref's history holds
dev.knotless.getFile?repo=&ref=&path=a file, highlighted or rendered
dev.knotless.getBlame?repo=&ref=&path=who last changed each line of a text file, as git blame finds it: its text, highlighted; its lines in runs (hunks), each with its commit and, where the commit's parent had the file, previous, that parent and the file's path there; and each commit once. 400 for a binary or a file over 256 KiB
dev.knotless.listCommits?repo=&ref=&path=&limit=&cursor=&subjectOnly=history, 50 commits a page by default, with the newest checks of each; with subjectOnly, each message only to its first line
dev.knotless.getCommit?repo=&sha=a commit and its diff
dev.knotless.listBranches?repo=&limit=&cursor=&commits=the branches, the default first, 100 a page by default; with commits, each with its tip and that commit's newest checks
dev.knotless.listTags?repo=&limit=&cursor=&commits=the tags that name a commit, the newest commit first, 100 a page by default; with commits, each with its commit and that commit's newest checks
dev.knotless.listItems?repo=&kind=&state=&q=&limit=&cursor=the open or closed pulls or issues that the filter q matches (collaboration.md), review-requested:@me the asker, each with draft for a draft pull and the reviewers it asks, 50 a page by default, how many of each, and the repository's labels and the items' authors, for a filter to name
dev.knotless.getThread?repo=&uri=&filesOnly=&firstRows=a pull or an issue and everything said on it, each comment with its uri, Markdown body and, for a review, the commit it looked at; on a pull's lines, its line (path, side, line, startLine, the lines it names as they were, at most 20, and outdated once that side of the file has changed), the review it went with and resolved; an answer's reply, the comment starting its thread; for a pull, reviews, where each reviewer stands (outdated once the open pull has moved past the head it looked at, maintainer for the owner or a collaborator), and in changes the head it is at; for a merged pull, revertedBy ({uri, title}), the revert whose merge undid it; for an issue, mentionedBy, the pulls on the repository whose title or body names it, oldest first, each its item and closes where it names it after a closing keyword; in each html, a bare at:// URI of a pull or issue on the repository, or a link to one, as a link to its page, a bare one showing its title; for a pull of a public repository, the minutes each runner counts for its owner (dev.knotless.minutes); with an index, reactions on the item and on each comment, by content in GitHub's order, each naming the asker's own reaction when it has an asker; and with an asker, what it may do (viewer: setState, hide, revert for a merged pull, and the labels it may set); with filesOnly, a pull's changed files without their rows, and with firstRows too, the rows of the first files, up to 10 files or 1500 lines, passing over any that changes more than 400
dev.knotless.getPullDiff?repo=&uri=&paths=the files of a pull's diff that paths names (repeated, 1 to 50), rows and all: what the pull page fetches as the reader comes to each file
dev.knotless.getTemplate?repo=&kind=&base=the template a new pull (at base) or issue is asked to follow, .knotless/<kind>.md, as knotless template reads it; {} with none, 400 for a kind that is neither
dev.knotless.getCompare?repo=&base=&head=&branch=the repositories a pull can come from (heads: this one, then its forks), and with branch what it brings in from head (this repository if not given) into base (the default branch if not given), as a pull's changes: 404 for a branch on neither
dev.knotless.getReactions?repo=&uri=the reactions to a pull or an issue and to each comment on it, as {subjects: [{subject, reactions: [{content, count, reaction}]}]}, leaving out subjects none reacted to: 404 with no index
dev.knotless.getQueue?repo=the merge queues, what they ejected, and what they landed last
dev.knotless.listRuns?repo=&workflow=&limit=&cursor=the runs of the repository's workflows, or of one, newest first, 30 a page by default and at most 100, each with the ref, pull or batch it ran on; and the workflows on the default branch, those that run only when asked among them
dev.knotless.getCheck?repo=&uri=a check, its steps and the end of its log
dev.knotless.getCheckImage?repo=&uri=&cid=a picture a check names
dev.knotless.getAvatar?actor=&cid=a dev.knotless.profile picture
dev.knotless.getAttachment?repo=&uri=&cid=a blob a pull, issue or comment attaches, such as a picture its body shows, from its author's PDS or their repo in a private repository's space; 404 for one its record does not attach
dev.knotless.getViewerwho is asking (actor), the other accounts the browser is logged in to (accounts), whether this server offers a login (login, only with --public-url), admin: true for an operator, delegated: true when its gateway writes for the asker, organization: true when the asker is an organization, reauth: true when their login to the pages asked for less than the pages ask for now, redelegate: true when the gateway writes for them under a grant that asked for less than it does now, and spaces, whether the asker's PDS has spaces for private repositories, once this server has listed theirs (as they log in)
dev.knotless.getFollowers?actor=&limit=&cursor=the accounts following an account, the latest first, 50 a page by default: 404 with no index, ProfileNotFound for an unknown account
dev.knotless.getFollows?actor=&limit=&cursor=the accounts an account follows, the same way
dev.knotless.listThemes?actor=&q=&uris=&limit=&cursor=dev.knotless.theme records, everyone's or actor's, the latest first, as {themes, cursor}; with q, everyone's whose name starts with it in any case, by name, with no cursor; with uris, repeated, 50 at most, those it names that there are, in its order, with no cursor and the rest passed over: each {uri, creator, name, palette, createdAt}, its palette only the colours that are #rrggbb, and one with no name or no colour it must give left out, as is an account taken down. ProfileNotFound for no such actor, 400 for over 50 uris, 404 with no index
dev.knotless.listSteps?repo=&q=&limit=&cursor=the shared steps the index has found (serve.md), everyone's or one repository's (repo, its dev.knotless.repo URI, by handle or DID), by name, 50 a page by default, those whose name starts with q if given, as {steps, cursor}: each at its latest version, the highest that is not a pre-release, as {uri, name, repo, description, version, secretCount, useCount, createdAt}, useCount how many public repositories pin it in the .knotless/uses.lock of their default branch. One taken down, or of an account or repository taken down, is left out. 400 for a repo that is not a repository's URI, 404 for no such repository or with no index
dev.knotless.getStep?repo=&name=one of those, as listSteps gives it, with its latest version's commit, its file's path, the uses that takes its major version (<handle>/<repo>/<name>@1, or @0.3 before 1.0.0), its inputs, each {name, description, required, default}, the commands it needs and the secrets it asks for, each {name}, its versions, each {uri, version, commit, createdAt}, the highest first, and usedBy, at most 30 of the repositories pinning it. 404 for no such repository or step, or with no index
dev.knotless.listNotifications?limit=&cursor=&kind=what others did that concerns the asker, the latest first, a pull or an issue opened on any repository it looks after (its own, those naming it a collaborator, its organizations'), 50 a page by default, each isRead by seenAt, and a review's verdict; reason mention for a pull, issue or comment whose facets name the asker, dated by the edit that first named the asker, and each record once, for its other reason when it has one the asker hears of, then with mentioned: true; reasonSubject what each is about, the pull or issue a comment is on, else the pull or issue itself, or for a star the repository's record; lineComments, how many line comments a review was sent with; review-requested for a pull, not a draft, that asks the asker to review it; reason invite for an organization's invitation the asker has yet to answer, while it stands, with the organization as author, its dev.knotless.orgMember record of the asker as uri, the role it offers and no repo; reason checks-failed for the asker's pull whose checks failed at the latest run of each workflow, one for each head and base, with a runner the repository names as author, a failed check as uri, the pull as reasonSubject and checks, each {workflow, status, check}; reason ejected for the asker's pull the merge queue took out, with its runner's dev.knotless.queue record as uri and the checks that failed as checks. A pull, an issue, a comment, a mention in one, or a review request has text, the start of what it says as Markdown, at most 1,000 characters; a review request has reviewed: true once the asker has approved the pull or asked for changes since it was asked; each about a pull or an issue has subjectState, open, closed or merged, as its page says, and checks-failed and ejected are left out once their pull has merged or closed. kind narrows them to one of mentions (with comments heard of for another reason that name the asker), reviews (review requests, and comments with a verdict), comments (those without), pulls-and-issues, checks (checks-failed and ejected) or stars-and-follows; an unknown kind answers none: 401 AuthenticationRequired with no asker, 404 with no index
dev.knotless.getUnreadCounthow many of those are new, counting at most 100
dev.knotless.getPreferencesthe asker's preferences, kept by the appview, as {preferences}, each by its $type: #appearancePref, {theme, palette, source, night}, once it chose one, night the same three for while the system is dark, if it chose two; #themesPref, {themes}, the themes people made that it added, each {uri, name, creator, palette, addedAt}, once it added one; and #notificationPref always, which it hears of: reasons, each listNotifications reason (from its lexicon's knownValues, so a new one is listed) with enabled, and followedOnly, whether only of what accounts it follows did. Until it says, all, from anyone. listNotifications and getUnreadCount leave out the rest: 401 with no asker, 404 with no index
dev.knotless.getOwnProfilethe asker's dev.knotless.profile alone, as its settings show it (displayName, description, pronouns, links, avatar's address, bot), with nothing of its Bluesky profile: 401 with no asker
dev.knotless.getAccountDatawhat knotless data the account logged in to the pages holds, read from its PDS: {did, collections, repos, spaces}, each dev.knotless.* collection it writes in as {collection, count}, its repositories as {name, private, collaborators, members?} (members, for a private one, the accounts that read it but its runners), and the spaces of its private repositories; the pages' cookie only, 401 otherwise
dev.knotless.getEmails{count}, how many addresses the asker's dev.knotless.emails claims: only hashes are kept, so they cannot be listed. 401 with no asker
dev.knotless.listBlocksthe asker's blocks, the latest first, from its own repo: each uri, subject (an actor), repo when it is from one repository only, and createdAt. Those naming a private repository live in its space and are not listed. 401 with no asker
dev.knotless.getRepoSettings?repo=what the owner sets: description, links, default branch, protected branches, labels, collaborators, triagers, gateway, an offer to move it (transfer), storage, as knotless storage counts it, the gateway the owner delegated to from these pages (delegated), and for a private one, to its owner alone, the members who read but do not push (readers)
dev.knotless.getOrg?actor=an organization's members, as the index has its dev.knotless.org and dev.knotless.orgMember records and its members' dev.knotless.orgAcceptance ones: {org, organization, inside, gateway?, members}, gateway the one its public dev.knotless.org record names, each member {actor, role, standing, invitedAt, expiresAt?, joinedAt?}, the first invited first, standing one of invited, lapsed, public and hidden. To anyone, its members in public; to the organization and its members, logged in or with a service token (inside), its invitations and hidden members too, from its space, which this server keeps an index of; to an account it invited, its own entry. organization: false, with no members, for an account that is none; ProfileNotFound for an unknown account
dev.knotless.getActorStars?actor=the public repositories an account starred, newest first, each with its language, starCount and forkCount: 404 with no index, ProfileNotFound for an unknown account
dev.knotless.getMergeStatus?repo=&pull=whether an open pull can merge now, as knotless merge --check says (below): 401 with no asker, 400 for a pull not open, 404 for none
dev.knotless.listWorkflows?repo=&ref=the workflows at a ref (the default branch), in file name order, and what runs each there: push, pull, asked (its owner may ask for it there by name), error; 404 for no such ref
dev.knotless.listRunners?repo=the runners the repository names, as accounts, for a public repository the minutes each counts for its owner (dev.knotless.minutes), and the key each publishes to seal secrets to (dev.knotless.runnerKey), as keys, {runner, key}

getRepo's answer has starCount, how many accounts starred it, when the server keeps an index and the repository is public, and viewer.star, the asker's star, when it has an asker. With an index, a public repository that has published releases has releases: how many (count), the release to show under its name (release: the latest of the package named as the repository, else the newest that is its package's latest, or the newest of all when each is a pre-release), and its packages, as listReleases gives them. getProfile's has followersCount and followsCount when the server keeps an index, and viewer.following, the asker's follow, and viewer.blocking, its block from all its repositories, when it has an asker; and orgs, the organizations it is a member of in public, by actor, and to the account itself, asking, its hidden memberships too, when it has any; and organization: true when it is an organization, which its page then shows the members of.

Logging in and stars

The pages log in and star through these, which exist only on a server with --public-url, else 404. Each POST but the callback is refused 403 not from this site unless its Origin is the site's own.

Route
GET /oauth/web-client-metadata.jsonthe pages' OAuth client metadata
POST /oauth/login{handle, back}: {url}, the PDS's page to approve on, and a knotless_login cookie for 15 minutes; 400 No account answers to <handle>., 403 <handle> is taken down here. Or {create: true, back}: the same, the page being the sign-up page of the PDS --sign-up-pds names, and the account made there logged in as it comes back
GET /oauth/web-callbackwhere the PDS sends the browser back: on to back (a path on the site, else /), setting knotless_session for 90 days if approved, and keeping the account it replaces, if any, in knotless_accounts; 400 for a login this browser did not start
POST /oauth/switch{did}: makes that account, one knotless_accounts holds, the one in use, keeping the one before in its place; 400 This browser is not logged in to that account.
POST /oauth/logout{}: revokes the session, refuses every earlier cookie for the account, and goes on as the other account used last, or clears the cookie if there is none
POST /xrpc/dev.knotless.starRepo{repo}: stars it as the account logged in, answering {starCount, star}
POST /xrpc/dev.knotless.unstarRepo{repo}: takes the account's star back, answering {starCount}
POST /xrpc/dev.knotless.updateSeen{seenAt}: keeps that the asker saw its notifications up to then, unless it saw later ones already, answering the {seenAt} kept; with a service token too, and with the cookie only from the site; 400 for a seenAt that is no datetime
POST /xrpc/dev.knotless.followActor{subject}, a DID: follows it as the account logged in, answering {followersCount, following}; 400 for the account itself, ProfileNotFound for no account
POST /xrpc/dev.knotless.unfollowActor{subject}: takes the account's follows of it back, answering {followersCount}
POST /xrpc/dev.knotless.createTheme{name, palette}: writes a dev.knotless.theme as the account logged in, and indexes it, answering it as listThemes does; 400 for a name empty or over 64 characters, a scheme not light or dark, a colour not #rrggbb, none of background, text, primary and accent, or a text that would not read at 4.5 to 1 on what it is drawn on, each named, as textMuted on background is 1.2 to 1
POST /xrpc/dev.knotless.deleteTheme{uri}: deletes one of the account's own themes, answering {}; 400 for one that is not its own
POST /xrpc/dev.knotless.updateProfile{displayName, description, pronouns, links, removeAvatar, bot}, each optional: changes the account's dev.knotless.profile as knotless profile does, a field given empty taken out, bot true saying the account is a bot and false that it is not, answering what it holds now, as getOwnProfile; 400 for a field too long or a link that is no http or https address
POST /xrpc/dev.knotless.uploadAvatarthe picture as the whole body, a PNG, JPEG, WebP or GIF: uploads it, made to fit as collaboration.md says, and names it in the profile, answering as updateProfile; 400 for none, or another kind
POST /xrpc/dev.knotless.addEmail{email}: claims it, as knotless email add, answering {changed, count}; 400 for one with no @
POST /xrpc/dev.knotless.removeEmail{email}: takes the claim back, answering the same
POST /xrpc/dev.knotless.blockActor{subject, repo}, a handle or DID, and optionally the at:// URI of one of the account's own repositories: blocks it from all of them or from that one, answering {subject, blocking}; 400 for the account itself, another's repository or a URI that is no repository's
POST /xrpc/dev.knotless.unblockActor{subject, repo}: takes those blocks back, answering the same
POST /xrpc/dev.knotless.putPreferences{preferences}: each takes the place of its kind whole, and the rest stay, answering all of them as getPreferences. A #notificationPref's reasons not named are on, and followedOnly absent is false; an #appearancePref with no theme follows the system, and logging in to the pages copies its theme to the browser's theme cookie, and for custom, its palette to the palette cookie, and its night's to theme-night and palette-night. A #themesPref holds 50 at most, each a copy, so choosing a theme leaves it be. With a service token too, and with the cookie only from the site; 400 for a kind there is none of, a reason there is none of, a theme not named in a-z, 0-9 and - (64 at most), custom with no palette, a palette not as its lexicon says (its contrast is not checked here), a source that is no dev.knotless.theme, or an added theme whose uri is no dev.knotless.theme, whose palette is not as its lexicon says, whose name is over 64 characters, whose creator is no DID or whose addedAt is no time
POST /xrpc/dev.knotless.createRepo{name, description?, defaultBranch?, private?}: creates a repository in the account logged in, public or with private a space of its own, as knotless create [--private] does, answering {uri, name}; 400 for a name no repository can have, one the account has already, or a private one on a PDS without spaces
POST /xrpc/dev.knotless.updateRepo{repo, description?, links?, topics?, defaultBranch?}: as knotless describe, answering {description, links, topics, defaultBranch} as they now stand; 400 for a link that is not an http or https address, or more than five, and for a topic that is not letters, digits and dashes, or more than twenty
POST /xrpc/dev.knotless.renameRepo{repo, name}: as knotless rename, answering {uri, name}
POST /xrpc/dev.knotless.compactRepo{repo}: as knotless compact, answering {packs, folded, claims}
POST /xrpc/dev.knotless.transferRepo{repo, to?, name?}: offers it to the account to under name (its own if absent), or with no to withdraws the offer, as knotless transfer, answering {transfer}, where it is offered now
POST /xrpc/dev.knotless.archiveRepo{repo, unarchive?}: archives it, making it read only, or with unarchive makes it writable again, as knotless archive, answering {archivedAt} while it is archived; one already as asked is 400
POST /xrpc/dev.knotless.acceptTransfer{repo}: as knotless accept, by the account it is offered to, answering {uri, name} where it is now
POST /xrpc/dev.knotless.deleteRepo{repo}: as knotless delete, answering {uri}
POST /xrpc/dev.knotless.deleteAccountData{did, limit?}, did the account asking: with the pages' cookie, deletes up to limit (400 by default) of its knotless records and spaces with the session kept for it, as knotless delete-data does, answering {deleted, done}; asked again, it carries on. Once done, the server has forgotten the account, as one deleted at its PDS, keeping takedowns and reports, ended its sessions, the pages' and MCP's, and logged the browser out. With a service token it deletes nothing, and forgets the account once its PDS holds no knotless record, or answers 400 RecordsRemain. 403 Forbidden for a did not the asker's
POST /xrpc/dev.knotless.createBranch{repo, name, from?}: as knotless branch, answering {name, sha}
POST /xrpc/dev.knotless.deleteBranch{repo, name}: as knotless branch --delete, answering {name}
POST /xrpc/dev.knotless.renameBranch{repo, name, to}: as knotless branch --rename, answering {name}, the new one
POST /xrpc/dev.knotless.protectBranch{repo, branch, merges?, queue?, reviews?: {approvals?, codeOwners?, dismissStale?}}: as knotless protect, answering {rules}
POST /xrpc/dev.knotless.unprotectBranch{repo, branch}: as knotless unprotect, answering {rules}
POST /xrpc/dev.knotless.defineLabel{repo, name, color?, description?, exclusive?}: as knotless labels add, answering {labels}
POST /xrpc/dev.knotless.removeLabel{repo, name}: as knotless labels remove, answering {labels}
POST /xrpc/dev.knotless.shareRepo{repo, with, write?}: as knotless share --write, or with write: false as knotless share, letting them only read a private repository, answering {collaborators}; 400 to share for writing before delegating, or a public one for reading
POST /xrpc/dev.knotless.delegateWrites{}: starts delegating the account's writes to this server's gateway, as knotless delegate does, answering {url, state}, the page where the account approves the gateway's grant
POST /xrpc/dev.knotless.awaitDelegation{state}: waits up to 25 seconds for that approval, answering {done: false} to ask again, or {done: true, gateway}; 400 for a refused grant or a state not started
POST /xrpc/dev.knotless.inviteToOrg{org, member, role?}, member a DID or handle: as knotless org add, by the organization or one of its admins, answering {}. The account's first invitation makes it an organization, through the gateway it delegated to, or this server's when its gateway writes for it; 400 for anyone else, or before delegating
POST /xrpc/dev.knotless.removeFromOrg{org, member}: as knotless org remove, taking a member out or withdrawing an invitation, answering {}
POST /xrpc/dev.knotless.setOrgMembership{org, visibility}, public or hidden: as knotless org join, joining as the account logged in, or showing or hiding its membership, answering {}
POST /xrpc/dev.knotless.endOrgMembership{org}: as knotless org leave, leaving or declining an invitation, answering {warning?}, set when the gateway did not take them out
POST /xrpc/dev.knotless.deleteOrg{}: as knotless org delete --yes, ending the organization the account logged in is, answering {}
POST /xrpc/dev.knotless.unshareRepo{repo, with}: as knotless unshare, answering {collaborators}
POST /xrpc/dev.knotless.setTriager{repo, who, scopes}: as knotless triagers add, answering {triagers}
POST /xrpc/dev.knotless.removeTriager{repo, who}: as knotless triagers remove, answering {triagers}
POST /xrpc/dev.knotless.forkRepo{repo, name?}: forks it into the account logged in, under its own name or name, as knotless fork does, answering {uri, name}; 404 for no such repository, 400 for a name the account has already
POST /xrpc/dev.knotless.uploadImage?repo=a picture as the whole body, a PNG, JPEG, WebP or GIF: uploads it to the account logged in, made to fit, to show in a pull, issue or comment on repo, answering {blob, url, width, height}, the last two the size it is drawn at in CSS pixels (collaboration.md); 400 for anything else. Its PDS keeps it once a write below attaches it
POST /xrpc/dev.knotless.createIssue{repo, title, body?, images?, labels?}: opens an issue as the account logged in, answering {uri}, with labels (each {name}) put on as labelItem would; 400, with the template, for a body that follows none of the repository's, and, before anything is written, for a label the account may not put on
POST /xrpc/dev.knotless.createPull{repo, base, head, branch, title, body?, images?, draft?, labels?, reviewers?}: opens a pull to merge branch of head, this repository or a fork, into base, as a draft with draft, answering {uri}, with labels put on as createIssue does and reviewers (each {did}) asked to review it; 400 for a branch on neither, a body that follows none of the template, or a label the account may not put on
POST /xrpc/dev.knotless.createComment{repo, subject, body, verdict?, images?, lines?}: comments on a pull or issue as the account logged in, or with verdict (approve, changes) reviews a pull; lines, each {path, side?, line, startLine?, body}, comment on the pull's diff, as the review's when it has a body or verdict. A comment as subject answers it in its thread. Answers the first it wrote's {uri}; 400 for a comment that says nothing
POST /xrpc/dev.knotless.editItem{repo, subject, title?, body?, images?, draft?}: changes the account's own pull, issue or comment, answering {uri}; draft, for a pull alone, makes it a draft or with false marks it ready. images, each {image, width?, height?} with a blob and size uploadImage gave, are attached to what the body links to (collaboration.md). It, createIssue, createPull and createComment write each @handle of the title and body that resolves as a facet naming its DID (collaboration.md)
POST /xrpc/dev.knotless.deleteComment{repo, subject}: deletes the account's own comment or review, answering {uri}
POST /xrpc/dev.knotless.setItemState{repo, subject, state}: closes (closed) or reopens (open) a pull or issue, or resolves (resolved) or opens (open) the thread a comment on a pull's lines starts, answering the state record's {uri}
POST /xrpc/dev.knotless.labelItem{repo, subject, label, removed?}: puts a label on, or takes it off, answering the {labels} it carries now
POST /xrpc/dev.knotless.hideItem{repo, subject, hidden?}: as the owner, hides a pull, issue or comment, or with hidden: false shows it again, answering {hidden}
POST /xrpc/dev.knotless.renderMarkdown{text, repo?}: the HTML the pages show text as once posted, relative links inside the repository repo names and each @handle a link to its profile, answering {html}; needs no login, and 400 for text past 20000 bytes, a body's longest
POST /xrpc/dev.knotless.createReport{subject, reason, text?}: reports a handle, knotless:// URL or at:// URI to the moderation service, answering {reportId}
POST /xrpc/dev.knotless.addReaction{repo, subject, content}: reacts to a pull, issue or comment the repository's pages show, as the account logged in, answering {subject, reactions}; 400 for a content not among the eight, 404 NotFound for a subject the pages do not show
POST /xrpc/dev.knotless.removeReaction{repo, subject, content}: takes the account's reactions of that content to it back, answering {subject, reactions}

Changing the profile, emails and blocks needs the pages' cookie, as starring does, and writes with the session kept for the account. A change to the profile, or a block from all repositories, is indexed at once, so the next page shows it.

updateRepo, renameRepo, compactRepo, transferRepo, archiveRepo, deleteRepo, protectBranch, unprotectBranch, defineLabel, removeLabel, shareRepo, unshareRepo, setTriager and removeTriager are the owner's alone: anyone else logged in is 403 Forbidden, whatever the pages show them. createBranch, deleteBranch and renameBranch push as the account logged in, as knotless branch does: a collaborator through the gateway, which refuses anyone else. getRepo's viewer.transfer names where the repository is offered, when it is offered to the asker.

Starring, following, reacting, writing on pulls and issues and changing a repository need the pages' cookie: a service token, or none, is 401 AuthenticationRequired. What the knotless crate refuses to write, as a comment on what is no pull or issue, an edit of someone else's comment, a label its writer may not set or a default branch the repository does not have, is 400 InvalidRequest in its words. Starring a private repository is 404 NotFound, as a star is public; a reaction on one is written in its space.

Landing and checks

A pull's page lands it, and the checks pages run its workflows, through these writes, as the account logged in, with the session the server keeps for it, through the same code as the command line. Each needs the pages' cookie as starring does (401 without it, 403 from another site), and answers a refusal 400 InvalidRequest in the command's words: a pull that conflicts, checks that have not passed, an account that may not. A private repository's are made and read as the member logged in.

Route
POST /xrpc/dev.knotless.mergePull{repo, pull, keepBranch}: merges it as knotless merge does, waiting for no checks, answering {commit, deleted, closed: [{uri}]}; 400 into a branch that takes pulls only through the merge queue, which queuePull queues it into
POST /xrpc/dev.knotless.revertPull{repo, pull, reason}: as the owner or a collaborator, reverts a merged pull as knotless revert does, answering {uri, branch, commit}, uri the pull it opens, which is indexed at once; 400 on a pull not merged, or whose undoing conflicts. getThread's viewer.revert says who may
POST /xrpc/dev.knotless.queuePull{repo, pull}: puts it in its branch's merge queue, as knotless merge --queue, answering {base, position, of}; 400 while the queue lands through this server's gateway and the owner has not delegated writes to it
POST /xrpc/dev.knotless.leaveQueue{repo, pull}: takes it out, as knotless queue --leave, answering {}
POST /xrpc/dev.knotless.retargetPull{repo, pull, base, head}: points it at other branches, either or both, as knotless retarget, answering {}
POST /xrpc/dev.knotless.requestRerun{repo, pull} or {repo, ref}, with workflow, and at a ref env: [{name, value}]: as the owner, asks the runners to run them again, as knotless rerun, answering {uri}, the dev.knotless.rerun record
POST /xrpc/dev.knotless.addRunner{repo, runner}, a handle or DID: as the owner, names it a runner, as knotless runners add, answering {runners, warning}, the warning when a private repository could not tell it
POST /xrpc/dev.knotless.removeRunner{repo, runner}: as the owner, names it no longer, answering {runners}

getMergeStatus answers someone asking, by the cookie or a service token, from what they may read of the repository, a private one read as them, as listWorkflows and listRunners are: state (clean, conflicting with conflicts: [{path}], blocked by its checks or its reviews, or draft, whatever else holds, until its author marks it ready), and reason, why not, in a merge's words, and that the asker may not merge it when it may not; checks (passed, failed, running, pending, or none when the repository names no runners or has no workflow that runs on the pull); method (fastForward, merge, squash into a linear branch); deletes, the branch it would delete; base and head, the commits; and may, what the asker's part in the repository lets it do, not whether that would work now: merge and queue (owner and collaborators, the queue only with runners), force (owner), base (those who may close it), head (its author), rerun (owner, with runners); and queueBlocked: "undelegated" when the asker may queue it but no runner the repository names may push, so its queue lands through this server's gateway, and the owner has not delegated writes to it; and queueOnly: true when its branch takes pulls only through the merge queue, where mergePull refuses it and the pages queue it with queuePull; reviews (passed, blocked, or none when its branch asks for none), reviewsMissing: [{text}], what they lack, reviewsUncounted: [{text}], the approvals that do not count and why, and codeOwnersWarnings: [{text}], the CODEOWNERS lines skipped, as knotless merge --check prints them. A merge, queue or retarget is indexed at once, so the page after shows it.

ref and path together are split at the longest branch or tag name that starts them, else after the ref's first segment. The pictures are sent as they are, never as a page. Served beside them, not as queries:

Route
/<authority>/<repo>/raw/<rev>/<path>a file's bytes
/<authority>/<repo>/releases/download/<package>/<version>/<file>a file of a release the index has, application/octet-stream, Content-Disposition: attachment with its name, Content-Length, its SHA-256 as ETag (a matching If-None-Match is 304), read once from the owner's PDS, checked against its record and kept, as /releases/ keeps its own (packages.md); not compressed. One the index does not have, taken down, or of a private repository is 404, and one that cannot be read 502
/card.png, /<authority>/<repo>/card.png, /<authority>/<repo>/<pull or issue>/<did>/<rkey>/card.pngsocial cards, 1200×630 PNG

These routes but the release files are compressed (gzip or br). Answers are Cache-Control: no-cache, even at a commit hash, and a failure no-store. A raw file, release file or card asked for by an old name answers 307 to the same path under the new name, and the new owner if it moved, keeping the query.