Skip to content

forgesworn/nostr-attestations

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

115 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nostr-attestations

npm CI GitHub Sponsors

One Nostr event kind for all attestations -- credentials, endorsements, vouches, provenance, and licensing. Supports both direct attestation and assertion-first patterns within a single kind.

Nostr: npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2

Install

npm install nostr-attestations

Quick Start

import { createAttestation, TYPES } from 'nostr-attestations'

// Create an unsigned attestation event template
const event = createAttestation({
  type: TYPES.CREDENTIAL,
  identifier: '<subject-pubkey>', // d-tag segment: names this attestation
  subject: '<subject-pubkey>',    // p-tag: who the attestation is about
  summary: 'Professional credential verified',
  expiration: 1735689600,
  tags: [['profession', 'attorney'], ['jurisdiction', 'US-NY']],
  content: JSON.stringify({ proof: '...' }),
})
// => { kind: 31000, tags: [...], content: '...' }
// Sign with your preferred library and publish to relays

No relay client, no signing library, no crypto. Bring your own. Works with nostr-tools, any Nostr SDK, or bare WebSocket code.

Assertion-First Pattern

Attest to someone else's claim -- the individual makes an assertion, you verify it:

import { createAttestation } from 'nostr-attestations'

// Attest to a first-person assertion event
const event = createAttestation({
  assertion: { id: '<assertion-event-id>', relay: 'wss://relay.example.com' },
  subject: '<subject-pubkey>',
  content: 'Identity verified in person',
})
// type is inferred from the referenced assertion -- no type tag needed

Temporal Context

Record when the attested event actually happened, separate from when the attestation was published:

const event = createAttestation({
  type: TYPES.ENDORSEMENT,
  subject: '<service-provider-pubkey>',
  occurredAt: 1710900000,  // service completed last Tuesday
  summary: 'Completed frontend work ahead of schedule',
})
// created_at = when attestation published, occurred_at = when it happened

NIP-32 Labels

All attestations include NIP-32 labels for relay-side discoverability. Typed attestations also include a type label:

["L", "nip-va"]
["l", "credential", "nip-va"]

This enables queries like {"#L": ["nip-va"]} (all attestations) or {"#l": ["credential"]} (by type).

Why This?

Nostr has several ways to label or badge identities, but none designed for verifiable attestations. NIP-58 badges are display-only -- no expiry, no revocation, no structured claims. NIP-85 covers social graph metrics, not arbitrary claims. NIP-32 labels are lightweight but not individually replaceable per subject.

nostr-attestations uses one kind (31000) with a type tag instead of inventing a new event kind for every attestation use case. Credentials, endorsements, vouches, licensing, provenance, and revocations all share the same event structure. New attestation types need zero protocol changes -- just define a new type value.

Every attestation is self-describing -- the type tag and NIP-32 labels mean a client can render a meaningful card, and a relay can filter by attestation type, without fetching any referenced events first.

It supports two attestation patterns within the same kind:

  • Direct attestation -- the attestor defines the type and makes a claim about a subject
  • Assertion-first -- the subject makes their own claim, and the attestor references and verifies it

The base layer is semantically neutral -- it carries attestations but does not interpret them. Flows like attestation requests, trust lists, and attestor discovery belong at the application layer, not in the protocol. Application profiles (identity verification, professional licensing, service reputation) are built downstream, not in the library.

Revocation

import { createRevocation, isRevoked, parseAttestation } from 'nostr-attestations'

// Revoke a previously issued attestation
const revocation = createRevocation({
  type: 'credential',
  identifier: '<subject-pubkey>',
  subject: '<subject-pubkey>',
  reason: 'licence-expired',
  effective: 1704067200,
})
// Publish -- addressable event semantics replace the original

// Check if a fetched event is revoked
const revoked = isRevoked(event) // true if ["status", "revoked"] tag present

Parsing

import { parseAttestation } from 'nostr-attestations'

const attestation = parseAttestation(event) // returns null for non-attestation events
if (!attestation) throw new Error('Not a valid attestation')
// {
//   kind: 31000,
//   type: 'credential',          // "assertion" for assertion-only attestations
//   pubkey: '<attestor-pubkey>',
//   createdAt: 1700000000,
//   identifier: '<subject-pubkey>',
//   subject: '<subject-pubkey>',
//   assertionId: null,            // event ID if assertion-first
//   assertionAddress: null,       // addressable coord if assertion-first
//   assertionRelay: null,         // relay hint for assertion
//   summary: 'Professional credential verified',
//   expiration: 1735689600,
//   validFrom: null,
//   validTo: null,                // validity window end
//   request: null,                // what prompted this attestation
//   schema: null,                 // machine-readable schema URI
//   occurredAt: null,             // when the attested event happened
//   revoked: false,
//   reason: null,
//   tags: [...],
//   content: '...',
// }

Validation

import { validateAttestation, isValid } from 'nostr-attestations'

// Structural correctness (d-tag, type, assertion refs, etc.)
const result = validateAttestation(event)
if (!result.valid) console.error(result.errors)

// Temporal validity (revocation, expiration, validity window)
const validity = isValid(event)
if (!validity.valid) console.warn(validity.reason)
// reason: 'revoked' | 'expired' | 'not-yet-active' | 'claim-expired'

API Reference

Builders

Function Signature Returns
createAttestation (params: AttestationParams) => EventTemplate Unsigned attestation event
createRevocation (params: RevocationParams) => EventTemplate Unsigned revocation event

Parsers

Function Signature Returns
parseAttestation (event: NostrEvent) => Attestation | null Typed attestation data, or null for non-attestation events
isRevoked (event: NostrEvent) => boolean True if event has ["status", "revoked"]

Validators

Function Signature Returns
validateAttestation (event: NostrEvent) => ValidationResult { valid: boolean, errors: string[] }

Validity

Function Signature Returns
isValid (event: NostrEvent, now?: number) => ValidityResult { valid: boolean, reason?: string }

Filters

Function Signature Returns
attestationFilter (params: FilterParams) => NostrFilter Relay query filter
revocationFilter (type, identifier) or ({ assertionId | assertionAddress }) Revocation check filter
buildDTag (type: string, identifier: string) => string "type:identifier" string
buildAssertionDTag (ref: string) => string "assertion:ref" string
parseDTag (dTag: string) => { type: string; identifier: string } | null Parsed d-tag (type is "assertion" for assertion-only)

Constants

Export Value Description
ATTESTATION_KIND 31000 NIP-VA event kind
TYPES { CREDENTIAL, ENDORSEMENT, VOUCH, VERIFIER, PROVENANCE } Well-known type constants

Types

AssertionRef, AttestationParams, RevocationParams, Attestation, ValidationResult, ValidityResult, FilterParams, NostrFilter, NostrEvent, EventTemplate -- all exported from the package root and from nostr-attestations/types (zero-runtime import).

Test Vectors

vectors/attestations.json contains 20 frozen conformance test vectors covering the full range of attestation types (credential, endorsement, vouch, verifier, provenance) and states (active, revoked, self-attestation). Any conformant implementation must produce identical parse results from these inputs. The vectors are pinned -- if tests against them fail, the implementation is broken, not the vector.

Attested on Nostr

This library's authorship is claimed on Nostr using the very protocol it implements -- NIP-VA eating its own dog food.

A self-attestation alone only proves that the holder of a private key claims authorship -- not that the claim is true. The real value comes from third-party attestations: other pubkeys independently publishing type: endorsement events that reference this repo.

But counting endorsements isn't enough either -- anyone can create 50 throwaway npubs and endorse themselves. What matters is who endorses, not how many. A single endorsement from a pubkey with a verified NIP-05 domain, a history of notes, and followers you recognise is worth more than a thousand from anonymous keys. This is a web of trust, not a vote count. When verifying, ask: do I know this endorser? Do people I trust follow them? That's how you resist Sybil attacks without a centralised authority.

All verification uses nak (the Nostr Army Knife). Install with go install github.com/fiatjaf/nak@latest or brew install fiatjaf/tap/nak.

1. GitHub → Nostr -- this README claims npub1mgv... (see header above)

2. Nostr → GitHub -- the repo announcement points back here:

nak req -k 30617 \
  -a $(nak decode npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2) \
  -d nostr-attestations \
  wss://relay.damus.io
# Look for the "web" tag → github.com/forgesworn/nostr-attestations

3. Verify the authorship claim -- signed by the same key:

nak req -k 31000 \
  -a $(nak decode npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2) \
  -d authorship:nostr-attestations \
  wss://relay.damus.io 2>/dev/null | nak verify \
  && echo "✓ Signature valid"

4. Check for third-party endorsements:

nak req -k 31000 \
  -t a=30617:da19f1cd34beca44be74da4b306d9d1dd86b6343cef94ce22c49c6f59816e5bd:nostr-attestations \
  wss://relay.damus.io
# Returns all attestations referencing this repo -- filter by pubkey or type client-side

Same npub on both sides -- you'd need to control both GitHub and the private key to fake it. Third-party endorsements add independent signatures that can't be faked by one person.

Endorse it yourself:

nak event -k 31000 \
  --prompt-sec \
  -d endorsement:da19f1cd34beca44be74da4b306d9d1dd86b6343cef94ce22c49c6f59816e5bd \
  -t type=endorsement \
  -p da19f1cd34beca44be74da4b306d9d1dd86b6343cef94ce22c49c6f59816e5bd \
  -t a=30617:da19f1cd34beca44be74da4b306d9d1dd86b6343cef94ce22c49c6f59816e5bd:nostr-attestations \
  -t summary="Reviewed and endorsed nostr-attestations" \
  wss://relay.damus.io wss://nos.lol wss://relay.nostr.band

NIP-VA

Full protocol specification: NIP-VA.md | NostrHub

Part of the ForgeSworn Toolkit

ForgeSworn builds open-source cryptographic identity, payments, and coordination tools for Nostr.

Library What it does
nsec-tree Deterministic sub-identity derivation
ring-sig SAG/LSAG ring signatures on secp256k1
range-proof Pedersen commitment range proofs
canary-kit Coercion-resistant spoken verification
spoken-token Human-speakable verification tokens
toll-booth L402 payment middleware
geohash-kit Geohash toolkit with polygon coverage
nostr-attestations NIP-VA verifiable attestations
dominion Epoch-based encrypted access control
nostr-veil Privacy-preserving Web of Trust

Licence

MIT

About

One Nostr event kind for all attestations — credentials, endorsements, vouches, provenance, licensing, and trust. NIP-VA (kind 31000).

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors