docs / guide

view as .md

Wallet login (user-sig)#

A human signs in with their wallet and your app receives a stable, app-scoped pseudonym — not their address. There is no browser redirect, no issuer, and no session-granting server in between: the wallet signs, the SDK mints a deed, and your app verifies it directly against the on-chain registry. If you haven't already, read Concepts for the mental model.

The flow#

  1. Your app issues a challengeGET /challenge on your own deed guard.
  2. The wallet signs. The SDK has the wallet sign a human-readable message naming the origin (twice, compared for determinism) to derive a root_seed, then derives a per-(tenant, origin) app key from it and signs the deed with that key. One wallet popup per origin, not one for the whole web.
  3. Your app verifies the deed with DeedGuard/DeedVerifier and mints its own session, however it already does. See Verify a deed.

The full walkthrough — the exact derivation (derive_root_seedderive_app_keymint_user_deed), what is and is not in the key-derivation scope (tenant + origin, deliberately not aud), and what a sub looks like across audiences/origins/tenants — is documented once, in Sovereign tier § User login (user-sig). Read that rather than a second copy here.

TypeScript: signInWithDeed#

@grantor/sdk exports signInWithDeed, which composes the wallet-signing steps above (and the origin-provenance check, below) into one call:

import { signInWithDeed } from "@grantor/sdk";

const deed = await signInWithDeed({
  signMessage,      // wraps the wallet's personal_sign
  tenantId,
  audience,
  origin,           // the origin YOU are actually running on
  challenge,        // from your app's own /challenge
  exp,
  vouch,            // the RP's published origin_vouch
  chainRegistry: REGISTRY,
  chainReader,
});

Python, Go and Rust expose the same steps (deriveRootSeed/deriveAppKey/ mintUserDeed, in their language-specific casing) as separate calls rather than one wrapper. The Rust snippet in Sovereign tier § User login is the source of truth; sdk/README.md maps the equivalent calls per language.

Origin provenance: required before signing#

signInWithDeed (and authenticate(), the equivalent agent-side helper) refuses to sign before it signs anything — not after — if the origin cannot show a tenant admin's on-chain vouch that it speaks for (tenant, audience). This is what stops a hostile origin that republishes a victim tenant's tenant/audience from harvesting a pseudonym by simply asking a holder to sign in. chainReader/chainRegistry are REQUIRED — omitting either is a fail-closed error, not a skipped check. See Sovereign tier § Origin provenance and § Constructing a chainReader.

What this proves — and what it does not#

user-sig proves "I control a key", not "I control a wallet". Permissionless login is open by definition — anyone can generate a key. The wallet's role is portability of identity across devices, not gatekeeping. There is no on-chain user revocation, because there is no on-chain user state to revoke; an RP that wants to ban someone bans the sub on its own side.

personal_sign also means the wallet cannot tell who is asking: a hostile page can request the victim's origin-scoped message, and the only control left is a human reading a prompt that names an origin they are not on. See Sovereign tier § What S1 cost, and what it did not buy for the full residual-risk statement — do not describe this mode as a hard phishing-resistance guarantee.

See also#

This page is also served as Markdown — agents should read that. The whole tree is indexed for machines in llms.txt.