registerRelay mounts fourteen routes on a Fastify instance you own. It is a
plugin, not a server: mount it inside the app you already have, under whatever
prefix you like.
Return null to reject with 401. The authentication mechanism can be a bearer
token, session cookie, or mTLS; argala does not impose one.
The token must bind BOTH the member and the device it was issued to. A
token naming only the member is a vulnerability, not a shortcut: it would let
any of that member’s devices fetch another device’s wrapped keys. The relay
checks the binding it is given; it cannot invent one.
A practical approach is to mint a device-scoped session when a device first
registers and include the device ID in the token claims.
membersOfGroup drives rotation completeness — it is how the relay knows
which devices a new epoch must cover. Get it wrong and rotations either strand
people or include people who left.
classMyDirectoryimplementsDirectory{
asyncmembersOfGroup(groupId:string){
constrows=awaitsql`SELECT user_id FROM memberships WHERE team_id = ${groupId}`;
const[u]=awaitsql`SELECT 1 FROM users WHERE id = ${memberId} AND deleted_at IS NULL`;
returnBoolean(u);
}
}
SqliteDirectory is provided for tests, small deployments, and anything with
no other user table:
constdirectory=newSqliteDirectory(db);
directory.addMember('sam');
directory.joinGroup('acme','sam');
Pairwise graphs are an existence oracle. Addressing d:<memberId> answers
404 for a member who does not exist and 200 for one who does, so any
authenticated member can test whether an ID is real. In most apps members
already know each other. In one where they do not, isActiveMember is where
to refuse.
Alert on quota.exceeded and a rising rate of auth.rejected events. The
callback runs synchronously, so keep it cheap. It must never throw (the relay
swallows it if it does, but do not rely on that behaviour).
Rate limits default to per-member token buckets: push and pull at 600/minute
(burst 1,200), blobs at 120/minute (burst 240), and keys at 300/minute (burst
600).
MemoryRateLimiter is per process. It is correct for one relay but wrong
for four behind a load balancer: four relays mean four allowances. Use
SharedRateLimiter once you run more than one, backed by any counter you can
increment and read:
argala does not depend on a Redis client — it is arithmetic, and a library
holding other people’s keys does not get to add a dependency for arithmetic.
Schema changes are append-only migrations under PRAGMA user_version. They run
when openDb is called. The relay refuses to start against a database written
by a newer build rather than guessing. Never edit a migration that has shipped.
ARGALA_DEV_AUTH=1npm-w@argala/relayrundev# a relay on :8787
It refuses to start without the environment variable because it authenticates
nobody. It is for experimenting with the wire protocol, not for deployment.