Skip to content

Commit 8c3c2a1

Browse files
authored
Merge pull request #127 from Kzoeps/75-sdk-operations-only-work-on-pds-registered-in-sdk-instance
feat(sdk-core): auto-detect PDS URL from OAuth session
2 parents 248d609 + 5662f3f commit 8c3c2a1

14 files changed

Lines changed: 441 additions & 124 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
"@hypercerts-org/sdk-core": minor
3+
---
4+
5+
Auto-detect user's PDS URL from OAuth session instead of requiring static configuration
6+
7+
**Breaking Changes:**
8+
9+
- `servers.pds` config option has been removed. The user's PDS URL is now automatically detected from the OAuth
10+
session's token info (`tokenInfo.aud`) during `callback()` and `restoreSession()`. This means the SDK correctly routes
11+
operations to each user's actual PDS regardless of which server they're hosted on.
12+
- A new `handleResolver` config option replaces `servers.pds` for its handle resolution role during OAuth authorization.
13+
This is optional — if omitted, DNS-based resolution is used.
14+
- `getAccountEmail()` no longer requires `servers.pds` to be configured, since it uses the session's fetch handler which
15+
internally routes to the correct PDS.
16+
- `sdk.repository()` is now async (returns `Promise<Repository>`). On cache miss it automatically resolves the PDS from
17+
the session's token info, so callers no longer need to manually call `resolveSessionPds()` first.
18+
19+
**New APIs:**
20+
21+
- `sdk.resolveSessionPds(session)` — Manually resolve and cache a session's PDS URL. Useful for pre-warming the cache or
22+
for sessions created outside the SDK's auth flow.
23+
24+
**Migration:**
25+
26+
```typescript
27+
// Before
28+
const sdk = createATProtoSDK({
29+
oauth: { ... },
30+
servers: { pds: "https://bsky.social", sds: "https://sds.example.com" },
31+
});
32+
33+
// After
34+
const sdk = createATProtoSDK({
35+
oauth: { ... },
36+
handleResolver: "https://bsky.social", // optional, for handle resolution only
37+
servers: { sds: "https://sds.example.com" },
38+
});
39+
```

CERTS_SDK.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Host a `client-metadata.json` file at a public URL (e.g., `https://your-app.com/
5353
# .env.local
5454
NEXT_PUBLIC_OAUTH_CLIENT_ID=https://your-app.com/client-metadata.json
5555
NEXT_PUBLIC_OAUTH_REDIRECT_URI=https://your-app.com/callback
56-
NEXT_PUBLIC_PDS_URL=https://bsky.social
56+
NEXT_PUBLIC_HANDLE_RESOLVER_URL=https://bsky.social
5757
NEXT_PUBLIC_SDS_URL=https://sds.hypercerts.org
5858
JWK_PRIVATE_KEY={"kty":"EC","crv":"P-256",...}
5959
```
@@ -86,8 +86,9 @@ export const atproto = createATProtoReact({
8686
redirectUri: process.env.NEXT_PUBLIC_OAUTH_REDIRECT_URI!,
8787
scope: "atproto transition:generic",
8888
},
89+
// Optional: URL for handle resolution during OAuth (defaults to DNS-based resolution)
90+
handleResolver: process.env.NEXT_PUBLIC_HANDLE_RESOLVER_URL || "https://bsky.social",
8991
servers: {
90-
pds: process.env.NEXT_PUBLIC_PDS_URL || "https://bsky.social",
9192
sds: process.env.NEXT_PUBLIC_SDS_URL,
9293
},
9394
},
@@ -565,8 +566,8 @@ const sdk = new ATProtoSDK({
565566
jwksUri: `${process.env.NEXT_PUBLIC_APP_URL}/.well-known/jwks.json`,
566567
jwkPrivate: process.env.JWK_PRIVATE_KEY!,
567568
},
569+
handleResolver: process.env.NEXT_PUBLIC_HANDLE_RESOLVER_URL || "https://bsky.social",
568570
servers: {
569-
pds: process.env.NEXT_PUBLIC_PDS_URL || "https://bsky.social",
570571
sds: process.env.NEXT_PUBLIC_SDS_URL,
571572
},
572573
});
@@ -673,8 +674,8 @@ export const atproto = createATProtoReact({
673674
redirectUri: process.env.NEXT_PUBLIC_OAUTH_REDIRECT_URI!,
674675
scope: "atproto transition:generic",
675676
},
677+
handleResolver: process.env.NEXT_PUBLIC_HANDLE_RESOLVER_URL || "https://bsky.social",
676678
servers: {
677-
pds: process.env.NEXT_PUBLIC_PDS_URL || "https://bsky.social",
678679
sds: process.env.NEXT_PUBLIC_SDS_URL,
679680
},
680681
},

packages/sdk-core/README.md

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,9 @@ const sdk = createATProtoSDK({
2020
jwksUri: "https://your-app.com/jwks.json",
2121
jwkPrivate: process.env.ATPROTO_JWK_PRIVATE!,
2222
},
23-
// set up with your pds url.
24-
// entryway doesn't work for this. Has to be a PDS URL.
25-
servers: {
26-
pds: "https://pds-eu-west4.test.certified.app",
27-
},
23+
// Optional: URL for handle resolution during OAuth.
24+
// If omitted, DNS-based resolution is used.
25+
handleResolver: "https://pds-eu-west4.test.certified.app",
2826
});
2927

3028
// 2. Authenticate user
@@ -80,10 +78,8 @@ const sdk = createATProtoSDK({
8078
// Optional: suppress warnings
8179
developmentMode: true,
8280
},
83-
servers: {
84-
// Point to local PDS for testing
85-
pds: "http://localhost:2583",
86-
},
81+
// Optional: handle resolver for local testing
82+
handleResolver: "http://localhost:2583",
8783
logger: console, // Enable debug logging
8884
});
8985

@@ -155,17 +151,17 @@ The SDK supports two types of AT Protocol servers:
155151
- **Purpose**: User's own data storage (e.g., Bluesky)
156152
- **Use case**: Individual hypercerts, personal records
157153
- **Features**: Profile management, basic CRUD operations
158-
- **Example**: `bsky.social`, any Bluesky PDS
154+
- **Auto-detected**: The SDK automatically discovers the user's PDS from the OAuth session -- no configuration needed
159155

160156
#### Shared Data Server (SDS)
161157

162158
- **Purpose**: Collaborative data storage with access control
163159
- **Use case**: Organization hypercerts, team collaboration
164160
- **Features**: Organizations, multi-user access, role-based permissions
165-
- **Example**: `sds.hypercerts.org`
161+
- **Configured via**: `servers.sds` in SDK config
166162

167163
```typescript
168-
// Connect to user's PDS (default)
164+
// Connect to user's PDS (default) -- auto-detected from session
169165
const pdsRepo = sdk.repository(session);
170166
await pdsRepo.hypercerts.create({ ... }); // Creates in user's PDS
171167

@@ -179,20 +175,34 @@ const orgRepo = sdsRepo.repo(orgs.organizations[0].did);
179175
await orgRepo.hypercerts.list(); // Queries organization's hypercerts on SDS
180176
```
181177

178+
#### How PDS Auto-Detection Works
179+
180+
The SDK automatically discovers each user's PDS URL from their OAuth session. You do not need to configure a PDS URL.
181+
182+
1. **During authentication** (`callback()` or `restoreSession()`), the SDK extracts the user's PDS URL from the OAuth
183+
token's `aud` field, which is resolved from the user's DID Document.
184+
185+
2. **When creating a repository** (`sdk.repository(session)`), the SDK uses the cached PDS URL for that session's DID.
186+
187+
3. **For sessions created outside the SDK**, you can manually resolve the PDS:
188+
```typescript
189+
await sdk.resolveSessionPds(session);
190+
```
191+
182192
#### How Repository Routing Works
183193

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

186196
1. **Initial Repository Creation**
187197

188198
```typescript
189-
// User authenticates (OAuth session knows user's PDS)
199+
// User authenticates -- PDS URL is automatically cached from the session
190200
const session = await sdk.callback(params);
191201

192-
// Create PDS repository - routes to user's PDS
202+
// Create PDS repository -- routes to user's auto-detected PDS
193203
const pdsRepo = sdk.repository(session);
194204

195-
// Create SDS repository - routes to SDS server
205+
// Create SDS repository -- routes to configured SDS server
196206
const sdsRepo = sdk.repository(session, { server: "sds" });
197207
```
198208

@@ -206,21 +216,22 @@ The SDK uses a `ConfigurableAgent` to route requests to different servers while
206216
const orgRepo = userSdsRepo.repo("did:plc:org-did");
207217

208218
// All operations on orgRepo still route to SDS, not user's PDS
209-
await orgRepo.hypercerts.list(); // Queries SDS
210-
await orgRepo.collaborators.list(); // Queries SDS
219+
await orgRepo.hypercerts.list(); // Queries SDS
220+
await orgRepo.collaborators.list(); // Queries SDS
211221
```
212222

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

220231
#### Common Patterns
221232

222233
```typescript
223-
// Pattern 1: Personal hypercerts on PDS
234+
// Pattern 1: Personal hypercerts on PDS (auto-detected)
224235
const myRepo = sdk.repository(session);
225236
await myRepo.hypercerts.create({ title: "My Personal Impact" });
226237

packages/sdk-core/src/auth/OAuthClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ interface AuthorizeOptions {
5252
* jwksUri: "https://my-app.com/.well-known/jwks.json",
5353
* jwkPrivate: process.env.JWK_PRIVATE_KEY!,
5454
* },
55-
* servers: { pds: "https://bsky.social" },
55+
* handleResolver: "https://pds-eu-west4.test.certified.app",
5656
* });
5757
*
5858
* // Start authorization
@@ -146,7 +146,7 @@ export class OAuthClient {
146146
keyset,
147147
stateStore: this.createStateStoreAdapter(stateStore),
148148
sessionStore: this.createSessionStoreAdapter(sessionStore),
149-
handleResolver: this.config.servers?.pds,
149+
handleResolver: this.config.handleResolver,
150150
fetch: this.config.fetch ?? fetchWithTimeout,
151151
});
152152

0 commit comments

Comments
 (0)