This commit is contained in:
j3d1 2026-08-17 18:28:01 +02:00
parent 25cef95711
commit 7d7730354e
8 changed files with 381 additions and 39 deletions

View file

@ -20,4 +20,97 @@ uses it to verify access to the friend's inventory. While accepting a friend req
their own public key to the friend's server. This way both users can access each other's inventory.
The protocol is based on a simple HTTPS API exchanging JSON data that is signed with the user's private key. By default
Toolshed servers provide a documentation of the API at [/docs/api](/docs/api).
Toolshed servers provide a documentation of the API at [/docs/api](/docs/api).
## Unique Handles
Federation only works if every server can talk about the same thing without a central authority to ask. Toolshed's
answer is that every kind of entity that needs to be referenced across servers gets a handle: a name that is unique
within its own scope and that carries, as part of itself, enough information to say where it is authoritative. This
keeps servers independent of each other while still letting them agree on what they're talking about.
### Users (and Groups)
A user's handle is their username paired with the domain their account belongs to, written the way an email address
is, e.g. `user@toolsheddomain.tld`. Uniqueness is only required within a single domain, not across all of Toolshed,
so two different domains can each have their own "alice" without conflict, the same way two different email
providers can each have an "alice" mailbox. The domain half of the handle is what makes the name globally
unambiguous, and it is also what tells any other backend where to look to find out who's currently authoritative for
that identity, i.e. which backend holds the account and can vouch for its public key.
Groups aren't implemented yet, but they're intended to fit the same idea: a group would get its own handle on the
domain of the server that hosts it, the same way a user does, so that group membership and group-owned data could be
referenced by other servers without needing a separate mechanism. A group handle is written with a leading `#`, e.g.
`#groupname@toolsheddomain.tld`, so that group and user handles occupy visibly distinct spaces on the same domain
and a name can't be squatted as one to collide with the other. See [groups.md](design-in-progress/groups.md) for
details.
### Servers
The domain half of a handle, e.g. `toolsheddomain.tld`, is an authority record, not a location. Owning a domain
just means being able to say which backend is currently authoritative for handles under it; it says nothing about
where that backend is hosted, who operates it, or how many other domains it might also be authoritative for. A
single backend can just as easily host entities for one domain or for many unrelated ones at once, there's no
assumption anywhere in the model that a domain and a backend are the same thing, or that the relationship is one to
one.
The frontend application is a third, separate thing again. The app a user loads isn't necessarily served by, or
even related to, the backend that ends up handling their requests: when given a handle, the frontend looks up which
backend is currently authoritative for that handle's domain and talks to that backend directly from then on. So
using the frontend at one domain to log into a backend authoritative for a completely different domain isn't a
special case, it's the normal path, since "where the app was loaded from" and "which backend answers for a given
handle" were never the same question to begin with. Servers, in the cryptographic sense described below, don't have
an identity of their own beyond the handles they're currently authoritative for; a backend is, conceptually, just
wherever a given domain's handles happen to resolve to right now.
### Tags, Properties, and Categories
Inventory items aren't just described in free text, they can be classified with tags, properties, and categories,
and those get handles too, written as an origin followed by the kind and name, e.g. `origin#tag:drill` or
`origin#category:power-tools`. This lets the same short name (e.g. a "drill" tag) exist independently under
different origins without colliding, while a handle as a whole unambiguously says which taxonomy an entry belongs
to.
An origin isn't necessarily a server; it's whatever the classification is considered to have come from, which could
be a shared, canonical reference dataset that multiple servers import and reuse, just as easily as it could be a
server's own locally-invented taxonomy. This lets independently-run servers converge on a shared vocabulary where it
matters, without forcing every server to invent its own from scratch or requiring a central body to define one.
Handles are resolved strictly: a reference to an origin or entity a server doesn't know about is left unresolved
rather than being guessed at or silently merged into something that looks similar. This mirrors the rest of
Toolshed's federation philosophy, nothing is combined across servers implicitly; agreement always has to be
traceable to an explicit, shared handle.
### Items
Inventory items don't get a handle of their own the way tags or categories do, because they don't need one: every
item belongs to exactly one user, so a simple local identifier is already enough to tell two items apart within that
user's inventory. Combined with the owner's user handle, that local identifier is automatically unique across all of
Toolshed too, since no two users share a handle. Unlike a tag or category, an item isn't meant to be the same entity
reused across servers, it describes something one specific person actually owns, so there's no shared-origin concept
to design for here, ownership alone already provides the scope.
## Cryptography
Handles say who or what is being referred to; cryptography is what lets a server trust that the entity behind a
handle really is who it claims to be, without needing to ask a central authority. Every user handle has exactly one
asymmetric keypair backing it: a private key that never leaves the user's control, and a public key that gets handed
out freely as part of establishing that handle elsewhere.
A server first learns a public key at the moment it has reason to trust it: for its own users, that's registration;
for a friend's handle, that's the friend-request/accept exchange described above. From then on, a public key is
permanently paired with the handle it arrived with, never with a server. This is why friending is really a
key-exchange ceremony rather than just a social action, accepting a request is the moment a server starts trusting a
new handle's signature.
Every request made on a user's behalf is signed with that user's private key, and whichever server receives it
verifies the signature against the public key it holds for that handle. This is what makes it safe for a request to
travel to a server that isn't the user's home server: the receiving server doesn't need to trust the network path or
the sender, only the signature.
It's worth being explicit about what this layer of cryptography is for and what it isn't. Signing establishes
authenticity and integrity, that a request genuinely came from the handle it claims to, unaltered, not
confidentiality. The data itself isn't encrypted by the protocol; keeping it private in transit is what the
underlying HTTPS layer is for. Only user handles carry a keypair; tags, categories, properties, and item handles are
just names, their trustworthiness comes entirely from being reachable only through a signed request from the user
handle that owns or created them, not from any cryptographic identity of their own.