Skip to content

Cooperation-org/claim-atproto

Repository files navigation

@cooperation/claim-atproto

TypeScript library for creating and publishing linked claims on ATProto (Bluesky)

A composable, type-safe library for working with verifiable claims on the AT Protocol. Implements the LinkedClaims specification from the Decentralized Identity Foundation (DIF).

Features

  • Fluent Builder API - Chainable, type-safe claim construction
  • ATProto Native - Seamless integration with Bluesky/ATProto
  • Claims-about-Claims - Built-in support for endorsements, disputes, revocations
  • Content Hashing - Compute integrity hashes for evidence
  • Schema Validation - Automatic validation against the community.claim lexicon
  • Universal - Works in Node.js and browser environments
  • TypeScript - Full type safety with excellent IDE support

Installation

npm install @cooperation/claim-atproto

Requirements:

  • Node.js 18+ or modern browser
  • @atproto/api (peer dependency)

Quick Start

import { AtpAgent } from '@atproto/api'
import { ClaimClient, createClaim } from '@cooperation/claim-atproto'

// Authenticate with Bluesky
const agent = new AtpAgent({ service: 'https://bsky.social' })
await agent.login({
  identifier: 'alice.bsky.social',
  password: 'app-password', // Use an app password, not your main password
})

// Create a claim client
const client = new ClaimClient({ agent })

// Build and publish a claim
const claim = createClaim()
  .subject('did:plc:alice')
  .type('skill')
  .object('React')
  .statement('5 years of production experience')
  .confidence(0.9)
  .build()

const published = await client.publish(claim)
console.log(`Published at: ${published.uri}`)

Core Concepts

Claims

A claim is an immutable, signed assertion about any URI-addressable subject:

const claim = createClaim()
  .subject('did:plc:alice')           // Who/what the claim is about
  .type('skill')                      // Category of claim
  .object('TypeScript')               // Optional: specific object
  .statement('Expert level')          // Human-readable explanation
  .confidence(1.0)                    // Optional: confidence (0-1)
  .build()

Claims-about-Claims

Endorsements, disputes, and other meta-claims reference another claim's AT-URI:

import { createEndorsement } from '@cooperation/claim-atproto'

// Endorse another claim
const endorsement = createEndorsement(
  'at://did:plc:alice/community.claim/xyz123',
  'I can confirm Alice has these skills',
  { confidence: 1.0, howKnown: 'FIRST_HAND' }
).build()

await client.publish(endorsement)

Evidence & Provenance

Add structured evidence with content hashing:

import { createSource, computeDigestMultibase } from '@cooperation/claim-atproto'

const evidenceHash = await computeDigestMultibase('Evidence content...')

const claim = createClaim()
  .subject('https://ngo.org/project')
  .type('impact')
  .statement('Delivered 500 water filters')
  .withSource(
    createSource()
      .uri('https://evidence.org/report.pdf')
      .digest(evidenceHash)
      .howKnown('WEB_DOCUMENT')
  )
  .build()

API Overview

Builders

  • createClaim() - Build a claim with fluent API
  • createSource() - Build evidence/provenance metadata
  • createProof() - Build external proof (for future external signing support)

Client

  • ClaimClient - Publish and manage claims on ATProto
    • .publish(claim) - Publish to your repository
    • .publishTo(did, claim) - Publish to another repository
    • .get(uri) - Fetch a claim by AT-URI
    • .delete(uri) - Delete a claim

Helpers

  • createEndorsement(uri, statement, options) - Create an endorsement
  • createDispute(uri, statement, options) - Create a dispute
  • createSuperseding(uri, statement) - Create an update/replacement
  • createRevocation(uri, reason) - Create a revocation
  • computeDigestMultibase(content) - Hash content for integrity
  • fetchAndHash(uri) - Fetch and hash remote content

Validation

  • validateClaim(claim) - Validate against lexicon (throws on error)
  • isValidClaim(claim) - Check validity (returns boolean)

Examples

Basic Skill Claim

const claim = createClaim()
  .subject('did:plc:alice')
  .type('skill')
  .object('React')
  .statement('3 years production experience')
  .build()

const published = await client.publish(claim)

Impact Claim with Evidence

const claim = createClaim()
  .subject('https://example.org/ngo/project-123')
  .type('impact')
  .statement('Distributed 500 water filters in Kibera')
  .withSource(
    createSource()
      .uri('ipfs://bafybei...')
      .howKnown('FIRST_HAND')
      .dateObserved(new Date('2024-12-15'))
  )
  .effectiveDate(new Date('2024-12-15'))
  .build()

await client.publish(claim)

Endorsement

import { createEndorsement } from '@cooperation/claim-atproto'

const endorsement = createEndorsement(
  'at://did:plc:bob/community.claim/abc123',
  'I worked with Bob for 2 years and can confirm his skills',
  { confidence: 1.0, howKnown: 'FIRST_HAND' }
).build()

await client.publish(endorsement)

Dispute

import { createDispute } from '@cooperation/claim-atproto'

const dispute = createDispute(
  'at://did:plc:alice/community.claim/xyz789',
  'The actual count was 200, not 500',
  {
    evidence: 'https://evidence.org/actual-count.pdf',
    howKnown: 'WEB_DOCUMENT'
  }
).build()

await client.publish(dispute)

Rating

const rating = createClaim()
  .subject('https://restaurant.example.com')
  .type('rating')
  .object('food-quality')
  .stars(4)
  .statement('Excellent pasta, slightly slow service')
  .build()

await client.publish(rating)

TypeScript Types

Full TypeScript support with exported types:

import type {
  Claim,
  ClaimSource,
  EmbeddedProof,
  PublishedClaim,
  HowKnown,
  ClaimClientConfig,
} from '@cooperation/claim-atproto'

Claim Types

The claimType field is an open string. Common values include:

  • skill - Professional skills
  • credential - Certifications, degrees
  • impact - NGO/charity impact claims
  • endorsement - Endorsement of another claim
  • dispute - Dispute of another claim
  • rating - Star ratings (use stars field)
  • review - Reviews with text
  • membership - Organization membership
  • supersedes - Claim that replaces another
  • revocation - Claim revocation

You can use any string value that fits your use case.

Claim Signing

All claims published to ATProto are automatically signed by the repository's signing key. This happens transparently when you call client.publish().

Who signed a claim?

  • If published to user's own repo → signer is the user's DID
  • If published to server's repo → signer is the server's DID

For external signing (MetaMask, DIDs, etc.), see the embeddedProof field in the types. External signing support may be added in future versions.

Validation

Claims are automatically validated against the community.claim lexicon before publishing:

import { validateClaim, isValidClaim } from '@cooperation/claim-atproto'

// Throws error if invalid
validateClaim(claim)

// Returns boolean
if (isValidClaim(claim)) {
  await client.publish(claim)
}

// Disable validation for testing
const client = new ClaimClient({ agent, validate: false })

Browser Usage

The library works in browsers too:

<script type="module">
  import { createClaim } from 'https://esm.sh/@cooperation/claim-atproto'

  const claim = createClaim()
    .subject('did:plc:alice')
    .type('endorsement')
    .build()
</script>

Or with a bundler (Vite, Webpack, etc.):

import { ClaimClient, createClaim } from '@cooperation/claim-atproto'
// ... use normally

More Examples

See the examples/ directory for complete working examples:

Development

# Install dependencies
npm install

# Build
npm run build

# Test
npm test

# Type check
npm run type-check

# Run examples
npx tsx examples/node/basic-claim.ts

Related Projects

License

MIT

Contributing

Contributions welcome! Please open an issue or PR.

Questions?

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors