@hypercerts-org/sdk-core 0.6.0-beta.0 → 0.8.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,7 +44,100 @@ const claim = await repo.hypercerts.create({
44
44
 
45
45
  ## Core Concepts
46
46
 
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
48
141
 
49
142
  The SDK uses OAuth 2.0 for authentication with support for both PDS (Personal Data Server) and SDS (Shared Data Server).
50
143
 
@@ -67,7 +160,7 @@ const session = await sdk.restoreSession("did:plc:user123");
67
160
  const repo = sdk.getRepository(session);
68
161
  ```
69
162
 
70
- ### 2. Working with Hypercerts
163
+ ### 3. Working with Hypercerts
71
164
 
72
165
  #### Creating a Hypercert
73
166
 
@@ -144,7 +237,7 @@ await repo.hypercerts.delete(
144
237
  );
145
238
  ```
146
239
 
147
- ### 3. Contributions and Measurements
240
+ ### 4. Contributions and Measurements
148
241
 
149
242
  #### Adding Contributions
150
243
 
@@ -174,7 +267,7 @@ const measurement = await repo.hypercerts.addMeasurement({
174
267
  });
175
268
  ```
176
269
 
177
- ### 4. Blob Operations (Images & Files)
270
+ ### 5. Blob Operations (Images & Files)
178
271
 
179
272
  ```typescript
180
273
  // Upload an image or file
@@ -188,7 +281,7 @@ const blobData = await repo.blobs.get(
188
281
  );
189
282
  ```
190
283
 
191
- ### 5. Organizations (SDS only)
284
+ ### 6. Organizations (SDS only)
192
285
 
193
286
  Organizations allow multiple users to collaborate on shared repositories.
194
287
 
@@ -216,7 +309,7 @@ const org = await repo.organizations.get("did:plc:org123");
216
309
  console.log(`${org.name} - ${org.description}`);
217
310
  ```
218
311
 
219
- ### 6. Collaborator Management (SDS only)
312
+ ### 7. Collaborator Management (SDS only)
220
313
 
221
314
  Manage who has access to your repository and what they can do.
222
315
 
@@ -285,7 +378,7 @@ await repo.collaborators.transferOwnership({
285
378
  });
286
379
  ```
287
380
 
288
- ### 7. Generic Record Operations
381
+ ### 8. Generic Record Operations
289
382
 
290
383
  For working with any ATProto record type:
291
384
 
@@ -330,7 +423,7 @@ const { records, cursor } = await repo.records.list({
330
423
  });
331
424
  ```
332
425
 
333
- ### 8. Profile Management (PDS only)
426
+ ### 9. Profile Management (PDS only)
334
427
 
335
428
  ```typescript
336
429
  // Get user profile
@@ -457,6 +550,35 @@ try {
457
550
 
458
551
  ## Advanced Usage
459
552
 
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
+
460
582
  ### Custom Session Storage
461
583
 
462
584
  ```typescript
package/dist/index.cjs CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  var oauthClientNode = require('@atproto/oauth-client-node');
4
4
  var lexicon$1 = require('@atproto/lexicon');
5
- var api = require('@atproto/api');
6
5
  var lexicon = require('@hypercerts-org/lexicon');
6
+ var api = require('@atproto/api');
7
7
  var eventemitter3 = require('eventemitter3');
8
8
  var zod = require('zod');
9
9
 
@@ -1246,6 +1246,80 @@ class LexiconRegistry {
1246
1246
  }
1247
1247
  }
1248
1248
 
1249
+ /**
1250
+ * ConfigurableAgent - Agent with configurable service URL routing.
1251
+ *
1252
+ * This module provides an Agent extension that allows routing requests to
1253
+ * a specific server URL, overriding the default URL from the OAuth session.
1254
+ *
1255
+ * @packageDocumentation
1256
+ */
1257
+ /**
1258
+ * Agent subclass that routes requests to a configurable service URL.
1259
+ *
1260
+ * The standard Agent uses the service URL embedded in the OAuth session's
1261
+ * fetch handler. This class allows overriding that URL to route requests
1262
+ * to different servers (e.g., PDS vs SDS, or multiple SDS instances).
1263
+ *
1264
+ * @remarks
1265
+ * This is particularly useful for:
1266
+ * - Routing to a Shared Data Server (SDS) while authenticated via PDS
1267
+ * - Supporting multiple SDS instances for different organizations
1268
+ * - Testing against different server environments
1269
+ *
1270
+ * @example Basic usage
1271
+ * ```typescript
1272
+ * const session = await sdk.authorize("user.bsky.social");
1273
+ *
1274
+ * // Create agent routing to SDS instead of session's default PDS
1275
+ * const sdsAgent = new ConfigurableAgent(session, "https://sds.hypercerts.org");
1276
+ *
1277
+ * // All requests will now go to the SDS
1278
+ * await sdsAgent.com.atproto.repo.createRecord({...});
1279
+ * ```
1280
+ *
1281
+ * @example Multiple SDS instances
1282
+ * ```typescript
1283
+ * // Route to organization A's SDS
1284
+ * const orgAAgent = new ConfigurableAgent(session, "https://sds-org-a.example.com");
1285
+ *
1286
+ * // Route to organization B's SDS
1287
+ * const orgBAgent = new ConfigurableAgent(session, "https://sds-org-b.example.com");
1288
+ * ```
1289
+ */
1290
+ class ConfigurableAgent extends api.Agent {
1291
+ /**
1292
+ * Creates a ConfigurableAgent that routes to a specific service URL.
1293
+ *
1294
+ * @param session - OAuth session for authentication
1295
+ * @param serviceUrl - Base URL of the server to route requests to
1296
+ *
1297
+ * @remarks
1298
+ * The agent wraps the session's fetch handler to intercept requests and
1299
+ * prepend the custom service URL instead of using the session's default.
1300
+ */
1301
+ constructor(session, serviceUrl) {
1302
+ // Create a custom fetch handler that uses our service URL
1303
+ const customFetchHandler = async (pathname, init) => {
1304
+ // Construct the full URL with our custom service
1305
+ const url = new URL(pathname, serviceUrl).toString();
1306
+ // Use the session's fetch handler for authentication (DPoP, etc.)
1307
+ return session.fetchHandler(url, init);
1308
+ };
1309
+ // Initialize the parent Agent with our custom fetch handler
1310
+ super(customFetchHandler);
1311
+ this.customServiceUrl = serviceUrl;
1312
+ }
1313
+ /**
1314
+ * Gets the service URL this agent routes to.
1315
+ *
1316
+ * @returns The base URL of the configured service
1317
+ */
1318
+ getServiceUrl() {
1319
+ return this.customServiceUrl;
1320
+ }
1321
+ }
1322
+
1249
1323
  /**
1250
1324
  * RecordOperationsImpl - Low-level record CRUD operations.
1251
1325
  *
@@ -2196,6 +2270,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
2196
2270
  // Step 2: Create rights record
2197
2271
  this.emitProgress(params.onProgress, { name: "createRights", status: "start" });
2198
2272
  const rightsRecord = {
2273
+ $type: lexicon.HYPERCERT_COLLECTIONS.RIGHTS,
2199
2274
  rightsName: params.rights.name,
2200
2275
  rightsType: params.rights.type,
2201
2276
  rightsDescription: params.rights.description,
@@ -2224,6 +2299,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
2224
2299
  // Step 3: Create hypercert record
2225
2300
  this.emitProgress(params.onProgress, { name: "createHypercert", status: "start" });
2226
2301
  const hypercertRecord = {
2302
+ $type: lexicon.HYPERCERT_COLLECTIONS.CLAIM,
2227
2303
  title: params.title,
2228
2304
  description: params.description,
2229
2305
  workScope: params.workScope,
@@ -2571,6 +2647,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
2571
2647
  * await repo.hypercerts.attachLocation(hypercertUri, {
2572
2648
  * value: "San Francisco, CA",
2573
2649
  * name: "SF Bay Area",
2650
+ * srs: "EPSG:4326",
2574
2651
  * });
2575
2652
  * ```
2576
2653
  *
@@ -2589,32 +2666,54 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
2589
2666
  */
2590
2667
  async attachLocation(hypercertUri, location) {
2591
2668
  try {
2592
- // Get hypercert to get CID
2593
- const hypercert = await this.get(hypercertUri);
2669
+ // Validate required srs field
2670
+ if (!location.srs) {
2671
+ throw new ValidationError("srs (Spatial Reference System) is required. Example: 'EPSG:4326' for WGS84 coordinates, or 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' for CRS84.");
2672
+ }
2673
+ // Validate that hypercert exists (unused but confirms hypercert is valid)
2674
+ await this.get(hypercertUri);
2594
2675
  const createdAt = new Date().toISOString();
2595
- let locationValue = location.value;
2676
+ // Determine location type and prepare location data
2677
+ let locationData;
2678
+ let locationType;
2596
2679
  if (location.geojson) {
2680
+ // Upload GeoJSON as a blob
2597
2681
  const arrayBuffer = await location.geojson.arrayBuffer();
2598
2682
  const uint8Array = new Uint8Array(arrayBuffer);
2599
2683
  const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
2600
2684
  encoding: location.geojson.type || "application/geo+json",
2601
2685
  });
2602
2686
  if (uploadResult.success) {
2603
- locationValue = {
2687
+ locationData = {
2604
2688
  $type: "blob",
2605
2689
  ref: { $link: uploadResult.data.blob.ref.toString() },
2606
2690
  mimeType: uploadResult.data.blob.mimeType,
2607
2691
  size: uploadResult.data.blob.size,
2608
2692
  };
2693
+ locationType = "geojson-point";
2694
+ }
2695
+ else {
2696
+ throw new NetworkError("Failed to upload GeoJSON blob");
2609
2697
  }
2610
2698
  }
2699
+ else {
2700
+ // Use value as a URI reference
2701
+ locationData = {
2702
+ $type: "app.certified.defs#uri",
2703
+ uri: location.value,
2704
+ };
2705
+ locationType = "coordinate-decimal";
2706
+ }
2707
+ // Build location record according to app.certified.location lexicon
2611
2708
  const locationRecord = {
2612
- hypercert: { uri: hypercert.uri, cid: hypercert.cid },
2613
- value: locationValue,
2709
+ $type: lexicon.HYPERCERT_COLLECTIONS.LOCATION,
2710
+ lpVersion: "1.0",
2711
+ srs: location.srs,
2712
+ locationType,
2713
+ location: locationData,
2614
2714
  createdAt,
2615
2715
  name: location.name,
2616
2716
  description: location.description,
2617
- srs: location.srs,
2618
2717
  };
2619
2718
  const validation = this.lexiconRegistry.validate(lexicon.HYPERCERT_COLLECTIONS.LOCATION, locationRecord);
2620
2719
  if (!validation.valid) {
@@ -2701,10 +2800,12 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
2701
2800
  try {
2702
2801
  const createdAt = new Date().toISOString();
2703
2802
  const contributionRecord = {
2803
+ $type: lexicon.HYPERCERT_COLLECTIONS.CONTRIBUTION,
2704
2804
  contributors: params.contributors,
2705
2805
  role: params.role,
2706
2806
  createdAt,
2707
2807
  description: params.description,
2808
+ hypercert: { uri: "", cid: "" }, // Will be set below if hypercertUri provided
2708
2809
  };
2709
2810
  if (params.hypercertUri) {
2710
2811
  const hypercert = await this.get(params.hypercertUri);
@@ -2765,6 +2866,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
2765
2866
  const hypercert = await this.get(params.hypercertUri);
2766
2867
  const createdAt = new Date().toISOString();
2767
2868
  const measurementRecord = {
2869
+ $type: lexicon.HYPERCERT_COLLECTIONS.MEASUREMENT,
2768
2870
  hypercert: { uri: hypercert.uri, cid: hypercert.cid },
2769
2871
  measurers: params.measurers,
2770
2872
  metric: params.metric,
@@ -2820,6 +2922,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
2820
2922
  const subject = await this.get(params.subjectUri);
2821
2923
  const createdAt = new Date().toISOString();
2822
2924
  const evaluationRecord = {
2925
+ $type: lexicon.HYPERCERT_COLLECTIONS.EVALUATION,
2823
2926
  subject: { uri: subject.uri, cid: subject.cid },
2824
2927
  evaluators: params.evaluators,
2825
2928
  summary: params.summary,
@@ -2894,6 +2997,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
2894
2997
  }
2895
2998
  }
2896
2999
  const collectionRecord = {
3000
+ $type: lexicon.HYPERCERT_COLLECTIONS.COLLECTION,
2897
3001
  title: params.title,
2898
3002
  claims: params.claims.map((c) => ({ claim: { uri: c.uri, cid: c.cid }, weight: c.weight })),
2899
3003
  createdAt,
@@ -3772,11 +3876,10 @@ class Repository {
3772
3876
  this.lexiconRegistry = lexiconRegistry;
3773
3877
  this._isSDS = isSDS;
3774
3878
  this.logger = logger;
3775
- // Create Agent with OAuth session
3776
- this.agent = new api.Agent(session);
3777
- // Configure Agent to use the specified server URL (PDS or SDS)
3778
- // This ensures queries are routed to the correct server
3779
- this.agent.api.xrpc.uri = new URL(serverUrl);
3879
+ // Create a ConfigurableAgent that routes requests to the specified server URL
3880
+ // This allows routing to PDS, SDS, or any custom server while maintaining
3881
+ // the OAuth session's authentication
3882
+ this.agent = new ConfigurableAgent(session, serverUrl);
3780
3883
  this.lexiconRegistry.addToAgent(this.agent);
3781
3884
  // Register hypercert lexicons
3782
3885
  this.lexiconRegistry.registerMany(lexicon.HYPERCERT_LEXICONS);
@@ -4696,6 +4799,7 @@ exports.ATProtoSDKError = ATProtoSDKError;
4696
4799
  exports.AuthenticationError = AuthenticationError;
4697
4800
  exports.CollaboratorPermissionsSchema = CollaboratorPermissionsSchema;
4698
4801
  exports.CollaboratorSchema = CollaboratorSchema;
4802
+ exports.ConfigurableAgent = ConfigurableAgent;
4699
4803
  exports.InMemorySessionStore = InMemorySessionStore;
4700
4804
  exports.InMemoryStateStore = InMemoryStateStore;
4701
4805
  exports.LexiconRegistry = LexiconRegistry;