Deed
drift.Deed.{KeyAuth,JWT,Vault,Link,Pocket}: identity, a peer namespace to Backbone rather than one of its primitives. The Deed guide explains what each one is for; this is the call surface.
KeyAuth
// passwordless Ed25519 device-key auth (uid = the public key)
Challenge(pubkey string) (string, error) // → one-time nonce
Verify(pubkey, sig, domain string) (string, error) // checks sig over {domain,nonce,pubkey} → THIS slice's JWTThe nonce lives 120 seconds and is burned when it verifies. The signature is over one exact byte string (compact JSON, sorted keys, no whitespace) and a mismatch of one byte comes back as 401 bad signature with no further detail.
JWT
// general-purpose HS256 sign/verify; KeyAuth mints its tokens through this
Issue(claims JWTClaims) (string, error) // Exp REQUIRED; Iat/Iss/Jti auto-set if zero
Verify(token string, opts JWTVerifyOptions) (JWTClaims, error) // pass JWTVerifyOptions{}, NOT nil
SliceID() string // this slice's issuer string, "" on error
type JWTClaims struct {
Sub string
Iat int64
Exp int64
Nbf int64
Iss string
Aud []string
Jti string
Custom map[string]any // your app claims live HERE, e.g. claims.Custom["role"]
}
type JWTVerifyOptions struct { Audience, AllowedIssuer string }Three things bite if you guess.
Exp is required on Issue (a missing or past expiry errors); app-defined fields go in Custom, a map, not at the top level; and Verify enforces the issuer is your slice by default, so tokens minted elsewhere fail unless you set AllowedIssuer.A failure carries one of nine stable reasons: malformed, bad_signature, expired, not_yet_valid, wrong_algorithm, wrong_issuer, wrong_audience, invalid_claims, missing_exp. Branch on that, not on the message text.
Vault
// zero-knowledge recovery store (encrypt client-side before Put)
Put(uid string, blob any) error · Get(uid string) (json.RawMessage, error) // latest blobVault does not authenticate its caller.
uid is a plain argument, so any function in your slice can read or write any uid's blob. The confidentiality guarantee rests entirely on the client having encrypted first, so check who is asking before you hand a blob back. Pocket is the primitive that enforces per-identity isolation for you.Link
// multi-device continuity (sig is computed client-side, never by this SDK)
Begin(pubkey string, metadata ...string) (string, error) // → session id
SessionInfo(sessionID string) (LinkSessionInfo, error) // {NewPubkey, Metadata}
Attest(identity, sessionID, attestingPubkey, sig string, sealed ...string) error
Complete(sessionID string) (LinkStatus, error) // {Status, Identity, Sealed}
Revoke(identity, targetPubkey, revokingPubkey, sig string) error
QR(text string) (string, error) // → inline SVG markupThe two trailing variadics carry opaque strings Deed relays without interpreting. Begin's metadata is whatever the joining device wants an attesting device to see, typically an ephemeral public key, retrieved with SessionInfo. Attest's sealed is a payload encrypted for that key, handed back by Complete once the session reports "attested". Rust exposes them as separate functions, begin_with_metadata and attest_with_seal, since it has no variadics.
// E2EE per-identity app data; every call takes the bearer token explicitly
Set(token, key string, blob any) error · Get(token, key string) (json.RawMessage, error)
Delete(token, key string) error · List(token string) ([]string, error)Pocket is the one Deed primitive that authenticates its caller: the token is the one KeyAuth.Verify returned, and its identity is the only one a call can read or write under. Passing the token explicitly rather than holding hidden session state is deliberate: the authority for a call is visible at the call site.
None of this works on your laptop.
drift atomic run errors with deed requires a running slice (DEED_URL). That is deliberate, because a silent no-op would report success while storing nothing. Structure a function so its Deed calls sit behind one seam and the rest stays testable locally; see what runs locally.Authentication walks the full login→verify flow end to end.