Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .changeset/configurable-agent-routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"@hypercerts-org/sdk-core": minor
---

Implement ConfigurableAgent for proper multi-server routing

This release introduces the `ConfigurableAgent` class that enables proper routing of AT Protocol requests to different servers (PDS, SDS, or custom instances) while maintaining OAuth authentication from a single session.

**Breaking Changes:**
- Repository now uses `ConfigurableAgent` internally instead of standard `Agent`
- This fixes the issue where invalid `agent.service` and `agent.api.xrpc.uri` property assignments were causing TypeScript errors

**New Features:**
- `ConfigurableAgent` class exported from `@hypercerts-org/sdk-core`
- Support for simultaneous connections to multiple SDS instances with one OAuth session
- Proper request routing based on configured service URL rather than session defaults

**Bug Fixes:**
- Remove invalid Agent property assignments that caused TypeScript compilation errors (TS2339)
- Replace all `any` types in test files with proper type annotations
- Eliminate build warnings from missing type declarations

**Architecture:**
The new routing system wraps the OAuth session's fetch handler to prepend the target server URL, ensuring requests go to the intended destination while maintaining full authentication (DPoP, access tokens, etc.). This enables use cases like:
- Routing to SDS while authenticated via PDS
- Accessing multiple organization SDS instances simultaneously
- Testing against different server environments
- Dynamic switching between PDS and SDS operations

**Migration:**
No action required - the change is transparent to existing code. The Repository API remains unchanged.
5 changes: 0 additions & 5 deletions .changeset/fix-agent-service-url.md

This file was deleted.

24 changes: 24 additions & 0 deletions .changeset/fix-repository-routing-to-sds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@hypercerts-org/sdk-core": patch
---

fix(sdk-core): ensure repository operations route to correct server (PDS/SDS)

**Problem:**
When using OAuth authentication to access organization repositories on SDS via `repo.repo(organizationDid)`, all operations like `hypercerts.list()` and `hypercerts.listCollections()` were incorrectly routing to the user's PDS instead of the SDS server, causing "Could not find repo" errors.

**Root Cause:**
The AT Protocol Agent was created from the OAuth session but only had its `api.xrpc.uri` property configured. Without setting the Agent's `service` property, it continued using the session's default PDS URL for all requests, even when switched to organization repositories.

**Solution:**
Set both `agent.service` and `agent.api.xrpc.uri` to the specified server URL in the Repository constructor. This ensures that:
- Initial repository creation routes to the correct server (PDS or SDS)
- Repository switching via `.repo(did)` maintains the same server routing
- All operation implementations (HypercertOperationsImpl, RecordOperationsImpl, ProfileOperationsImpl, BlobOperationsImpl) now route correctly

**Documentation:**
Added comprehensive PDS/SDS orchestration explanation to README covering:
- Server type comparison and use cases
- How repository routing works internally
- Common patterns for personal vs organization hypercerts
- Key implementation details about Agent configuration
138 changes: 130 additions & 8 deletions packages/sdk-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,100 @@ const claim = await repo.hypercerts.create({

## Core Concepts

### 1. Authentication
### 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
- **Example**: `bsky.social`, any Bluesky PDS

#### 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
- **Example**: `sds.hypercerts.org`

```typescript
// Connect to user's PDS (default)
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 Repository Routing Works

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

1. **Initial Repository Creation**
```typescript
// User authenticates (OAuth session knows user's PDS)
const session = await sdk.callback(params);

// Create PDS repository - routes to user's PDS
const pdsRepo = sdk.repository(session);

// Create SDS repository - routes to SDS server
const sdsRepo = sdk.repository(session, { server: "sds" });
```

2. **Switching Repositories with `.repo()`**
```typescript
// 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)
- 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

```typescript
// Pattern 1: Personal hypercerts on PDS
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

The SDK uses OAuth 2.0 for authentication with support for both PDS (Personal Data Server) and SDS (Shared Data Server).

Expand All @@ -67,7 +160,7 @@ const session = await sdk.restoreSession("did:plc:user123");
const repo = sdk.getRepository(session);
```

### 2. Working with Hypercerts
### 3. Working with Hypercerts

#### Creating a Hypercert

Expand Down Expand Up @@ -144,7 +237,7 @@ await repo.hypercerts.delete(
);
```

### 3. Contributions and Measurements
### 4. Contributions and Measurements

#### Adding Contributions

Expand Down Expand Up @@ -174,7 +267,7 @@ const measurement = await repo.hypercerts.addMeasurement({
});
```

### 4. Blob Operations (Images & Files)
### 5. Blob Operations (Images & Files)

```typescript
// Upload an image or file
Expand All @@ -188,7 +281,7 @@ const blobData = await repo.blobs.get(
);
```

### 5. Organizations (SDS only)
### 6. Organizations (SDS only)

Organizations allow multiple users to collaborate on shared repositories.

Expand Down Expand Up @@ -216,7 +309,7 @@ const org = await repo.organizations.get("did:plc:org123");
console.log(`${org.name} - ${org.description}`);
```

### 6. Collaborator Management (SDS only)
### 7. Collaborator Management (SDS only)

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

Expand Down Expand Up @@ -285,7 +378,7 @@ await repo.collaborators.transferOwnership({
});
```

### 7. Generic Record Operations
### 8. Generic Record Operations

For working with any ATProto record type:

Expand Down Expand Up @@ -330,7 +423,7 @@ const { records, cursor } = await repo.records.list({
});
```

### 8. Profile Management (PDS only)
### 9. Profile Management (PDS only)

```typescript
// Get user profile
Expand Down Expand Up @@ -457,6 +550,35 @@ try {

## Advanced Usage

### Multi-Server Routing with ConfigurableAgent

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

```typescript
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

```typescript
Expand Down
89 changes: 89 additions & 0 deletions packages/sdk-core/src/agent/ConfigurableAgent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* ConfigurableAgent - Agent with configurable service URL routing.
*
* This module provides an Agent extension that allows routing requests to
* a specific server URL, overriding the default URL from the OAuth session.
*
* @packageDocumentation
*/

import { Agent } from "@atproto/api";
import type { Session } from "../core/types.js";

/**
* FetchHandler type - function that makes HTTP requests with authentication.
* Takes a pathname and request init, returns a Response promise.
*/
type FetchHandler = (pathname: string, init: RequestInit) => Promise<Response>;

/**
* Agent subclass that routes requests to a configurable service URL.
*
* The standard Agent uses the service URL embedded in the OAuth session's
* fetch handler. This class allows overriding that URL to route requests
* to different servers (e.g., PDS vs SDS, or multiple SDS instances).
*
* @remarks
* This is particularly useful for:
* - Routing to a Shared Data Server (SDS) while authenticated via PDS
* - Supporting multiple SDS instances for different organizations
* - Testing against different server environments
*
* @example Basic usage
* ```typescript
* const session = await sdk.authorize("user.bsky.social");
*
* // Create agent routing to SDS instead of session's default PDS
* const sdsAgent = new ConfigurableAgent(session, "https://sds.hypercerts.org");
*
* // All requests will now go to the SDS
* await sdsAgent.com.atproto.repo.createRecord({...});
* ```
*
* @example Multiple SDS instances
* ```typescript
* // Route to organization A's SDS
* const orgAAgent = new ConfigurableAgent(session, "https://sds-org-a.example.com");
*
* // Route to organization B's SDS
* const orgBAgent = new ConfigurableAgent(session, "https://sds-org-b.example.com");
* ```
*/
export class ConfigurableAgent extends Agent {
private customServiceUrl: string;

/**
* Creates a ConfigurableAgent that routes to a specific service URL.
*
* @param session - OAuth session for authentication
* @param serviceUrl - Base URL of the server to route requests to
*
* @remarks
* The agent wraps the session's fetch handler to intercept requests and
* prepend the custom service URL instead of using the session's default.
*/
constructor(session: Session, serviceUrl: string) {
// Create a custom fetch handler that uses our service URL
const customFetchHandler: FetchHandler = async (pathname: string, init: RequestInit) => {
// Construct the full URL with our custom service
const url = new URL(pathname, serviceUrl).toString();

// Use the session's fetch handler for authentication (DPoP, etc.)
return session.fetchHandler(url, init);
};

// Initialize the parent Agent with our custom fetch handler
super(customFetchHandler);

this.customServiceUrl = serviceUrl;
}

/**
* Gets the service URL this agent routes to.
*
* @returns The base URL of the configured service
*/
getServiceUrl(): string {
return this.customServiceUrl;
}
}
3 changes: 3 additions & 0 deletions packages/sdk-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export type { AuthorizeOptions } from "./core/SDK.js";
export type { ATProtoSDKConfig } from "./core/config.js";
export type { Session } from "./core/types.js";

// Agent
export { ConfigurableAgent } from "./agent/ConfigurableAgent.js";

// Repository (fluent API)
export { Repository } from "./repository/Repository.js";
export type {
Expand Down
13 changes: 6 additions & 7 deletions packages/sdk-core/src/repository/Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
* @packageDocumentation
*/

import { Agent } from "@atproto/api";
import { SDSRequiredError } from "../core/errors.js";
import type { LoggerInterface } from "../core/interfaces.js";
import type { Session } from "../core/types.js";
import { HYPERCERT_LEXICONS } from "@hypercerts-org/lexicon";
import { ConfigurableAgent } from "../agent/ConfigurableAgent.js";
import type { Agent } from "@atproto/api";
import type { LexiconRegistry } from "./LexiconRegistry.js";

// Types
Expand Down Expand Up @@ -179,12 +180,10 @@ export class Repository {
this._isSDS = isSDS;
this.logger = logger;

// Create Agent with OAuth session
this.agent = new Agent(session);

// Configure Agent to use the specified server URL (PDS or SDS)
// This ensures queries are routed to the correct server
this.agent.api.xrpc.uri = new URL(serverUrl);
// Create a ConfigurableAgent that routes requests to the specified server URL
// This allows routing to PDS, SDS, or any custom server while maintaining
// the OAuth session's authentication
this.agent = new ConfigurableAgent(session, serverUrl);

this.lexiconRegistry.addToAgent(this.agent);

Expand Down
Loading