@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-coreQuick 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/forclientIdandhttp://127.0.0.1:<port>forredirectUriandjwksUri(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.sdsin 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 SDSHow 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.
During authentication (
callback()orrestoreSession()), the SDK extracts the user's PDS URL from the OAuth token'saudfield, which is resolved from the user's DID Document.When creating a repository (
sdk.repository(session)), the SDK uses the cached PDS URL for that session's DID.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:
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" });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 SDSKey Implementation Details
- Each Repository uses a
ConfigurableAgentthat 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
- Each Repository uses a
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 hypercerts2. 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 addressPROFILE_READ/PROFILE_WRITE- Profile accessPOST_WRITE- Create postsSOCIAL_WRITE- Likes, reposts, followsMEDIA_UPLOAD- Image and video uploadsPOSTING_APP- Full posting with mediaEMAIL_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/...), includespronouns(max 20 graphemes) andwebsitefields, returnsnullif 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 withget*()) - 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, MockSessionStoreAdvanced 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 modeLicense
MIT
