The Model at a Glance
| Concern | Mechanism |
|---|---|
| Secret storage | PBKDF2-SHA384, 1,000,000 iterations, per-secret random salt |
| Sessions | AES-GCM-encrypted identity token — no session table |
| Logout | Deterministic SHA-384 token hash in a revocation blocklist |
| Future trust | ECDSA P-384 keypair per account, ready for signed messages |
Everything uses the standard Web Crypto API (crypto.subtle), which is
why the same crypto module runs in Node and the browser.
Issuing a Session
accounts/connect verifies the secret against its stored PBKDF2 hash, then
encrypts the caller’s identity into an opaque token:
// server/src/services/accounts/connect.ts (excerpt)
// verify the secret against the stored hash
const isValid = await isSecretValid(identity.token, data.secret)
if (!isValid) throw new InvalidError(invalidMessage)
// issue an encrypted identity token
const role = account.role ?? "BUYER"
const tokenData: TokenIdentity = {
accountId: account.id,
identifier: identity.identifier,
role,
}
const token = await io.agent.encrypt(JSON.stringify(tokenData))
return { token, role }
The client attaches this token to every message’s from field but cannot
read it — only the server holds the AES key. On each request the server
decrypts it back into { accountId, identifier, role }. That’s a stateless
session: any server instance with the key can authenticate any request.
Note the deliberately vague error message — the same reply whether the identifier is unknown, the account inactive, or the secret wrong, so the endpoint doesn’t leak which accounts exist.
Revocation That Actually Works
Logout stores a hash of the token in a blocklist that Agent.process checks
on every request:
// server/src/agent.ts (excerpt)
// revocation check uses a deterministic hash so a stored token can be matched again
async function isTokenRevoked(token: string) {
const tokenHash = await stringToHash(token)
const revoked = await db.query.revokedTokens.findFirst({
where: eq(db.revokedTokens.tokenHash, tokenHash),
})
return Boolean(revoked)
}
The comment carries a war story: the hash must be deterministic, because a lookup key has to be reproducible. Salting is for password storage, not for blocklist keys — an earlier iteration of this design used a random salt here, which meant the stored hash never matched the recomputed one and logout silently revoked nothing. Stateless sessions are only honest if the revocation path genuinely works.
Secret Hashing
Secrets never touch the database in recoverable form. secretToHash derives a
384-bit key via PBKDF2 and packs version + salt + iterations + key into one
self-describing string, so parameters can evolve without breaking stored
hashes:
// shared/src/crypto.ts (excerpt)
const algo = { name: "PBKDF2", hash: "SHA-384", salt, iterations: 1e6 }
const baseKey = await keyload("PBKDF2", encode(secret), ["deriveBits"])
const keyBuffer = await crypto.subtle.deriveBits(algo, baseKey, keySize)
Groundwork: Per-Account Keypairs
Every account is created with an ECDSA P-384 keypair. Nothing signs messages yet — but because the protocol addresses agents rather than URLs, signed messages and agent-to-agent trust are a planned extension, and the key material is already in place.