chadtmiller.com/knotless
git that lives in your Atmosphere account
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.
| Status | When |
|---|---|
| 400 | a bad parameter, or a bad repository name |
| 401 | a missing or bad service token |
| 403 | a 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) |
| 502 | the 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 | |
|---|---|
ok | everything holds |
problems | strings, below; dead feeds are listed even while ok is true |
uptime_seconds | since the process started |
feeds[] | name, url, ok, connected, silent_seconds, stale_after_seconds, events, seq |
index | ok, error; with --index |
mirror | repo, 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>sno feed is bringing eventsindex: <error>mirror <repo>: <error>, including?repo= takes <handle or did>/<name>
Git smart HTTP
| Route | |
|---|---|
GET /<authority>/<repo>/info/refs?service=git-upload-pack | the ref advertisement |
POST /<authority>/<repo>/git-upload-pack | a 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.
| Route | Parameters | Answer |
|---|---|---|
GET /refs | {refs: [{name, target}], head} | |
GET /tree | rev, path | {rev, path, entries: [{name, mode, type, sha, size}]} |
GET /blob | rev, path | the raw bytes, application/octet-stream, ETag the blob's hash |
GET /log | rev, path, limit (default 50, at most 1000) | {rev, commits: [{sha, parents, author: {name, email, date}, message}]} |
GET /diff | base, head | git 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
| Parameter | Default | |
|---|---|---|
q | matched against name and description | |
runner | a DID: only repositories whose record names it in runners | |
triager | a DID: only repositories whose record names it in triagers | |
topic | only repositories whose record names it in topics | |
limit | 50 | clamped to 1 to 500 |
cursor | from 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
| Parameter | Default | |
|---|---|---|
q | required; matched against the handle, ignoring case | |
limit | 10 | clamped 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
| Parameter | Default | |
|---|---|---|
repo | the repository's at:// URI; only its owner's DID is used | |
key | the repository's key | |
limit | 500 | clamped to 1 to 1000 |
cursor | the 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
| Parameter | Default | |
|---|---|---|
repo, key | as for getForge | |
ref | only checks of this ref | |
commit | only checks of this commit | |
pull | only checks of this pull's at:// URI | |
limit | 50 | clamped to 1 to 100 |
cursor | from 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
| Parameter | Default | |
|---|---|---|
actor | only what this DID did | |
filter | everyone | following: what the account asking and those it follows did, and anything done on its repositories and those it starred |
kind | every kind | only 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 |
limit | 40 | clamped to 1 to 100 |
cursor | from the page before |
Answers events (createdAt, actor {did, handle, avatar?}, repo
{did, handle, name}, action) and cursor. action.$type is one of:
$type | Fields |
|---|---|
dev.knotless.getFeed#created | description?, forkOf? |
dev.knotless.getFeed#pushed | ref, created, to?, forced?, count, new, commits (sha, message, merge?) |
dev.knotless.getFeed#deleted | ref |
dev.knotless.getFeed#opened | subject, title |
dev.knotless.getFeed#commented | subject, title, text, verdict? |
dev.knotless.getFeed#changed | subject, title, state |
dev.knotless.getFeed#starred | subject, 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
| Parameter | Default | |
|---|---|---|
actor | the account's DID | |
since | a year before now | the first day counted is the one this time falls on in tz |
tz | UTC | the 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 | |
|---|---|
actor | a 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.
| Path | Parameters | Events |
|---|---|---|
GET /_live/repo | repo: the repository's at:// URI | changed, 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/check | repo and uri, as dev.knotless.getCheck; the Last-Event-ID header, where the output was had to | output: 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.
| Method | Auth | Parameters | Answer |
|---|---|---|---|
POST com.atproto.moderation.createReport | service token, lxm this method | subject, reasonType, reason? | the stored report: id, reasonType, subject, reportedBy, createdAt, reason? |
GET dev.knotless.getLogs | admin | trace, or any of message, level (that or above), minMs; limit (default 500 for a trace, else 100; clamped to 1 to 500); needs --logs | lines, oldest first, each {line}, the JSON line as logged; 400 asking for none of them, 404 keeping no logs |
GET dev.knotless.listReports | admin | limit (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.updateSubjectStatus | admin | subject, takedown.applied | subject, takedown |
GET com.atproto.admin.getSubjectStatus | admin | did or uri | subject, takedown.applied |
GET com.atproto.label.queryLabels | uriPatterns, repeatable: a URI, or a prefix ending in * | labels | |
GET com.atproto.label.subscribeLabels | cursor | a 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.
| Method | Auth | Parameters | Answer |
|---|---|---|---|
POST dev.knotless.inviteRunner | service token, lxm this method | runner (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.listRunnerInvites | service token, lxm this method | limit (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.
| Method | Auth | Parameters | Answer |
|---|---|---|---|
POST dev.knotless.putSecret | service token, lxm this method, or the pages' login | repo (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.deleteSecret | service token, lxm this method, or the pages' login | repo; name, runner, or both, to delete only those | deleted, how many of the caller's went |
GET dev.knotless.listSecrets | service token, lxm this method, or the pages' login | owner, repo | secrets, 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.
| Method | Auth | Input | Answer |
|---|---|---|---|
POST com.atproto.space.notifyWrite | service token, aud the server's #knotless_appview, lxm this method | space; 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.notifySpaceDeleted | the same | space | {}; 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.
| Method | Auth | Input | Answer |
|---|---|---|---|
POST dev.knotless.putRunOutput | service token, lxm this method | check: 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.awaitSpaceWrites | service token, lxm this method | cursor? | 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.
| Route | Input | Answer |
|---|---|---|
GET /.well-known/did.json | the did:web document, with #knotless_gateway, #atproto_labeler and #knotless_appview services | |
GET /oauth-client-metadata.json | the gateway's OAuth client metadata | |
GET /oauth/callback | the authorization server's redirect | a page saying whether the gateway now writes for you |
GET /oauth/bot-client-metadata.json | the bots' OAuth client metadata; with --bots | |
GET /oauth/bot | account, a handle or DID that KNOTLESS_BOT_DIDS names | a redirect to approve a bot's login at its PDS; with --bots |
GET /oauth/bot-callback | the authorization server's redirect | a page saying whether the bot is logged in; with --bots |
POST dev.knotless.putDelegation | {url, state} | |
GET dev.knotless.getDelegation | state | {done} |
POST dev.knotless.pushRefs | a 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 | |
|---|---|
/mcp | the 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/mcp | protected-resource metadata |
GET /.well-known/oauth-authorization-server | authorization-server metadata |
POST /mcp/register | client registration (RFC 7591) |
GET, POST /mcp/authorize | sign-in, with atproto OAuth at the user's PDS |
GET /mcp/oauth/callback | where the user's PDS sends them back |
POST /mcp/token | tokens |
GET /mcp/oauth-client-metadata.json | the client metadata it signs users in with |
Registrations and tokens are signed, not stored.
| Lifetime | |
|---|---|
| Access token | 1 hour |
| Refresh token | 90 days |
| An account's session, from its last token | 90 days |
| Client registration | 100 years |
| Sign-in | 15 minutes |
| Authorization code | 5 minutes |
| Limit | |
|---|---|
| Tool calls running, per account | 8 |
Of those, with a wait | 2 |
| Tool calls started a minute, per account | 120 |
| Sign-ins waiting on PDSes, from everyone | 1000 |
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.sh | the 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/latest | the 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/token | a 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.getViewer | who 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.getUnreadCount | how many of those are new, counting at most 100 |
dev.knotless.getPreferences | the 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.getOwnProfile | the 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.getAccountData | what 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.listBlocks | the 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.json | the 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-callback | where 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.uploadAvatar | the 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.png | social 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.