npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hypercerts-org/sdk-core

v0.10.0-beta.9

Published

Framework-agnostic ATProto SDK core for authentication, repository operations, and lexicon management

Downloads

607

Readme

@hypercerts-org/sdk-core

Framework-agnostic ATProto SDK for Hypercerts. Create, manage, and collaborate on hypercerts using the AT Protocol.

pnpm add @hypercerts-org/sdk-core

Quick Start

import { createATProtoSDK } from "@hypercerts-org/sdk-core";

// 1. Create SDK with OAuth configuration
const sdk = createATProtoSDK({
  oauth: {
    clientId: "https://your-app.com/client-metadata.json",
    redirectUri: "https://your-app.com/callback",
    scope: "atproto",
    jwksUri: "https://your-app.com/jwks.json",
    jwkPrivate: process.env.ATPROTO_JWK_PRIVATE!,
  },
  // Optional: URL for handle resolution during OAuth.
  // If omitted, DNS-based resolution is used.
  handleResolver: "https://pds-eu-west4.test.certified.app",
});

// 2. Authenticate user
const authUrl = await sdk.authorize("user.bsky.social");
// Redirect user to authUrl...

// 3. Handle OAuth callback
const session = await sdk.callback(callbackParams);

// 4. Get repository and start creating hypercerts
const repo = sdk.getRepository(session);
const claim = await repo.hypercerts.create({
  title: "Tree Planting Initiative 2025",
  shortDescription: "1000 trees planted in rainforest",
  description: "Planted 1000 trees in the Amazon rainforest region",
  workScope: "Environmental Conservation",
  startDate: "2025-01-01",
  endDate: "2025-12-31",
  rights: {
    name: "Attribution",
    type: "license",
    description: "CC-BY-4.0",
  },
});

Local Development

For local development and testing, you can use HTTP loopback URLs with localhost or 127.0.0.1:

NextJS App Router Example

// lib/atproto.ts
import { createATProtoSDK } from "@hypercerts-org/sdk-core";

const sdk = createATProtoSDK({
  oauth: {
    // Use localhost for client_id (loopback client)
    clientId: "http://localhost/",

    // Use 127.0.0.1 with your app's port for redirect
    redirectUri: "http://127.0.0.1:3000/api/auth/callback",

    scope: "atproto",

    // Serve JWKS from your app
    jwksUri: "http://127.0.0.1:3000/.well-known/jwks.json",

    // Load from environment variable
    jwkPrivate: process.env.ATPROTO_JWK_PRIVATE!,

    // Optional: suppress warnings
    developmentMode: true,
  },
  // Optional: handle resolver for local testing
  handleResolver: "http://localhost:2583",
  logger: console, // Enable debug logging
});

export default sdk;

API Route Setup

// app/api/auth/callback/route.ts
import sdk from "@/lib/atproto";

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);

  try {
    const session = await sdk.callback(searchParams);

    // Store session (use secure httpOnly cookie in production)
    return Response.json({ success: true, did: session.sub });
  } catch (error) {
    return Response.json({ error: "Authentication failed" }, { status: 401 });
  }
}

Serve JWKS Endpoint

// app/.well-known/jwks.json/route.ts
export async function GET() {
  const jwk = JSON.parse(process.env.ATPROTO_JWK_PRIVATE!);

  // Return public keys only (remove private key 'd' parameter)
  const publicKeys = jwk.keys.map(({ d, ...publicKey }) => publicKey);

  return Response.json({ keys: publicKeys });
}

Environment Variables

# .env.local
ATPROTO_JWK_PRIVATE='{"keys":[{"kty":"EC","crv":"P-256",...}]}'

Important Notes

Authorization Server Support: The AT Protocol OAuth spec makes loopback support optional. Most AT Protocol servers support loopback clients for development, but verify your target authorization server supports this feature.

Port Matching: Redirect URIs can use any port - the authorization server ignores port numbers when validating loopback redirects (only the path must match).

Production Use: ⚠️ Never use HTTP loopback URLs in production. Always use HTTPS with proper TLS certificates.

IP vs Hostname: Use http://localhost/ for clientId and http://127.0.0.1:<port> for redirectUri and jwksUri (this is the recommended pattern per AT Protocol spec).

Core Concepts

1. PDS vs SDS: Understanding Server Types

The SDK supports two types of AT Protocol servers:

Personal Data Server (PDS)

  • Purpose: User's own data storage (e.g., Bluesky)
  • Use case: Individual hypercerts, personal records
  • Features: Profile management, basic CRUD operations
  • Auto-detected: The SDK automatically discovers the user's PDS from the OAuth session -- no configuration needed

Shared Data Server (SDS)

  • Purpose: Collaborative data storage with access control
  • Use case: Organization hypercerts, team collaboration
  • Features: Organizations, multi-user access, role-based permissions
  • Configured via: servers.sds in SDK config
// Connect to user's PDS (default) -- auto-detected from session
const pdsRepo = sdk.repository(session);
await pdsRepo.hypercerts.create({ ... }); // Creates in user's PDS

// Connect to SDS for collaboration features
const sdsRepo = sdk.repository(session, { server: "sds" });
await sdsRepo.organizations.create({ name: "My Org" }); // SDS-only feature

// Switch to organization repository (still on SDS)
const orgs = await sdsRepo.organizations.list();
const orgRepo = sdsRepo.repo(orgs.organizations[0].did);
await orgRepo.hypercerts.list(); // Queries organization's hypercerts on SDS

How PDS Auto-Detection Works

The SDK automatically discovers each user's PDS URL from their OAuth session. You do not need to configure a PDS URL.

  1. During authentication (callback() or restoreSession()), the SDK extracts the user's PDS URL from the OAuth token's aud field, which is resolved from the user's DID Document.

  2. When creating a repository (sdk.repository(session)), the SDK uses the cached PDS URL for that session's DID.

  3. For sessions created outside the SDK, you can manually resolve the PDS:

    await sdk.resolveSessionPds(session);

How Repository Routing Works

The SDK uses a ConfigurableAgent to route requests to different servers while maintaining your OAuth authentication:

  1. Initial Repository Creation

    // User authenticates -- PDS URL is automatically cached from the session
    const session = await sdk.callback(params);
    
    // Create PDS repository -- routes to user's auto-detected PDS
    const pdsRepo = sdk.repository(session);
    
    // Create SDS repository -- routes to configured SDS server
    const sdsRepo = sdk.repository(session, { server: "sds" });
  2. Switching Repositories with .repo()

    // Start with user's SDS repository
    const userSdsRepo = sdk.repository(session, { server: "sds" });
    
    // Switch to organization's repository
    const orgRepo = userSdsRepo.repo("did:plc:org-did");
    
    // All operations on orgRepo still route to SDS, not user's PDS
    await orgRepo.hypercerts.list(); // Queries SDS
    await orgRepo.collaborators.list(); // Queries SDS
  3. Key Implementation Details

    • Each Repository uses a ConfigurableAgent that wraps your OAuth session's fetch handler
    • The agent routes all requests to the specified server URL (PDS, SDS, or custom)
    • The user's PDS URL is auto-detected from the OAuth session's token info (tokenInfo.aud)
    • When you call .repo(did), a new Repository is created with the same server configuration
    • Your OAuth session provides authentication (DPoP, access tokens), while the agent handles routing
    • This enables simultaneous connections to multiple servers with one authentication session

Common Patterns

// Pattern 1: Personal hypercerts on PDS (auto-detected)
const myRepo = sdk.repository(session);
await myRepo.hypercerts.create({ title: "My Personal Impact" });

// Pattern 2: Organization hypercerts on SDS
const sdsRepo = sdk.repository(session, { server: "sds" });
const orgRepo = sdsRepo.repo(organizationDid);
await orgRepo.hypercerts.create({ title: "Team Impact" });

// Pattern 3: Reading another user's hypercerts
const otherUserRepo = myRepo.repo("did:plc:other-user");
await otherUserRepo.hypercerts.list(); // Read-only access to their PDS

// Pattern 4: Collaborating on organization data
const sdsRepo = sdk.repository(session, { server: "sds" });
await sdsRepo.collaborators.grant({
  userDid: "did:plc:teammate",
  role: "editor",
});
const orgRepo = sdsRepo.repo(organizationDid);
// Teammate can now access orgRepo and create hypercerts

2. Authentication & OAuth Permissions

The SDK uses OAuth 2.0 for authentication with granular permission control.

Basic Authentication

// First-time user authentication
const authUrl = await sdk.authorize("user.bsky.social");
// Redirect user to authUrl to complete OAuth flow

// Handle the OAuth callback
const session = await sdk.callback({
  code: "...",
  state: "...",
  iss: "...",
});

// Restore existing session for returning users
const session = await sdk.restoreSession("did:plc:user123");

// Get repository for authenticated user
const repo = sdk.getRepository(session);

OAuth Scopes & Permissions

Control exactly what your app can access using type-safe permission builders:

import { PermissionBuilder, ScopePresets, buildScope } from "@hypercerts-org/sdk-core";

// Use ready-made presets
const scope = ScopePresets.EMAIL_AND_PROFILE; // Request email + profile access
const scope = ScopePresets.POSTING_APP; // Full posting capabilities

// Or build custom permissions
const scope = buildScope(
  new PermissionBuilder()
    .accountEmail("read") // Read user's email
    .repoWrite("app.bsky.feed.post") // Create/update posts
    .blob(["image/*", "video/*"]) // Upload media
    .build(),
);

// Use in OAuth configuration
const sdk = createATProtoSDK({
  oauth: {
    clientId: "your-client-id",
    redirectUri: "https://your-app.com/callback",
    scope: scope, // Your custom scope
    // ... other config
  },
});

Available Presets:

  • EMAIL_READ - User's email address
  • PROFILE_READ / PROFILE_WRITE - Profile access
  • POST_WRITE - Create posts
  • SOCIAL_WRITE - Likes, reposts, follows
  • MEDIA_UPLOAD - Image and video uploads
  • POSTING_APP - Full posting with media
  • EMAIL_AND_PROFILE - Common combination

See OAuth Permissions Documentation for detailed usage.

3. Working with Hypercerts

Creating a Hypercert

const hypercert = await repo.hypercerts.create({
  title: "Climate Research Project",
  description: "Research on carbon capture technologies",
  image: imageBlob, // optional: File or Blob
  externalUrl: "https://example.com/project",

  impact: {
    scope: ["Climate Change", "Carbon Capture"],
    work: {
      from: "2024-01-01",
      to: "2025-12-31",
    },
    contributors: ["did:plc:researcher1", "did:plc:researcher2"],
  },

  rights: {
    license: "CC-BY-4.0",
    allowsDerivatives: true,
    transferrable: false,
  },
});

console.log("Created hypercert:", hypercert.uri);

Retrieving Hypercerts

// Get a specific hypercert by URI
const hypercert = await repo.hypercerts.get("at://did:plc:user123/org.hypercerts.claim/abc123");

// List all hypercerts in the repository
const { records } = await repo.hypercerts.list();
for (const claim of records) {
  console.log(claim.value.title);
}

// List with pagination
const { records, cursor } = await repo.hypercerts.list({ limit: 10 });
if (cursor) {
  const nextPage = await repo.hypercerts.list({ limit: 10, cursor });
}

Updating a Hypercert

// Update an existing hypercert
await repo.hypercerts.update("at://did:plc:user123/org.hypercerts.claim/abc123", {
  title: "Updated Climate Research Project",
  description: "Expanded scope to include renewable energy",
  impact: {
    scope: ["Climate Change", "Carbon Capture", "Renewable Energy"],
    work: { from: "2024-01-01", to: "2026-12-31" },
    contributors: ["did:plc:researcher1", "did:plc:researcher2"],
  },
});

Deleting a Hypercert

await repo.hypercerts.delete("at://did:plc:user123/org.hypercerts.claim/abc123");

4. Contributions and Measurements

Adding Contributions

// Add a contribution to a hypercert
const contribution = await repo.hypercerts.addContribution({
  claim: "at://did:plc:user123/org.hypercerts.claim/abc123",
  contributor: "did:plc:contributor456",
  description: "Led the research team and conducted field studies",
  contributionType: "Work",
  percentage: 40.0,
});

Adding Measurements

// Add a measurement/evaluation
const measurement = await repo.hypercerts.addMeasurement({
  claim: "at://did:plc:user123/org.hypercerts.claim/abc123",
  type: "Impact",
  value: 1000,
  unit: "trees planted",
  verifiedBy: "did:plc:auditor789",
  verificationMethod: "On-site inspection with GPS verification",
  measuredAt: new Date().toISOString(),
});

5. Collections and Projects

Collections organize multiple hypercerts into logical groupings. Projects are a special type of collection with type="project".

Creating a Collection

// Create a collection with weighted items
const collection = await repo.hypercerts.createCollection({
  title: "Climate Projects 2024",
  shortDescription: "Our climate impact portfolio",
  description: "A curated collection of climate-related hypercerts",
  items: [
    {
      itemIdentifier: { uri: hypercert1Uri, cid: hypercert1Cid },
      itemWeight: "0.5",
    },
    {
      itemIdentifier: { uri: hypercert2Uri, cid: hypercert2Cid },
      itemWeight: "0.3",
    },
    {
      itemIdentifier: { uri: hypercert3Uri, cid: hypercert3Cid },
      itemWeight: "0.2",
    },
  ],
  avatar: avatarBlob, // optional
  banner: bannerBlob, // optional
});

console.log("Created collection:", collection.uri);

Creating a Project

Projects are collections with automatic type="project":

const project = await repo.hypercerts.createProject({
  title: "Rainforest Restoration",
  shortDescription: "Multi-year restoration initiative",
  description: "Comprehensive rainforest restoration project",
  items: [
    {
      itemIdentifier: { uri: activity1Uri, cid: activity1Cid },
      itemWeight: "0.6",
    },
    {
      itemIdentifier: { uri: activity2Uri, cid: activity2Cid },
      itemWeight: "0.4",
    },
  ],
});

Attaching Locations

Both collections and projects can have location data attached as a sidecar record:

// Attach location to a project
const locationResult = await repo.hypercerts.attachLocationToProject(projectUri, {
  lpVersion: "1.0",
  srs: "EPSG:4326",
  locationType: "coordinate-decimal",
  location: "https://example.com/location.geojson", // or use a Blob
  name: "Project Site",
  description: "Main restoration site coordinates",
});

// Also works for collections
await repo.hypercerts.attachLocationToCollection(collectionUri, locationParams);

Listing and Retrieving

// Get a specific collection
const collection = await repo.hypercerts.getCollection(collectionUri);

// List all collections
const { records } = await repo.hypercerts.listCollections();

// Get a specific project
const project = await repo.hypercerts.getProject(projectUri);

// List all projects
const { records } = await repo.hypercerts.listProjects();

Updating Collections and Projects

// Update collection
await repo.hypercerts.updateCollection(collectionUri, {
  title: "Updated Title",
  items: [
    /* updated items */
  ],
  avatar: newAvatarBlob, // or null to remove
});

// Update project (same API)
await repo.hypercerts.updateProject(projectUri, {
  shortDescription: "Updated description",
  banner: null, // removes banner
});

Deleting

// Delete collection
await repo.hypercerts.deleteCollection(collectionUri);

// Delete project
await repo.hypercerts.deleteProject(projectUri);

// Remove location from project
await repo.hypercerts.removeLocationFromProject(projectUri);

6. Blob Operations (Images & Files)

// Upload an image or file
const blobResult = await repo.blobs.upload(imageFile);
console.log("Blob uploaded:", blobResult.ref.$link);

// Download a blob
const blobData = await repo.blobs.get("did:plc:user123", "bafyreiabc123...");

7. Organizations (SDS only)

Organizations allow multiple users to collaborate on shared repositories.

// Create an organization
const org = await repo.organizations.create({
  name: "Climate Research Institute",
  description: "Leading research on climate solutions",
  handle: "climate-research", // optional: unique handle
});

console.log("Organization DID:", org.did);

// List all organizations you belong to
const { organizations } = await repo.organizations.list();
for (const org of organizations) {
  console.log(`${org.name} (${org.role})`);
}

// List with pagination
const { organizations, cursor } = await repo.organizations.list({ limit: 10 });

// Get a specific organization
const org = await repo.organizations.get("did:plc:org123");
console.log(`${org.name} - ${org.description}`);

8. Collaborator Management (SDS only)

Manage who has access to your repository and what they can do.

Granting Access

// Grant different levels of access
await repo.collaborators.grant({
  userDid: "did:plc:user123",
  role: "editor", // viewer | editor | admin | owner
});

// Roles explained:
// - viewer: Read-only access
// - editor: Can create and edit records
// - admin: Can manage collaborators and settings
// - owner: Full control (same as repository owner)

Managing Collaborators

// List all collaborators with pagination
const { collaborators, cursor } = await repo.collaborators.list();
for (const collab of collaborators) {
  console.log(`${collab.userDid} - ${collab.role}`);
}

// List next page
if (cursor) {
  const nextPage = await repo.collaborators.list({ cursor, limit: 20 });
}

// Check if a user has access
const hasAccess = await repo.collaborators.hasAccess("did:plc:user123");

// Get a specific user's role
const role = await repo.collaborators.getRole("did:plc:user123");
console.log(`User role: ${role}`); // "editor", "admin", etc.

// Get current user's permissions
const permissions = await repo.collaborators.getPermissions();
if (permissions.admin) {
  console.log("You can manage collaborators");
}
if (permissions.create) {
  console.log("You can create records");
}

Revoking Access

// Remove a collaborator
await repo.collaborators.revoke({
  userDid: "did:plc:user123",
});

Transferring Ownership

// Transfer repository ownership (irreversible!)
await repo.collaborators.transferOwnership({
  newOwnerDid: "did:plc:newowner456",
});

9. Generic Record Operations

For working with any ATProto record type:

// Create a generic record
const record = await repo.records.create({
  collection: "org.hypercerts.claim",
  record: {
    $type: "org.hypercerts.claim",
    title: "My Claim",
    // ... record data
  },
});

// Get a record
const record = await repo.records.get({
  collection: "org.hypercerts.claim",
  rkey: "abc123",
});

// Update a record
await repo.records.update({
  collection: "org.hypercerts.claim",
  rkey: "abc123",
  record: {
    $type: "org.hypercerts.claim",
    title: "Updated Title",
    // ... updated data
  },
});

// Delete a record
await repo.records.delete({
  collection: "org.hypercerts.claim",
  rkey: "abc123",
});

// List records with pagination
const { records, cursor } = await repo.records.list({
  collection: "org.hypercerts.claim",
  limit: 50,
});

10. Profile Management (PDS only)

The SDK supports two profile types:

  • Bluesky Profile (app.bsky.actor.profile) - Standard AT Protocol profiles with CDN URLs
  • Certified Profile (app.certified.actor.profile) - Hypercerts profiles with additional fields (pronouns, website)

Bluesky Profile

// Get Bluesky profile
const bskyProfile = await repo.profile.getBskyProfile();
console.log(`${bskyProfile.displayName} (@${bskyProfile.handle})`);
console.log(bskyProfile.avatar); // CDN URL: "https://cdn.bsky.app/..."

// Create Bluesky profile
await repo.profile.createBskyProfile({
  displayName: "Alice",
  description: "Climate researcher",
  avatar: avatarBlob, // Upload blob
  banner: bannerBlob,
});

// Update Bluesky profile
await repo.profile.updateBskyProfile({
  displayName: "Alice Smith",
  description: null, // Remove description
});

Certified Profile

// Get Certified profile (returns null if it doesn't exist)
const certProfile = await repo.profile.getCertifiedProfile();
if (certProfile) {
  console.log(certProfile.displayName);
  console.log(certProfile.pronouns); // "she/her"
  console.log(certProfile.website); // "https://alice.com"
  console.log(certProfile.avatar); // Blob URL: "https://pds.../xrpc/..."
} else {
  console.log("User hasn't created a Certified profile yet");
}

// Create Certified profile
await repo.profile.createCertifiedProfile({
  displayName: "Alice",
  description: "Climate scientist and impact certificate advocate",
  pronouns: "she/her",
  website: "https://alice.com",
  avatar: avatarBlob, // Upload blob
  banner: bannerBlob,
});

// Update Certified profile
await repo.profile.updateCertifiedProfile({
  pronouns: "they/them",
  website: null, // Remove website
});

// Upsert Certified profile (create if missing, update if exists)
await repo.profile.upsertCertifiedProfile({
  displayName: "Alice",
  description: "Climate scientist",
  pronouns: "she/her",
  website: "https://alice.com",
  avatar: avatarBlob,
});

// Upsert Bluesky profile
await repo.profile.upsertBskyProfile({
  displayName: "Alice",
  description: "Impact researcher",
  avatar: avatarBlob,
});

Key Differences:

  • Bluesky: Returns avatar/banner as CDN URLs (https://cdn.bsky.app/...), throws error if profile doesn't exist
  • Certified: Returns avatar/banner as PDS blob URLs (https://pds.../xrpc/...), includes pronouns (max 20 graphemes) and website fields, returns null if profile doesn't exist

When to use upsert vs create/update:

  • Use upsert*() for convenience when you don't know if a profile exists (e.g., first-time setup flows)
  • Use create*() when you know the profile doesn't exist (e.g., after checking with get*())
  • Use update*() when you know the profile exists and only want to modify specific fields

API Reference

Repository Operations

| Operation | Method | PDS | SDS | Returns | | ------------------------ | ------------------------------------------------ | --- | --- | ------------------------------------ | | Records | | | | | | Create record | repo.records.create() | ✅ | ✅ | { uri, cid } | | Get record | repo.records.get() | ✅ | ✅ | Record data | | Update record | repo.records.update() | ✅ | ✅ | { uri, cid } | | Delete record | repo.records.delete() | ✅ | ✅ | void | | List records | repo.records.list() | ✅ | ✅ | { records, cursor? } | | Hypercerts | | | | | | Create hypercert | repo.hypercerts.create() | ✅ | ✅ | { uri, cid, value } | | Get hypercert | repo.hypercerts.get() | ✅ | ✅ | Full hypercert | | Update hypercert | repo.hypercerts.update() | ✅ | ✅ | { uri, cid } | | Delete hypercert | repo.hypercerts.delete() | ✅ | ✅ | void | | List hypercerts | repo.hypercerts.list() | ✅ | ✅ | { records, cursor? } | | Add contribution | repo.hypercerts.addContribution() | ✅ | ✅ | Contribution | | Add measurement | repo.hypercerts.addMeasurement() | ✅ | ✅ | Measurement | | Collections | | | | | | Create collection | repo.hypercerts.createCollection() | ✅ | ✅ | { uri, cid, record } | | Get collection | repo.hypercerts.getCollection() | ✅ | ✅ | Collection data | | List collections | repo.hypercerts.listCollections() | ✅ | ✅ | { records, cursor? } | | Update collection | repo.hypercerts.updateCollection() | ✅ | ✅ | { uri, cid } | | Delete collection | repo.hypercerts.deleteCollection() | ✅ | ✅ | void | | Attach location | repo.hypercerts.attachLocationToCollection() | ✅ | ✅ | { uri, cid } | | Remove location | repo.hypercerts.removeLocationFromCollection() | ✅ | ✅ | void | | Projects | | | | | | Create project | repo.hypercerts.createProject() | ✅ | ✅ | { uri, cid, record } | | Get project | repo.hypercerts.getProject() | ✅ | ✅ | Project data | | List projects | repo.hypercerts.listProjects() | ✅ | ✅ | { records, cursor? } | | Update project | repo.hypercerts.updateProject() | ✅ | ✅ | { uri, cid } | | Delete project | repo.hypercerts.deleteProject() | ✅ | ✅ | void | | Attach location | repo.hypercerts.attachLocationToProject() | ✅ | ✅ | { uri, cid } | | Remove location | repo.hypercerts.removeLocationFromProject() | ✅ | ✅ | void | | Blobs | | | | | | Upload blob | repo.blobs.upload() | ✅ | ✅ | { ref, mimeType, size } | | Get blob | repo.blobs.get() | ✅ | ✅ | Blob data | | Profile | | | | | | Get Bluesky profile | repo.profile.getBskyProfile() | ✅ | ❌ | BskyProfile (CDN URLs) | | Create Bluesky profile | repo.profile.createBskyProfile() | ✅ | ❌ | { uri, cid } | | Update Bluesky profile | repo.profile.updateBskyProfile() | ✅ | ❌ | { uri, cid } | | Upsert Bluesky profile | repo.profile.upsertBskyProfile() | ✅ | ❌ | { uri, cid } | | Get Certified profile | repo.profile.getCertifiedProfile() | ✅ | ❌ | CertifiedProfile | null (blob URLs) | | Create Certified profile | repo.profile.createCertifiedProfile() | ✅ | ❌ | { uri, cid } | | Update Certified profile | repo.profile.updateCertifiedProfile() | ✅ | ❌ | { uri, cid } | | Upsert Certified profile | repo.profile.upsertCertifiedProfile() | ✅ | ❌ | { uri, cid } | | Organizations | | | | | | Create org | repo.organizations.create() | ❌ | ✅ | { did, name, ... } | | Get org | repo.organizations.get() | ❌ | ✅ | Organization | | List orgs | repo.organizations.list() | ❌ | ✅ | { organizations, cursor? } | | Collaborators | | | | | | Grant access | repo.collaborators.grant() | ❌ | ✅ | void | | Revoke access | repo.collaborators.revoke() | ❌ | ✅ | void | | List collaborators | repo.collaborators.list() | ❌ | ✅ | { collaborators, cursor? } | | Check access | repo.collaborators.hasAccess() | ❌ | ✅ | boolean | | Get role | repo.collaborators.getRole() | ❌ | ✅ | Role string | | Get permissions | repo.collaborators.getPermissions() | ❌ | ✅ | Permissions | | Transfer ownership | repo.collaborators.transferOwnership() | ❌ | ✅ | void |

Type System

Types are generated from ATProto lexicon definitions and exported with friendly aliases:

import type {
  HypercertClaim,
  HypercertRights,
  HypercertContribution,
  HypercertMeasurement,
  HypercertEvaluation,
  HypercertCollection,
  HypercertLocation,
} from "@hypercerts-org/sdk-core";

// For validation, use namespaced imports
import { OrgHypercertsClaim } from "@hypercerts-org/sdk-core";

if (OrgHypercertsClaim.isRecord(data)) {
  // data is typed as HypercertClaim
}

| Lexicon Type | SDK Alias | | ------------------------------------- | ----------------------- | | OrgHypercertsClaim.Main | HypercertClaim | | OrgHypercertsClaimRights.Main | HypercertRights | | OrgHypercertsClaimContribution.Main | HypercertContribution | | OrgHypercertsClaimMeasurement.Main | HypercertMeasurement | | OrgHypercertsClaimEvaluation.Main | HypercertEvaluation | | OrgHypercertsCollection.Main | HypercertCollection | | AppCertifiedLocation.Main | HypercertLocation |

Error Handling

import {
  ValidationError,
  NetworkError,
  AuthenticationError,
  SDSRequiredError,
} from "@hypercerts-org/sdk-core/errors";

try {
  await repo.hypercerts.create({ ... });
} catch (error) {
  if (error instanceof ValidationError) {
    console.error("Invalid hypercert data:", error.message);
  } else if (error instanceof NetworkError) {
    console.error("Network issue:", error.message);
  } else if (error instanceof AuthenticationError) {
    console.error("Authentication failed:", error.message);
  } else if (error instanceof SDSRequiredError) {
    console.error("This operation requires SDS:", error.message);
  }
}

Package Entrypoints

@hypercerts-org/sdk-core
├── /              → Full SDK (createATProtoSDK, Repository, types, errors)
├── /types         → TypeScript types (re-exported from @hypercerts-org/lexicon)
├── /errors        → Error classes
├── /lexicons      → LexiconRegistry, HYPERCERT_LEXICONS, HYPERCERT_COLLECTIONS
├── /storage       → InMemorySessionStore, InMemoryStateStore
└── /testing       → createMockSession, MockSessionStore

Advanced Usage

Multi-Server Routing with ConfigurableAgent

The ConfigurableAgent allows you to create custom agents that route to specific servers:

import { ConfigurableAgent } from "@hypercerts-org/sdk-core";

// Authenticate once with your PDS
const session = await sdk.callback(params);

// Create agents for different servers using the same session
const pdsAgent = new ConfigurableAgent(session, "https://bsky.social");
const sdsAgent = new ConfigurableAgent(session, "https://sds.hypercerts.org");
const orgAgent = new ConfigurableAgent(session, "https://sds-org-a.example.com");

// Use agents directly with AT Protocol APIs
await pdsAgent.com.atproto.repo.createRecord({...});
await sdsAgent.com.atproto.repo.listRecords({...});

// Or pass to Repository for high-level operations
// (Repository internally uses ConfigurableAgent)

This is useful for:

  • Connecting to multiple SDS instances simultaneously
  • Testing against different server environments
  • Building tools that work across multiple organizations
  • Direct AT Protocol API access with custom routing

Custom Session Storage

import { createATProtoSDK } from "@hypercerts-org/sdk-core";
import { InMemorySessionStore } from "@hypercerts-org/sdk-core/storage";

const sdk = createATProtoSDK({
  oauth: { ... },
  sessionStore: new InMemorySessionStore(),
});

Testing with Mocks

import { createMockSession, MockSessionStore } from "@hypercerts-org/sdk-core/testing";

const mockSession = createMockSession({
  did: "did:plc:test123",
  handle: "test.user",
});

const mockStore = new MockSessionStore();
await mockStore.set(mockSession);

Working with Lexicons

The SDK exports lexicon types and validation utilities from the @hypercerts-org/lexicon package for direct record manipulation and validation.

Lexicon Types

All lexicon types are available with proper TypeScript support:

import type {
  HypercertClaim,
  HypercertRights,
  HypercertContribution,
  HypercertCollection,
  HypercertMeasurement,
  HypercertEvaluation,
  HypercertLocation,
  StrongRef,
} from "@hypercerts-org/sdk-core";

// Create a properly typed hypercert claim
const claim: HypercertClaim = {
  $type: "org.hypercerts.claim",
  title: "Community Garden Project",
  shortDescription: "Urban garden serving 50 families", // REQUIRED
  description: "Detailed description...",
  workScope: "Food Security",
  workTimeFrameFrom: "2024-01-01T00:00:00Z", // Note: Capital 'F'
  workTimeFrameTo: "2024-12-31T00:00:00Z", // Note: Capital 'F'
  rights: { uri: "at://...", cid: "..." },
  createdAt: new Date().toISOString(),
};

Validation

Validate records before creating them:

import { validate, OrgHypercertsClaim, HYPERCERT_COLLECTIONS } from "@hypercerts-org/sdk-core";

// Validate using the lexicon package
const validation = validate(
  HYPERCERT_COLLECTIONS.CLAIM, // "org.hypercerts.claim"
  claim,
);

if (!validation.valid) {
  console.error("Validation failed:", validation.error);
}

// Or use type-specific validators
const isValid = OrgHypercertsClaim.isMain(claim);
const validationResult = OrgHypercertsClaim.validateMain(claim);

Using LexiconRegistry

The SDK automatically initializes a LexiconRegistry with all hypercert lexicons. You can access it to validate records or register custom lexicons:

// Access the registry from the SDK
const registry = sdk.getLexiconRegistry();

// Validate a record
const result = registry.validate("org.hypercerts.claim.activity", claimData);

if (!result.valid) {
  console.error("Invalid record:", result.error);
}

// Register a custom lexicon
registry.registerFromJSON({
  lexicon: 1,
  id: "org.myapp.customRecord",
  defs: {
    main: {
      type: "record",
      key: "tid",
      record: {
        type: "object",
        required: ["$type", "title"],
        properties: {
          $type: { type: "string", const: "org.myapp.customRecord" },
          title: { type: "string" },
          createdAt: { type: "string", format: "datetime" },
        },
      },
    },
  },
});

// Check if a lexicon is registered
if (registry.isRegistered("org.myapp.customRecord")) {
  console.log("Custom lexicon is ready to use");
}

You can also access the registry from a Repository instance:

const repo = sdk.repository(session);
const registry = repo.getLexiconRegistry();

Creating Records with Proper Types

import type { HypercertContribution, StrongRef } from "@hypercerts-org/sdk-core";
import { HYPERCERT_COLLECTIONS } from "@hypercerts-org/sdk-core";

// Create a contribution record
const contribution: HypercertContribution = {
  $type: HYPERCERT_COLLECTIONS.CONTRIBUTION,
  hypercert: {
    uri: "at://did:plc:abc/org.hypercerts.claim/xyz",
    cid: "bafyrei...",
  } as StrongRef,
  contributors: ["did:plc:contributor1", "did:plc:contributor2"],
  role: "implementer",
  description: "On-ground implementation team",
  workTimeframeFrom: "2024-01-01T00:00:00Z", // Note: lowercase 'f' for contributions
  workTimeframeTo: "2024-06-30T00:00:00Z", // Note: lowercase 'f' for contributions
  createdAt: new Date().toISOString(),
};

// Use with repository operations
await repo.records.create({
  collection: HYPERCERT_COLLECTIONS.CONTRIBUTION,
  record: contribution,
});

Available Lexicon Collections

import { HYPERCERT_COLLECTIONS } from "@hypercerts-org/sdk-core";

// Collection NSIDs
HYPERCERT_COLLECTIONS.CLAIM; // "org.hypercerts.claim"
HYPERCERT_COLLECTIONS.RIGHTS; // "org.hypercerts.claim.rights"
HYPERCERT_COLLECTIONS.CONTRIBUTION; // "org.hypercerts.claim.contribution"
HYPERCERT_COLLECTIONS.MEASUREMENT; // "org.hypercerts.claim.measurement"
HYPERCERT_COLLECTIONS.EVALUATION; // "org.hypercerts.claim.evaluation"
HYPERCERT_COLLECTIONS.EVIDENCE; // "org.hypercerts.claim.evidence"
HYPERCERT_COLLECTIONS.COLLECTION; // "org.hypercerts.collection"
HYPERCERT_COLLECTIONS.LOCATION; // "app.certified.location"

Development

pnpm install        # Install dependencies
pnpm build          # Build the package
pnpm test           # Run tests
pnpm test:coverage  # Run tests with coverage
pnpm test:watch     # Run tests in watch mode

License

MIT

Resources