Skip to content

Commit e2c87ff

Browse files
authored
Merge pull request #60 from hypercerts-org/fix/sds_call_routing
SDS call routing
2 parents faf4c3f + f7594f8 commit e2c87ff

16 files changed

+462
-51
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
"@hypercerts-org/sdk-core": minor
3+
---
4+
5+
Implement ConfigurableAgent for proper multi-server routing
6+
7+
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.
8+
9+
**Breaking Changes:**
10+
- Repository now uses `ConfigurableAgent` internally instead of standard `Agent`
11+
- This fixes the issue where invalid `agent.service` and `agent.api.xrpc.uri` property assignments were causing TypeScript errors
12+
13+
**New Features:**
14+
- `ConfigurableAgent` class exported from `@hypercerts-org/sdk-core`
15+
- Support for simultaneous connections to multiple SDS instances with one OAuth session
16+
- Proper request routing based on configured service URL rather than session defaults
17+
18+
**Bug Fixes:**
19+
- Remove invalid Agent property assignments that caused TypeScript compilation errors (TS2339)
20+
- Replace all `any` types in test files with proper type annotations
21+
- Eliminate build warnings from missing type declarations
22+
23+
**Architecture:**
24+
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:
25+
- Routing to SDS while authenticated via PDS
26+
- Accessing multiple organization SDS instances simultaneously
27+
- Testing against different server environments
28+
- Dynamic switching between PDS and SDS operations
29+
30+
**Migration:**
31+
No action required - the change is transparent to existing code. The Repository API remains unchanged.

.changeset/fix-agent-service-url.md

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@hypercerts-org/sdk-core": patch
3+
---
4+
5+
fix(sdk-core): ensure repository operations route to correct server (PDS/SDS)
6+
7+
**Problem:**
8+
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.
9+
10+
**Root Cause:**
11+
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.
12+
13+
**Solution:**
14+
Set both `agent.service` and `agent.api.xrpc.uri` to the specified server URL in the Repository constructor. This ensures that:
15+
- Initial repository creation routes to the correct server (PDS or SDS)
16+
- Repository switching via `.repo(did)` maintains the same server routing
17+
- All operation implementations (HypercertOperationsImpl, RecordOperationsImpl, ProfileOperationsImpl, BlobOperationsImpl) now route correctly
18+
19+
**Documentation:**
20+
Added comprehensive PDS/SDS orchestration explanation to README covering:
21+
- Server type comparison and use cases
22+
- How repository routing works internally
23+
- Common patterns for personal vs organization hypercerts
24+
- Key implementation details about Agent configuration

packages/sdk-core/README.md

Lines changed: 130 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,100 @@ const claim = await repo.hypercerts.create({
4444

4545
## Core Concepts
4646

47-
### 1. Authentication
47+
### 1. PDS vs SDS: Understanding Server Types
48+
49+
The SDK supports two types of AT Protocol servers:
50+
51+
#### Personal Data Server (PDS)
52+
- **Purpose**: User's own data storage (e.g., Bluesky)
53+
- **Use case**: Individual hypercerts, personal records
54+
- **Features**: Profile management, basic CRUD operations
55+
- **Example**: `bsky.social`, any Bluesky PDS
56+
57+
#### Shared Data Server (SDS)
58+
- **Purpose**: Collaborative data storage with access control
59+
- **Use case**: Organization hypercerts, team collaboration
60+
- **Features**: Organizations, multi-user access, role-based permissions
61+
- **Example**: `sds.hypercerts.org`
62+
63+
```typescript
64+
// Connect to user's PDS (default)
65+
const pdsRepo = sdk.repository(session);
66+
await pdsRepo.hypercerts.create({ ... }); // Creates in user's PDS
67+
68+
// Connect to SDS for collaboration features
69+
const sdsRepo = sdk.repository(session, { server: "sds" });
70+
await sdsRepo.organizations.create({ name: "My Org" }); // SDS-only feature
71+
72+
// Switch to organization repository (still on SDS)
73+
const orgs = await sdsRepo.organizations.list();
74+
const orgRepo = sdsRepo.repo(orgs.organizations[0].did);
75+
await orgRepo.hypercerts.list(); // Queries organization's hypercerts on SDS
76+
```
77+
78+
#### How Repository Routing Works
79+
80+
The SDK uses a `ConfigurableAgent` to route requests to different servers while maintaining your OAuth authentication:
81+
82+
1. **Initial Repository Creation**
83+
```typescript
84+
// User authenticates (OAuth session knows user's PDS)
85+
const session = await sdk.callback(params);
86+
87+
// Create PDS repository - routes to user's PDS
88+
const pdsRepo = sdk.repository(session);
89+
90+
// Create SDS repository - routes to SDS server
91+
const sdsRepo = sdk.repository(session, { server: "sds" });
92+
```
93+
94+
2. **Switching Repositories with `.repo()`**
95+
```typescript
96+
// Start with user's SDS repository
97+
const userSdsRepo = sdk.repository(session, { server: "sds" });
98+
99+
// Switch to organization's repository
100+
const orgRepo = userSdsRepo.repo("did:plc:org-did");
101+
102+
// All operations on orgRepo still route to SDS, not user's PDS
103+
await orgRepo.hypercerts.list(); // ✅ Queries SDS
104+
await orgRepo.collaborators.list(); // ✅ Queries SDS
105+
```
106+
107+
3. **Key Implementation Details**
108+
- Each Repository uses a `ConfigurableAgent` that wraps your OAuth session's fetch handler
109+
- The agent routes all requests to the specified server URL (PDS, SDS, or custom)
110+
- When you call `.repo(did)`, a new Repository is created with the same server configuration
111+
- Your OAuth session provides authentication (DPoP, access tokens), while the agent handles routing
112+
- This enables simultaneous connections to multiple servers with one authentication session
113+
114+
#### Common Patterns
115+
116+
```typescript
117+
// Pattern 1: Personal hypercerts on PDS
118+
const myRepo = sdk.repository(session);
119+
await myRepo.hypercerts.create({ title: "My Personal Impact" });
120+
121+
// Pattern 2: Organization hypercerts on SDS
122+
const sdsRepo = sdk.repository(session, { server: "sds" });
123+
const orgRepo = sdsRepo.repo(organizationDid);
124+
await orgRepo.hypercerts.create({ title: "Team Impact" });
125+
126+
// Pattern 3: Reading another user's hypercerts
127+
const otherUserRepo = myRepo.repo("did:plc:other-user");
128+
await otherUserRepo.hypercerts.list(); // Read-only access to their PDS
129+
130+
// Pattern 4: Collaborating on organization data
131+
const sdsRepo = sdk.repository(session, { server: "sds" });
132+
await sdsRepo.collaborators.grant({
133+
userDid: "did:plc:teammate",
134+
role: "editor",
135+
});
136+
const orgRepo = sdsRepo.repo(organizationDid);
137+
// Teammate can now access orgRepo and create hypercerts
138+
```
139+
140+
### 2. Authentication
48141

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

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

70-
### 2. Working with Hypercerts
163+
### 3. Working with Hypercerts
71164

72165
#### Creating a Hypercert
73166

@@ -144,7 +237,7 @@ await repo.hypercerts.delete(
144237
);
145238
```
146239

147-
### 3. Contributions and Measurements
240+
### 4. Contributions and Measurements
148241

149242
#### Adding Contributions
150243

@@ -174,7 +267,7 @@ const measurement = await repo.hypercerts.addMeasurement({
174267
});
175268
```
176269

177-
### 4. Blob Operations (Images & Files)
270+
### 5. Blob Operations (Images & Files)
178271

179272
```typescript
180273
// Upload an image or file
@@ -188,7 +281,7 @@ const blobData = await repo.blobs.get(
188281
);
189282
```
190283

191-
### 5. Organizations (SDS only)
284+
### 6. Organizations (SDS only)
192285

193286
Organizations allow multiple users to collaborate on shared repositories.
194287

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

219-
### 6. Collaborator Management (SDS only)
312+
### 7. Collaborator Management (SDS only)
220313

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

@@ -285,7 +378,7 @@ await repo.collaborators.transferOwnership({
285378
});
286379
```
287380

288-
### 7. Generic Record Operations
381+
### 8. Generic Record Operations
289382

290383
For working with any ATProto record type:
291384

@@ -330,7 +423,7 @@ const { records, cursor } = await repo.records.list({
330423
});
331424
```
332425

333-
### 8. Profile Management (PDS only)
426+
### 9. Profile Management (PDS only)
334427

335428
```typescript
336429
// Get user profile
@@ -457,6 +550,35 @@ try {
457550

458551
## Advanced Usage
459552

553+
### Multi-Server Routing with ConfigurableAgent
554+
555+
The `ConfigurableAgent` allows you to create custom agents that route to specific servers:
556+
557+
```typescript
558+
import { ConfigurableAgent } from "@hypercerts-org/sdk-core";
559+
560+
// Authenticate once with your PDS
561+
const session = await sdk.callback(params);
562+
563+
// Create agents for different servers using the same session
564+
const pdsAgent = new ConfigurableAgent(session, "https://bsky.social");
565+
const sdsAgent = new ConfigurableAgent(session, "https://sds.hypercerts.org");
566+
const orgAgent = new ConfigurableAgent(session, "https://sds-org-a.example.com");
567+
568+
// Use agents directly with AT Protocol APIs
569+
await pdsAgent.com.atproto.repo.createRecord({...});
570+
await sdsAgent.com.atproto.repo.listRecords({...});
571+
572+
// Or pass to Repository for high-level operations
573+
// (Repository internally uses ConfigurableAgent)
574+
```
575+
576+
This is useful for:
577+
- Connecting to multiple SDS instances simultaneously
578+
- Testing against different server environments
579+
- Building tools that work across multiple organizations
580+
- Direct AT Protocol API access with custom routing
581+
460582
### Custom Session Storage
461583

462584
```typescript
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* ConfigurableAgent - Agent with configurable service URL routing.
3+
*
4+
* This module provides an Agent extension that allows routing requests to
5+
* a specific server URL, overriding the default URL from the OAuth session.
6+
*
7+
* @packageDocumentation
8+
*/
9+
10+
import { Agent } from "@atproto/api";
11+
import type { Session } from "../core/types.js";
12+
13+
/**
14+
* FetchHandler type - function that makes HTTP requests with authentication.
15+
* Takes a pathname and request init, returns a Response promise.
16+
*/
17+
type FetchHandler = (pathname: string, init: RequestInit) => Promise<Response>;
18+
19+
/**
20+
* Agent subclass that routes requests to a configurable service URL.
21+
*
22+
* The standard Agent uses the service URL embedded in the OAuth session's
23+
* fetch handler. This class allows overriding that URL to route requests
24+
* to different servers (e.g., PDS vs SDS, or multiple SDS instances).
25+
*
26+
* @remarks
27+
* This is particularly useful for:
28+
* - Routing to a Shared Data Server (SDS) while authenticated via PDS
29+
* - Supporting multiple SDS instances for different organizations
30+
* - Testing against different server environments
31+
*
32+
* @example Basic usage
33+
* ```typescript
34+
* const session = await sdk.authorize("user.bsky.social");
35+
*
36+
* // Create agent routing to SDS instead of session's default PDS
37+
* const sdsAgent = new ConfigurableAgent(session, "https://sds.hypercerts.org");
38+
*
39+
* // All requests will now go to the SDS
40+
* await sdsAgent.com.atproto.repo.createRecord({...});
41+
* ```
42+
*
43+
* @example Multiple SDS instances
44+
* ```typescript
45+
* // Route to organization A's SDS
46+
* const orgAAgent = new ConfigurableAgent(session, "https://sds-org-a.example.com");
47+
*
48+
* // Route to organization B's SDS
49+
* const orgBAgent = new ConfigurableAgent(session, "https://sds-org-b.example.com");
50+
* ```
51+
*/
52+
export class ConfigurableAgent extends Agent {
53+
private customServiceUrl: string;
54+
55+
/**
56+
* Creates a ConfigurableAgent that routes to a specific service URL.
57+
*
58+
* @param session - OAuth session for authentication
59+
* @param serviceUrl - Base URL of the server to route requests to
60+
*
61+
* @remarks
62+
* The agent wraps the session's fetch handler to intercept requests and
63+
* prepend the custom service URL instead of using the session's default.
64+
*/
65+
constructor(session: Session, serviceUrl: string) {
66+
// Create a custom fetch handler that uses our service URL
67+
const customFetchHandler: FetchHandler = async (pathname: string, init: RequestInit) => {
68+
// Construct the full URL with our custom service
69+
const url = new URL(pathname, serviceUrl).toString();
70+
71+
// Use the session's fetch handler for authentication (DPoP, etc.)
72+
return session.fetchHandler(url, init);
73+
};
74+
75+
// Initialize the parent Agent with our custom fetch handler
76+
super(customFetchHandler);
77+
78+
this.customServiceUrl = serviceUrl;
79+
}
80+
81+
/**
82+
* Gets the service URL this agent routes to.
83+
*
84+
* @returns The base URL of the configured service
85+
*/
86+
getServiceUrl(): string {
87+
return this.customServiceUrl;
88+
}
89+
}

packages/sdk-core/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ export type { AuthorizeOptions } from "./core/SDK.js";
1010
export type { ATProtoSDKConfig } from "./core/config.js";
1111
export type { Session } from "./core/types.js";
1212

13+
// Agent
14+
export { ConfigurableAgent } from "./agent/ConfigurableAgent.js";
15+
1316
// Repository (fluent API)
1417
export { Repository } from "./repository/Repository.js";
1518
export type {

packages/sdk-core/src/repository/Repository.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
* @packageDocumentation
88
*/
99

10-
import { Agent } from "@atproto/api";
1110
import { SDSRequiredError } from "../core/errors.js";
1211
import type { LoggerInterface } from "../core/interfaces.js";
1312
import type { Session } from "../core/types.js";
1413
import { HYPERCERT_LEXICONS } from "@hypercerts-org/lexicon";
14+
import { ConfigurableAgent } from "../agent/ConfigurableAgent.js";
15+
import type { Agent } from "@atproto/api";
1516
import type { LexiconRegistry } from "./LexiconRegistry.js";
1617

1718
// Types
@@ -179,12 +180,10 @@ export class Repository {
179180
this._isSDS = isSDS;
180181
this.logger = logger;
181182

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

189188
this.lexiconRegistry.addToAgent(this.agent);
190189

0 commit comments

Comments
 (0)