oss.sarwagya.wtf

Your first identity

Create an identity, sign a challenge, verify the proof.

Three moves: the browser creates (or loads) its identity, your server issues a one-time challenge, the browser answers it with a signature your server verifies.

1. Create the identity

import { createGhost } from "@0xsarwagya/ghost";
 
const ghost = await createGhost();
 
ghost.id;        // "ghost_1_crhgcniramqtgfpib5uiaautocwdigbt"
ghost.credentialId; // "cred_1_…"
ghost.publicKey; // base64url of the raw Ed25519 public key

The first call generates a non-extractable keypair and persists it in IndexedDB. Every later call — tomorrow, next month — loads the same key and returns the same Ghost ID. No network, no user gesture, no dashboard.

2. Issue a challenge on your server

import { createChallenge, InMemoryChallengeStore } from "@0xsarwagya/ghost/server";
 
const store = new InMemoryChallengeStore();
 
// GET /ghost/challenge
const challenge = createChallenge({
  audience: "https://app.example",
  action: "login",
});
// → { version: 1, nonce, audience, action, expiresAt }

Send it to the browser as JSON. Challenges expire (two minutes by default) and verify exactly once.

3. Sign it in the browser

const proof = await ghost.sign(challenge);
// POST /ghost/verify with the proof

4. Verify the proof on your server

import { verifyGhostProof } from "@0xsarwagya/ghost/server";
 
const result = await verifyGhostProof(proof, {
  expectedAudience: "https://app.example",
  expectedAction: "login",
  challengeStore: store,
});
 
if (result.ok) {
  // result.ghostId is proven: this client holds an active credential.
  // Store it next to whatever this ghost owns, or start your session.
}

verifyGhostProof never throws for bad proofs — it returns { ok: false, code } so a hostile request is a result, not an exception. The verifier enforces signature, expiry, audience, action, identity binding, and one-time nonce use; a valid signature alone is never enough.