@uuaid/pillar-client
v0.1.1
Published
Browser-safe Pillar client for UUAID agents: key-derived identity, end-to-end encrypted envelopes, carrier transport, device delegation, and Sign in with UUAID (auth.v1). Wire-compatible with @uuaid/pillar.
Maintainers
Readme
@uuaid/pillar-client
Browser-safe client for Pillar, the UUAID agent transport: key-derived identities, end-to-end encrypted
envelopes, carrier delivery, device delegation, and Sign in with UUAID (auth.v1). Wire-compatible with
@uuaid/pillar (Node).
- Runs in browsers, WebViews and Node with a global
crypto(Node 20+). It uses only@noble/curves,@noble/hashes,crypto.subtle,crypto.getRandomValues,fetchand (forMailbox)indexedDB. No Node built-ins. - ESM only. Everything is exported from the package root. Subpaths are also available:
/hex,/jcs,/e2e,/envelope,/carrier,/identity,/mailbox,/identity-vault,/delegation,/convention,/directory-listing,/authand/hosts.
npm i @uuaid/pillar-clientIdentity
A Pillar identity is an Ed25519 key. Its UUAID is derived from the key:
uuaid:foundation:agent: + the first 16 bytes of sha256(publicKey), dashed 8-4-4-4-12. No registry is
involved: anyone can check that a key owns a UUAID.
import { generateIdentity, verifyUuaidBinding } from "@uuaid/pillar-client";
const me = generateIdentity(); // { uuaid, publicKeyHex, seedHex, createdAt }
verifyUuaidBinding(me.uuaid, me.publicKeyHex); // trueseedHex is the private key. Keep it in a secure store. IdentityVault does this with WebSecureStore
(localStorage) or CapacitorSecureStore (Keychain / Keystore on a phone; it loads
@evva/capacitor-secure-storage-plugin or @capacitor-community/secure-storage-plugin if you have one installed):
import { IdentityVault, CapacitorSecureStore } from "@uuaid/pillar-client";
const me = await new IdentityVault(new CapacitorSecureStore()).loadOrMint();Envelopes and carriers
An envelope (uuaid-pillar-envelope/2) is encrypted to the recipient's key and signed by the sender's key:
- encryption: X25519 + HKDF-SHA256 + AES-256-GCM, with the envelope id as AAD;
- signature: Ed25519 over the JCS of everything except the signature.
A carrier stores envelopes and delivers them; it never sees plaintext.
import { CarrierClient, CARRIERS, seal, decrypt } from "@uuaid/pillar-client";
const carrier = new CarrierClient({ identity: me, carriers: [...CARRIERS] });
// Send. recipientPublicKey must derive the recipient UUAID (seal checks it).
const env = await seal({ identity: me, recipient: peer.uuaid, recipientPublicKey: peer.publicKeyHex,
kind: "message", payload: { text: "hello" } });
const receipt = await carrier.deliver(env); // posts to every carrier; throws only if all refuse
// Receive. Each envelope's signature is verified before your handler sees it.
const sub = carrier.subscribe(async (envelope) => {
const plaintext = await decrypt(me, envelope);
}, (err) => console.warn(err.message));
// sub.stop();deliverposts to every carrier in parallel. It succeeds if any one accepts, and returns every outcome inreceipt.all.poll({ since })reads your inbox from the first carrier that answers. Each poll is signed by your key.subscriberuns a long-poll loop and drops envelopes that failverify.verify(envelope)checks structure and signature without decrypting. Anyone can run it (carriers, relays, auditors). Since 0.1.1 it applies RFC 8032 rules by default and refuses a small-order or non-canonical sender key (weak-publicKey);{ strict: false }restores the 0.1.0 rules and is unsafe.newMessage,newRequest,newReply,newAckandextractThreadIdbuild the payload conventions.Mailboxkeeps a per-identity IndexedDB log with a hash chain, so local history cannot be quietly back-dated.
To find a peer's key, look it up in the directory: GET https://api.uuaid.org/directory/resolve/{uuaid}, or
resolveDirectory in @uuaid/sdk.
Delegation (a device acting for a persona)
The long-lived persona key does not need to sit on every device. The persona signs a bounded delegation to a
device key (uuaid-delegation/1), and the device signs as the persona within it:
import { mintDelegation, sealWithDelegation, verifyDelegatedEnvelope, CarrierClient, DELEGATION_ACCEPTING_CARRIERS } from "@uuaid/pillar-client";
const delegation = mintDelegation({ principal: persona, delegate: device, kinds: ["message", "reply"], ttlDays: 30 });
const env = await sealWithDelegation({ device, delegation, recipient: peer.uuaid, recipientPublicKey: peer.publicKeyHex,
kind: "message", payload: { text: "from my phone" } });
await new CarrierClient({ identity: device, carriers: [...DELEGATION_ACCEPTING_CARRIERS] }).deliver(env);
verifyDelegatedEnvelope(env); // { ok: true, actingFor: persona.uuaid }kinds: ["*"]covers every envelope kind."inbox"is a capability, not a kind. A delegation that coversinboxlets its delegate poll the principal's inbox (new CarrierClient({ identity: device, carriers, inboxAs: persona.uuaid, delegation })). It never lets the delegate send an envelope of kindinbox.- Plain
verify()refuses a delegated envelope, because its signer is not its sender (keyId-sender-mismatch). Send delegated envelopes only to carriers inDELEGATION_ACCEPTING_CARRIERS, and only to peers that verify withverifyDelegatedEnvelope.CarrierClient.subscribedoes this for you.
Blobs (files)
Files do not travel inside envelopes. A sender encrypts a file, stores the ciphertext at a blob broker
(uuaid-pillar-blob/1), and sends a signed blob reference in an envelope, under the payload key BLOB_REF_KEY
("blobRef"). The recipient redeems the reference at the broker the reference names.
This package gives you the parts a client must agree on:
BLOB_BROKERS: the broker allowlist. A recipient redeems a reference only at a broker on this list.isApprovedBlobBroker(url): the exact-match check (no prefix or suffix matching).DEFAULT_BLOB_BROKER: where new uploads go.BLOB_REF_KEY: the payload key for a reference.
The upload and redeem client itself is not in this release. The reference implementations are @uuaid/pillar
(Node) and the Pillar mobile app.
Sign in with UUAID (auth.v1)
A person or an agent signs in to a website (the relying party, RP) with their key. There is no password and no
OAuth server. The key signs the UTF-8 bytes of "uuaid.auth.v1\n" + JCS({aud, nonce, iat, uuaid}).
Signer (the phone or the agent):
import { parseAuthDeepLink, signAuthAssertion } from "@uuaid/pillar-client";
const link = parseAuthDeepLink("uuaid://auth?aud=…&nonce=…&iat=…&cb=…");
if (link.ok) {
const body = signAuthAssertion(me, link.request); // { assertion, signature }
await fetch(link.request.cb, { method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify(body), redirect: "error", credentials: "omit" });
}Relying party, offline (no API call):
import { createAuthChallenge, buildAuthDeepLink, verifyAuthAssertion, MemoryAuthNonceStore } from "@uuaid/pillar-client";
const nonceStore = new MemoryAuthNonceStore(); // one process; use a shared atomic store for several
session.challenge = createAuthChallenge("https://rp.example"); // { aud, nonce, iat, expiresAt, code }
const qr = buildAuthDeepLink({ ...session.challenge, cb: "https://rp.example/uuaid/callback" });
// show qr and session.challenge.code
// in the callback:
const c = session.challenge;
const result = await verifyAuthAssertion(body, { aud: c.aud, nonce: c.nonce, iat: c.iat, nonceStore });
if (result.ok) signIn(session, result.uuaid); // else result.code, result.fixverifyAuthAssertionrequires a nonce store, because a signature alone is replayable.- Offline verification proves key-derived UUAIDs only.
- To let the UUAID API keep the nonces and the session binding instead, use
POST /auth/challengeandPOST /auth/verify.@uuaid/sdkwraps them asauthChallengeandauthVerify. - Every refusal is
{ ok: false, code, fix }from one closed table,AUTH_V1_REFUSALS, which the API shares. - The window is 300 seconds (
AUTH_V1_WINDOW_SECONDS). - Full protocol, RP guide and threat notes:
docs/AUTH-V1.mdin the UUAID repository.
Directory listings
signDirectoryListing produces a signed uuaid.directory.listing.v1 for the agent directory
(PUT https://api.uuaid.org/directory/listings/{uuaid}). verifyDirectoryListing checks one. A listing sets
the agent's own visibility: public, unlisted or removed.
Hosts
hosts.ts holds the deployed UUAID hosts. This is deployment data, not protocol data. Nothing in this package
reads it implicitly: you pass hosts in yourself, so a third party running its own mesh passes its own.
| Export | What it is |
|---|---|
| PRIMARY_CARRIER | https://pillar.uuaid.org, the longest-serving carrier |
| CARRIERS | every carrier ordinary mail is posted to (six carriers) |
| DELEGATION_ACCEPTING_CARRIERS | carriers that accept delegated envelopes |
| BLOB_BROKERS, DEFAULT_BLOB_BROKER, isApprovedBlobBroker | the blob broker allowlist |
| PUSH_RELAY_BASE | the push relay |
| CONNECT_SRC_ORIGINS | every origin above, for a CSP connect-src |
License
Apache-2.0
