@hypercerts-org/sdk-core 0.2.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.
Files changed (83) hide show
  1. package/.turbo/turbo-build.log +328 -0
  2. package/.turbo/turbo-test.log +118 -0
  3. package/CHANGELOG.md +16 -0
  4. package/LICENSE +21 -0
  5. package/README.md +100 -0
  6. package/dist/errors.cjs +260 -0
  7. package/dist/errors.cjs.map +1 -0
  8. package/dist/errors.d.ts +233 -0
  9. package/dist/errors.mjs +253 -0
  10. package/dist/errors.mjs.map +1 -0
  11. package/dist/index.cjs +4531 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.ts +3430 -0
  14. package/dist/index.mjs +4448 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/dist/lexicons.cjs +420 -0
  17. package/dist/lexicons.cjs.map +1 -0
  18. package/dist/lexicons.d.ts +227 -0
  19. package/dist/lexicons.mjs +410 -0
  20. package/dist/lexicons.mjs.map +1 -0
  21. package/dist/storage.cjs +270 -0
  22. package/dist/storage.cjs.map +1 -0
  23. package/dist/storage.d.ts +474 -0
  24. package/dist/storage.mjs +267 -0
  25. package/dist/storage.mjs.map +1 -0
  26. package/dist/testing.cjs +415 -0
  27. package/dist/testing.cjs.map +1 -0
  28. package/dist/testing.d.ts +928 -0
  29. package/dist/testing.mjs +410 -0
  30. package/dist/testing.mjs.map +1 -0
  31. package/dist/types.cjs +220 -0
  32. package/dist/types.cjs.map +1 -0
  33. package/dist/types.d.ts +2118 -0
  34. package/dist/types.mjs +212 -0
  35. package/dist/types.mjs.map +1 -0
  36. package/eslint.config.mjs +22 -0
  37. package/package.json +90 -0
  38. package/rollup.config.js +75 -0
  39. package/src/auth/OAuthClient.ts +497 -0
  40. package/src/core/SDK.ts +410 -0
  41. package/src/core/config.ts +243 -0
  42. package/src/core/errors.ts +257 -0
  43. package/src/core/interfaces.ts +324 -0
  44. package/src/core/types.ts +281 -0
  45. package/src/errors.ts +57 -0
  46. package/src/index.ts +107 -0
  47. package/src/lexicons.ts +64 -0
  48. package/src/repository/BlobOperationsImpl.ts +199 -0
  49. package/src/repository/CollaboratorOperationsImpl.ts +288 -0
  50. package/src/repository/HypercertOperationsImpl.ts +1146 -0
  51. package/src/repository/LexiconRegistry.ts +332 -0
  52. package/src/repository/OrganizationOperationsImpl.ts +234 -0
  53. package/src/repository/ProfileOperationsImpl.ts +281 -0
  54. package/src/repository/RecordOperationsImpl.ts +340 -0
  55. package/src/repository/Repository.ts +482 -0
  56. package/src/repository/interfaces.ts +868 -0
  57. package/src/repository/types.ts +111 -0
  58. package/src/services/hypercerts/types.ts +87 -0
  59. package/src/storage/InMemorySessionStore.ts +127 -0
  60. package/src/storage/InMemoryStateStore.ts +146 -0
  61. package/src/storage.ts +63 -0
  62. package/src/testing/index.ts +67 -0
  63. package/src/testing/mocks.ts +142 -0
  64. package/src/testing/stores.ts +285 -0
  65. package/src/testing.ts +64 -0
  66. package/src/types.ts +86 -0
  67. package/tests/auth/OAuthClient.test.ts +164 -0
  68. package/tests/core/SDK.test.ts +176 -0
  69. package/tests/core/errors.test.ts +81 -0
  70. package/tests/repository/BlobOperationsImpl.test.ts +154 -0
  71. package/tests/repository/CollaboratorOperationsImpl.test.ts +323 -0
  72. package/tests/repository/HypercertOperationsImpl.test.ts +652 -0
  73. package/tests/repository/LexiconRegistry.test.ts +192 -0
  74. package/tests/repository/OrganizationOperationsImpl.test.ts +242 -0
  75. package/tests/repository/ProfileOperationsImpl.test.ts +254 -0
  76. package/tests/repository/RecordOperationsImpl.test.ts +375 -0
  77. package/tests/repository/Repository.test.ts +149 -0
  78. package/tests/utils/fixtures.ts +117 -0
  79. package/tests/utils/mocks.ts +109 -0
  80. package/tests/utils/repository-fixtures.ts +78 -0
  81. package/tsconfig.json +11 -0
  82. package/tsconfig.tsbuildinfo +1 -0
  83. package/vitest.config.ts +30 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,4531 @@
1
+ 'use strict';
2
+
3
+ var oauthClientNode = require('@atproto/oauth-client-node');
4
+ var lexicon$1 = require('@atproto/lexicon');
5
+ var api = require('@atproto/api');
6
+ var lexicon = require('@hypercerts-org/lexicon');
7
+ var eventemitter3 = require('eventemitter3');
8
+ var zod = require('zod');
9
+
10
+ /**
11
+ * Base error class for all SDK errors.
12
+ *
13
+ * All errors thrown by the Hypercerts SDK extend this class, making it easy
14
+ * to catch and handle SDK-specific errors.
15
+ *
16
+ * @example Catching all SDK errors
17
+ * ```typescript
18
+ * try {
19
+ * await sdk.authorize("user.bsky.social");
20
+ * } catch (error) {
21
+ * if (error instanceof ATProtoSDKError) {
22
+ * console.error(`SDK Error [${error.code}]: ${error.message}`);
23
+ * console.error(`HTTP Status: ${error.status}`);
24
+ * }
25
+ * }
26
+ * ```
27
+ *
28
+ * @example Checking error codes
29
+ * ```typescript
30
+ * try {
31
+ * await repo.records.get(collection, rkey);
32
+ * } catch (error) {
33
+ * if (error instanceof ATProtoSDKError) {
34
+ * switch (error.code) {
35
+ * case "AUTHENTICATION_ERROR":
36
+ * // Redirect to login
37
+ * break;
38
+ * case "VALIDATION_ERROR":
39
+ * // Show form errors
40
+ * break;
41
+ * case "NETWORK_ERROR":
42
+ * // Retry or show offline message
43
+ * break;
44
+ * }
45
+ * }
46
+ * }
47
+ * ```
48
+ */
49
+ class ATProtoSDKError extends Error {
50
+ /**
51
+ * Creates a new SDK error.
52
+ *
53
+ * @param message - Human-readable error description
54
+ * @param code - Machine-readable error code for programmatic handling
55
+ * @param status - HTTP status code associated with this error type
56
+ * @param cause - The underlying error that caused this error, if any
57
+ */
58
+ constructor(message, code, status, cause) {
59
+ super(message);
60
+ this.code = code;
61
+ this.status = status;
62
+ this.cause = cause;
63
+ this.name = "ATProtoSDKError";
64
+ Error.captureStackTrace?.(this, this.constructor);
65
+ }
66
+ }
67
+ /**
68
+ * Error thrown when authentication fails.
69
+ *
70
+ * This error indicates problems with the OAuth flow, invalid credentials,
71
+ * or failed token exchanges. Common causes:
72
+ * - Invalid authorization code
73
+ * - Expired or invalid state parameter
74
+ * - Revoked or invalid tokens
75
+ * - User denied authorization
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * try {
80
+ * const session = await sdk.callback(params);
81
+ * } catch (error) {
82
+ * if (error instanceof AuthenticationError) {
83
+ * // Clear any stored state and redirect to login
84
+ * console.error("Authentication failed:", error.message);
85
+ * }
86
+ * }
87
+ * ```
88
+ */
89
+ class AuthenticationError extends ATProtoSDKError {
90
+ /**
91
+ * Creates an authentication error.
92
+ *
93
+ * @param message - Description of what went wrong during authentication
94
+ * @param cause - The underlying error (e.g., from the OAuth client)
95
+ */
96
+ constructor(message, cause) {
97
+ super(message, "AUTHENTICATION_ERROR", 401, cause);
98
+ this.name = "AuthenticationError";
99
+ }
100
+ }
101
+ /**
102
+ * Error thrown when a session has expired and cannot be refreshed.
103
+ *
104
+ * This typically occurs when:
105
+ * - The refresh token has expired (usually after extended inactivity)
106
+ * - The user has revoked access to your application
107
+ * - The PDS has invalidated all sessions for the user
108
+ *
109
+ * When this error occurs, the user must re-authenticate.
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * try {
114
+ * const session = await sdk.restoreSession(did);
115
+ * } catch (error) {
116
+ * if (error instanceof SessionExpiredError) {
117
+ * // Clear stored session and prompt user to log in again
118
+ * localStorage.removeItem("userDid");
119
+ * window.location.href = "/login";
120
+ * }
121
+ * }
122
+ * ```
123
+ */
124
+ class SessionExpiredError extends ATProtoSDKError {
125
+ /**
126
+ * Creates a session expired error.
127
+ *
128
+ * @param message - Description of why the session expired
129
+ * @param cause - The underlying error from the token refresh attempt
130
+ */
131
+ constructor(message = "Session expired", cause) {
132
+ super(message, "SESSION_EXPIRED", 401, cause);
133
+ this.name = "SessionExpiredError";
134
+ }
135
+ }
136
+ /**
137
+ * Error thrown when input validation fails.
138
+ *
139
+ * This error indicates that provided data doesn't meet the required format
140
+ * or constraints. Common causes:
141
+ * - Missing required fields
142
+ * - Invalid URL formats
143
+ * - Invalid DID format
144
+ * - Schema validation failures for records
145
+ * - Invalid configuration values
146
+ *
147
+ * @example
148
+ * ```typescript
149
+ * try {
150
+ * await sdk.authorize(""); // Empty identifier
151
+ * } catch (error) {
152
+ * if (error instanceof ValidationError) {
153
+ * console.error("Invalid input:", error.message);
154
+ * // Show validation error to user
155
+ * }
156
+ * }
157
+ * ```
158
+ *
159
+ * @example With Zod validation cause
160
+ * ```typescript
161
+ * try {
162
+ * await repo.records.create(collection, record);
163
+ * } catch (error) {
164
+ * if (error instanceof ValidationError && error.cause) {
165
+ * // error.cause may be a ZodError with detailed field errors
166
+ * const zodError = error.cause as ZodError;
167
+ * zodError.errors.forEach(e => {
168
+ * console.error(`Field ${e.path.join(".")}: ${e.message}`);
169
+ * });
170
+ * }
171
+ * }
172
+ * ```
173
+ */
174
+ class ValidationError extends ATProtoSDKError {
175
+ /**
176
+ * Creates a validation error.
177
+ *
178
+ * @param message - Description of what validation failed
179
+ * @param cause - The underlying validation error (e.g., ZodError)
180
+ */
181
+ constructor(message, cause) {
182
+ super(message, "VALIDATION_ERROR", 400, cause);
183
+ this.name = "ValidationError";
184
+ }
185
+ }
186
+ /**
187
+ * Error thrown when a network request fails.
188
+ *
189
+ * This error indicates connectivity issues or server unavailability.
190
+ * Common causes:
191
+ * - No internet connection
192
+ * - DNS resolution failure
193
+ * - Server timeout
194
+ * - Server returned 5xx error
195
+ * - TLS/SSL errors
196
+ *
197
+ * These errors are typically transient and may succeed on retry.
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * try {
202
+ * await repo.records.list(collection);
203
+ * } catch (error) {
204
+ * if (error instanceof NetworkError) {
205
+ * // Implement retry logic or show offline indicator
206
+ * console.error("Network error:", error.message);
207
+ * await retryWithBackoff(() => repo.records.list(collection));
208
+ * }
209
+ * }
210
+ * ```
211
+ */
212
+ class NetworkError extends ATProtoSDKError {
213
+ /**
214
+ * Creates a network error.
215
+ *
216
+ * @param message - Description of the network failure
217
+ * @param cause - The underlying error (e.g., fetch error, timeout)
218
+ */
219
+ constructor(message, cause) {
220
+ super(message, "NETWORK_ERROR", 503, cause);
221
+ this.name = "NetworkError";
222
+ }
223
+ }
224
+ /**
225
+ * Error thrown when an SDS-only operation is attempted on a PDS.
226
+ *
227
+ * Certain operations are only available on Shared Data Servers (SDS),
228
+ * such as collaborator management and organization operations.
229
+ * This error is thrown when these operations are attempted on a
230
+ * Personal Data Server (PDS).
231
+ *
232
+ * @example
233
+ * ```typescript
234
+ * const pdsRepo = sdk.repository(session); // Default is PDS
235
+ *
236
+ * try {
237
+ * // This will throw SDSRequiredError
238
+ * await pdsRepo.collaborators.list();
239
+ * } catch (error) {
240
+ * if (error instanceof SDSRequiredError) {
241
+ * // Switch to SDS for this operation
242
+ * const sdsRepo = sdk.repository(session, { server: "sds" });
243
+ * const collaborators = await sdsRepo.collaborators.list();
244
+ * }
245
+ * }
246
+ * ```
247
+ */
248
+ class SDSRequiredError extends ATProtoSDKError {
249
+ /**
250
+ * Creates an SDS required error.
251
+ *
252
+ * @param message - Description of which operation requires SDS
253
+ * @param cause - Any underlying error
254
+ */
255
+ constructor(message = "This operation requires a Shared Data Server (SDS)", cause) {
256
+ super(message, "SDS_REQUIRED", 400, cause);
257
+ this.name = "SDSRequiredError";
258
+ }
259
+ }
260
+
261
+ /**
262
+ * In-memory implementation of the SessionStore interface.
263
+ *
264
+ * This store keeps OAuth sessions in memory using a Map. It's intended
265
+ * for development, testing, and simple use cases where session persistence
266
+ * across restarts is not required.
267
+ *
268
+ * @remarks
269
+ * **Warning**: This implementation is **not suitable for production** because:
270
+ * - Sessions are lost when the process restarts
271
+ * - Sessions cannot be shared across multiple server instances
272
+ * - No automatic cleanup of expired sessions
273
+ *
274
+ * For production, implement {@link SessionStore} with a persistent backend:
275
+ * - **Redis**: Good for distributed systems, supports TTL
276
+ * - **PostgreSQL/MySQL**: Good for existing database infrastructure
277
+ * - **MongoDB**: Good for document-based storage
278
+ *
279
+ * @example Basic usage
280
+ * ```typescript
281
+ * import { InMemorySessionStore } from "@hypercerts-org/sdk/storage";
282
+ *
283
+ * const sessionStore = new InMemorySessionStore();
284
+ *
285
+ * const sdk = new ATProtoSDK({
286
+ * oauth: { ... },
287
+ * storage: {
288
+ * sessionStore, // Will warn in logs for production
289
+ * },
290
+ * });
291
+ * ```
292
+ *
293
+ * @example Testing usage
294
+ * ```typescript
295
+ * const sessionStore = new InMemorySessionStore();
296
+ *
297
+ * // After tests, clean up
298
+ * sessionStore.clear();
299
+ * ```
300
+ *
301
+ * @see {@link SessionStore} for the interface definition
302
+ * @see {@link InMemoryStateStore} for the corresponding state store
303
+ */
304
+ class InMemorySessionStore {
305
+ constructor() {
306
+ /**
307
+ * Internal storage for sessions, keyed by DID.
308
+ * @internal
309
+ */
310
+ this.sessions = new Map();
311
+ }
312
+ /**
313
+ * Retrieves a session by DID.
314
+ *
315
+ * @param did - The user's Decentralized Identifier
316
+ * @returns Promise resolving to the session, or `undefined` if not found
317
+ *
318
+ * @example
319
+ * ```typescript
320
+ * const session = await sessionStore.get("did:plc:abc123");
321
+ * if (session) {
322
+ * console.log("Session found");
323
+ * }
324
+ * ```
325
+ */
326
+ async get(did) {
327
+ return this.sessions.get(did);
328
+ }
329
+ /**
330
+ * Stores or updates a session.
331
+ *
332
+ * @param did - The user's DID to use as the key
333
+ * @param session - The session data to store
334
+ *
335
+ * @remarks
336
+ * If a session already exists for the DID, it is overwritten.
337
+ *
338
+ * @example
339
+ * ```typescript
340
+ * await sessionStore.set("did:plc:abc123", sessionData);
341
+ * ```
342
+ */
343
+ async set(did, session) {
344
+ this.sessions.set(did, session);
345
+ }
346
+ /**
347
+ * Deletes a session by DID.
348
+ *
349
+ * @param did - The DID of the session to delete
350
+ *
351
+ * @remarks
352
+ * If no session exists for the DID, this is a no-op.
353
+ *
354
+ * @example
355
+ * ```typescript
356
+ * await sessionStore.del("did:plc:abc123");
357
+ * ```
358
+ */
359
+ async del(did) {
360
+ this.sessions.delete(did);
361
+ }
362
+ /**
363
+ * Clears all stored sessions.
364
+ *
365
+ * This is primarily useful for testing to ensure a clean state
366
+ * between test runs.
367
+ *
368
+ * @remarks
369
+ * This method is synchronous (not async) for convenience in test cleanup.
370
+ *
371
+ * @example
372
+ * ```typescript
373
+ * // In test teardown
374
+ * afterEach(() => {
375
+ * sessionStore.clear();
376
+ * });
377
+ * ```
378
+ */
379
+ clear() {
380
+ this.sessions.clear();
381
+ }
382
+ }
383
+
384
+ /**
385
+ * In-memory implementation of the StateStore interface.
386
+ *
387
+ * This store keeps OAuth state parameters in memory using a Map. State is
388
+ * used during the OAuth authorization flow for CSRF protection and PKCE.
389
+ *
390
+ * @remarks
391
+ * **Warning**: This implementation is **not suitable for production** because:
392
+ * - State is lost when the process restarts (breaking in-progress OAuth flows)
393
+ * - State cannot be shared across multiple server instances
394
+ * - No automatic cleanup of expired state (memory leak potential)
395
+ *
396
+ * For production, implement {@link StateStore} with a persistent backend
397
+ * that supports TTL (time-to-live):
398
+ * - **Redis**: Ideal choice with built-in TTL support
399
+ * - **Database with cleanup job**: PostgreSQL/MySQL with periodic cleanup
400
+ *
401
+ * **State Lifecycle**:
402
+ * 1. Created when user starts OAuth flow (`authorize()`)
403
+ * 2. Retrieved and validated during callback
404
+ * 3. Deleted after successful or failed callback
405
+ * 4. Should expire after ~15 minutes if callback never happens
406
+ *
407
+ * @example Basic usage
408
+ * ```typescript
409
+ * import { InMemoryStateStore } from "@hypercerts-org/sdk/storage";
410
+ *
411
+ * const stateStore = new InMemoryStateStore();
412
+ *
413
+ * const sdk = new ATProtoSDK({
414
+ * oauth: { ... },
415
+ * storage: {
416
+ * stateStore, // Will warn in logs for production
417
+ * },
418
+ * });
419
+ * ```
420
+ *
421
+ * @example Testing usage
422
+ * ```typescript
423
+ * const stateStore = new InMemoryStateStore();
424
+ *
425
+ * // After tests, clean up
426
+ * stateStore.clear();
427
+ * ```
428
+ *
429
+ * @see {@link StateStore} for the interface definition
430
+ * @see {@link InMemorySessionStore} for the corresponding session store
431
+ */
432
+ class InMemoryStateStore {
433
+ constructor() {
434
+ /**
435
+ * Internal storage for OAuth state, keyed by state string.
436
+ * @internal
437
+ */
438
+ this.states = new Map();
439
+ }
440
+ /**
441
+ * Retrieves OAuth state by key.
442
+ *
443
+ * @param key - The state key (random string from authorization URL)
444
+ * @returns Promise resolving to the state, or `undefined` if not found
445
+ *
446
+ * @remarks
447
+ * The key is a cryptographically random string generated during
448
+ * the authorization request. It's included in the callback URL
449
+ * and used to retrieve the associated PKCE verifier and other data.
450
+ *
451
+ * @example
452
+ * ```typescript
453
+ * // During OAuth callback
454
+ * const state = await stateStore.get(params.get("state")!);
455
+ * if (!state) {
456
+ * throw new Error("Invalid or expired state");
457
+ * }
458
+ * ```
459
+ */
460
+ async get(key) {
461
+ return this.states.get(key);
462
+ }
463
+ /**
464
+ * Stores OAuth state temporarily.
465
+ *
466
+ * @param key - The state key to use for storage
467
+ * @param state - The OAuth state data (includes PKCE verifier, etc.)
468
+ *
469
+ * @remarks
470
+ * In production implementations, state should be stored with a TTL
471
+ * of approximately 10-15 minutes to prevent stale state accumulation.
472
+ *
473
+ * @example
474
+ * ```typescript
475
+ * // Called internally by OAuthClient during authorize()
476
+ * await stateStore.set(stateKey, {
477
+ * // PKCE code verifier, redirect URI, etc.
478
+ * });
479
+ * ```
480
+ */
481
+ async set(key, state) {
482
+ this.states.set(key, state);
483
+ }
484
+ /**
485
+ * Deletes OAuth state by key.
486
+ *
487
+ * @param key - The state key to delete
488
+ *
489
+ * @remarks
490
+ * Called after the OAuth callback is processed (whether successful or not)
491
+ * to clean up the temporary state.
492
+ *
493
+ * @example
494
+ * ```typescript
495
+ * // After processing callback
496
+ * await stateStore.del(stateKey);
497
+ * ```
498
+ */
499
+ async del(key) {
500
+ this.states.delete(key);
501
+ }
502
+ /**
503
+ * Clears all stored state.
504
+ *
505
+ * This is primarily useful for testing to ensure a clean state
506
+ * between test runs.
507
+ *
508
+ * @remarks
509
+ * This method is synchronous (not async) for convenience in test cleanup.
510
+ * In production, be careful using this as it will invalidate all
511
+ * in-progress OAuth flows.
512
+ *
513
+ * @example
514
+ * ```typescript
515
+ * // In test teardown
516
+ * afterEach(() => {
517
+ * stateStore.clear();
518
+ * });
519
+ * ```
520
+ */
521
+ clear() {
522
+ this.states.clear();
523
+ }
524
+ }
525
+
526
+ /**
527
+ * OAuth 2.0 client for AT Protocol authentication with DPoP support.
528
+ *
529
+ * This class wraps the `@atproto/oauth-client-node` library to provide
530
+ * OAuth 2.0 authentication with the following features:
531
+ *
532
+ * - **DPoP (Demonstrating Proof of Possession)**: Binds tokens to cryptographic keys
533
+ * to prevent token theft and replay attacks
534
+ * - **PKCE (Proof Key for Code Exchange)**: Protects against authorization code interception
535
+ * - **Automatic Token Refresh**: Transparently refreshes expired access tokens
536
+ * - **Session Persistence**: Stores sessions in configurable storage backends
537
+ *
538
+ * @remarks
539
+ * This class is typically used internally by {@link ATProtoSDK}. Direct usage
540
+ * is only needed for advanced scenarios.
541
+ *
542
+ * The client uses lazy initialization - the underlying `NodeOAuthClient` is
543
+ * created asynchronously on first use. This allows the constructor to return
544
+ * synchronously while deferring async key parsing.
545
+ *
546
+ * @example Direct usage (advanced)
547
+ * ```typescript
548
+ * import { OAuthClient } from "@hypercerts-org/sdk";
549
+ *
550
+ * const client = new OAuthClient({
551
+ * oauth: {
552
+ * clientId: "https://my-app.com/client-metadata.json",
553
+ * redirectUri: "https://my-app.com/callback",
554
+ * scope: "atproto transition:generic",
555
+ * jwksUri: "https://my-app.com/.well-known/jwks.json",
556
+ * jwkPrivate: process.env.JWK_PRIVATE_KEY!,
557
+ * },
558
+ * servers: { pds: "https://bsky.social" },
559
+ * });
560
+ *
561
+ * // Start authorization
562
+ * const authUrl = await client.authorize("user.bsky.social");
563
+ *
564
+ * // Handle callback
565
+ * const session = await client.callback(new URLSearchParams(callbackUrl.search));
566
+ * ```
567
+ *
568
+ * @see {@link ATProtoSDK} for the recommended high-level API
569
+ * @see https://atproto.com/specs/oauth for AT Protocol OAuth specification
570
+ */
571
+ class OAuthClient {
572
+ /**
573
+ * Creates a new OAuth client.
574
+ *
575
+ * @param config - SDK configuration including OAuth credentials and server URLs
576
+ * @throws {@link AuthenticationError} if the JWK private key is not valid JSON
577
+ *
578
+ * @remarks
579
+ * The constructor validates the JWK format synchronously but defers
580
+ * the actual client initialization to the first API call.
581
+ */
582
+ constructor(config) {
583
+ /** The underlying NodeOAuthClient instance (lazily initialized) */
584
+ this.client = null;
585
+ this.config = config;
586
+ this.logger = config.logger;
587
+ // Validate JWK format synchronously (before async initialization)
588
+ try {
589
+ JSON.parse(config.oauth.jwkPrivate);
590
+ }
591
+ catch (error) {
592
+ throw new AuthenticationError("Failed to parse JWK private key. Ensure it is valid JSON.", error);
593
+ }
594
+ // Initialize client lazily (async initialization)
595
+ this.clientPromise = this.initializeClient();
596
+ }
597
+ /**
598
+ * Initializes the NodeOAuthClient asynchronously.
599
+ *
600
+ * This method is called lazily on first use. It:
601
+ * 1. Parses the JWK private key(s)
602
+ * 2. Builds OAuth client metadata
603
+ * 3. Creates the underlying NodeOAuthClient
604
+ *
605
+ * @returns Promise resolving to the initialized client
606
+ * @internal
607
+ */
608
+ async initializeClient() {
609
+ if (this.client) {
610
+ return this.client;
611
+ }
612
+ // Parse JWK private key (already validated in constructor)
613
+ const privateJWK = JSON.parse(this.config.oauth.jwkPrivate);
614
+ // Build client metadata
615
+ const clientMetadata = this.buildClientMetadata();
616
+ // Convert JWK keys to JoseKey instances (await here)
617
+ const keyset = await Promise.all(privateJWK.keys.map((key) => oauthClientNode.JoseKey.fromImportable(key, key.kid)));
618
+ // Create fetch with timeout
619
+ const fetchWithTimeout = this.createFetchWithTimeout(this.config.timeouts?.pdsMetadata ?? 30000);
620
+ // Use provided stores or fall back to in-memory implementations
621
+ const stateStore = this.config.storage?.stateStore ?? new InMemoryStateStore();
622
+ const sessionStore = this.config.storage?.sessionStore ?? new InMemorySessionStore();
623
+ this.client = new oauthClientNode.NodeOAuthClient({
624
+ clientMetadata,
625
+ keyset,
626
+ stateStore: this.createStateStoreAdapter(stateStore),
627
+ sessionStore: this.createSessionStoreAdapter(sessionStore),
628
+ handleResolver: this.config.servers?.pds,
629
+ fetch: this.config.fetch ?? fetchWithTimeout,
630
+ });
631
+ return this.client;
632
+ }
633
+ /**
634
+ * Gets the OAuth client instance, initializing if needed.
635
+ *
636
+ * @returns Promise resolving to the initialized client
637
+ * @internal
638
+ */
639
+ async getClient() {
640
+ return this.clientPromise;
641
+ }
642
+ /**
643
+ * Builds OAuth client metadata from configuration.
644
+ *
645
+ * The metadata describes your application to the authorization server
646
+ * and must match what's published at your `clientId` URL.
647
+ *
648
+ * @returns OAuth client metadata object
649
+ * @internal
650
+ *
651
+ * @remarks
652
+ * Key metadata fields:
653
+ * - `client_id`: URL to your client metadata JSON
654
+ * - `redirect_uris`: Where to redirect after auth (must match config)
655
+ * - `dpop_bound_access_tokens`: Always true for AT Protocol
656
+ * - `token_endpoint_auth_method`: Uses private_key_jwt for security
657
+ */
658
+ buildClientMetadata() {
659
+ const clientIdUrl = new URL(this.config.oauth.clientId);
660
+ return {
661
+ client_id: this.config.oauth.clientId,
662
+ client_name: "ATProto SDK Client",
663
+ client_uri: clientIdUrl.origin,
664
+ redirect_uris: [this.config.oauth.redirectUri],
665
+ scope: this.config.oauth.scope,
666
+ grant_types: ["authorization_code", "refresh_token"],
667
+ response_types: ["code"],
668
+ application_type: "web",
669
+ token_endpoint_auth_method: "private_key_jwt",
670
+ token_endpoint_auth_signing_alg: "ES256",
671
+ dpop_bound_access_tokens: true,
672
+ jwks_uri: this.config.oauth.jwksUri,
673
+ };
674
+ }
675
+ /**
676
+ * Creates a fetch handler with timeout support.
677
+ *
678
+ * @param timeoutMs - Request timeout in milliseconds
679
+ * @returns A fetch function that aborts after the timeout
680
+ * @internal
681
+ */
682
+ createFetchWithTimeout(timeoutMs) {
683
+ return async (input, init) => {
684
+ const controller = new AbortController();
685
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
686
+ try {
687
+ const response = await fetch(input, {
688
+ ...init,
689
+ signal: controller.signal,
690
+ });
691
+ clearTimeout(timeoutId);
692
+ return response;
693
+ }
694
+ catch (error) {
695
+ clearTimeout(timeoutId);
696
+ if (error instanceof Error && error.name === "AbortError") {
697
+ throw new NetworkError(`Request timeout after ${timeoutMs}ms`, error);
698
+ }
699
+ throw new NetworkError("Network request failed", error);
700
+ }
701
+ };
702
+ }
703
+ /**
704
+ * Creates a state store adapter compatible with NodeOAuthClient.
705
+ *
706
+ * @param store - The StateStore implementation to adapt
707
+ * @returns An adapter compatible with NodeOAuthClient
708
+ * @internal
709
+ */
710
+ createStateStoreAdapter(store) {
711
+ return {
712
+ get: (key) => store.get(key),
713
+ set: (key, value) => store.set(key, value),
714
+ del: (key) => store.del(key),
715
+ };
716
+ }
717
+ /**
718
+ * Creates a session store adapter compatible with NodeOAuthClient.
719
+ *
720
+ * @param store - The SessionStore implementation to adapt
721
+ * @returns An adapter compatible with NodeOAuthClient
722
+ * @internal
723
+ */
724
+ createSessionStoreAdapter(store) {
725
+ return {
726
+ get: (did) => store.get(did),
727
+ set: (did, session) => store.set(did, session),
728
+ del: (did) => store.del(did),
729
+ };
730
+ }
731
+ /**
732
+ * Initiates the OAuth authorization flow.
733
+ *
734
+ * This method resolves the user's identity from their identifier,
735
+ * generates PKCE codes, creates OAuth state, and returns an
736
+ * authorization URL to redirect the user to.
737
+ *
738
+ * @param identifier - The user's ATProto identifier. Accepts:
739
+ * - Handle (e.g., `"alice.bsky.social"`)
740
+ * - DID (e.g., `"did:plc:abc123..."`)
741
+ * - PDS URL (e.g., `"https://bsky.social"`)
742
+ * @param options - Optional authorization settings
743
+ * @returns A Promise resolving to the authorization URL
744
+ * @throws {@link AuthenticationError} if authorization setup fails
745
+ * @throws {@link NetworkError} if identity resolution fails
746
+ *
747
+ * @example
748
+ * ```typescript
749
+ * // Get authorization URL
750
+ * const authUrl = await client.authorize("user.bsky.social");
751
+ *
752
+ * // Redirect user (in a web app)
753
+ * window.location.href = authUrl;
754
+ *
755
+ * // Or return to client (in an API)
756
+ * res.json({ authUrl });
757
+ * ```
758
+ */
759
+ async authorize(identifier, options) {
760
+ try {
761
+ this.logger?.debug("Initiating OAuth authorization", { identifier });
762
+ const client = await this.getClient();
763
+ const scope = options?.scope ?? this.config.oauth.scope;
764
+ const authUrl = await client.authorize(identifier, { scope });
765
+ this.logger?.debug("Authorization URL generated", { identifier });
766
+ // Convert URL to string if needed
767
+ return typeof authUrl === "string" ? authUrl : authUrl.toString();
768
+ }
769
+ catch (error) {
770
+ this.logger?.error("Authorization failed", { identifier, error });
771
+ if (error instanceof NetworkError || error instanceof AuthenticationError) {
772
+ throw error;
773
+ }
774
+ throw new AuthenticationError(`Failed to initiate authorization: ${error instanceof Error ? error.message : String(error)}`, error);
775
+ }
776
+ }
777
+ /**
778
+ * Handles the OAuth callback and exchanges the authorization code for tokens.
779
+ *
780
+ * Call this method when the user is redirected back to your application.
781
+ * It validates the state, exchanges the code for tokens, and creates
782
+ * a persistent session.
783
+ *
784
+ * @param params - URL search parameters from the callback. Expected parameters:
785
+ * - `code`: The authorization code
786
+ * - `state`: The state parameter (for CSRF protection)
787
+ * - `iss`: The issuer (authorization server URL)
788
+ * @returns A Promise resolving to the authenticated OAuth session
789
+ * @throws {@link AuthenticationError} if:
790
+ * - The callback contains an OAuth error
791
+ * - The state is invalid or expired
792
+ * - The code exchange fails
793
+ * - Session persistence fails
794
+ *
795
+ * @example
796
+ * ```typescript
797
+ * // In your callback route handler
798
+ * app.get("/callback", async (req, res) => {
799
+ * const params = new URLSearchParams(req.url.split("?")[1]);
800
+ *
801
+ * try {
802
+ * const session = await client.callback(params);
803
+ * // Store DID for session restoration
804
+ * req.session.userDid = session.sub;
805
+ * res.redirect("/dashboard");
806
+ * } catch (error) {
807
+ * res.redirect("/login?error=auth_failed");
808
+ * }
809
+ * });
810
+ * ```
811
+ *
812
+ * @remarks
813
+ * After successful token exchange, this method verifies that the session
814
+ * was properly persisted by attempting to restore it. This ensures the
815
+ * storage backend is working correctly.
816
+ */
817
+ async callback(params) {
818
+ try {
819
+ this.logger?.debug("Processing OAuth callback");
820
+ // Check for OAuth errors
821
+ const error = params.get("error");
822
+ if (error) {
823
+ const errorDescription = params.get("error_description");
824
+ throw new AuthenticationError(errorDescription || error);
825
+ }
826
+ const client = await this.getClient();
827
+ const result = await client.callback(params);
828
+ const session = result.session;
829
+ const did = session.sub;
830
+ this.logger?.info("OAuth callback successful", { did });
831
+ // Verify session can be restored (validates persistence)
832
+ try {
833
+ const restored = await client.restore(did);
834
+ if (!restored) {
835
+ throw new AuthenticationError("OAuth session was not persisted");
836
+ }
837
+ this.logger?.debug("Session verified and restorable", { did });
838
+ }
839
+ catch (restoreError) {
840
+ this.logger?.error("Failed to verify persisted session", {
841
+ did,
842
+ error: restoreError,
843
+ });
844
+ throw new AuthenticationError("Failed to persist OAuth session", restoreError);
845
+ }
846
+ return session;
847
+ }
848
+ catch (error) {
849
+ this.logger?.error("OAuth callback failed", { error });
850
+ if (error instanceof AuthenticationError) {
851
+ throw error;
852
+ }
853
+ throw new AuthenticationError(`OAuth callback failed: ${error instanceof Error ? error.message : String(error)}`, error);
854
+ }
855
+ }
856
+ /**
857
+ * Restores an OAuth session by DID.
858
+ *
859
+ * Use this method to restore a previously authenticated session.
860
+ * The method automatically refreshes expired access tokens using
861
+ * the stored refresh token.
862
+ *
863
+ * @param did - The user's Decentralized Identifier (e.g., `"did:plc:abc123..."`)
864
+ * @returns A Promise resolving to the session, or `null` if not found
865
+ * @throws {@link AuthenticationError} if session restoration fails (not for missing sessions)
866
+ * @throws {@link NetworkError} if token refresh requires network and fails
867
+ *
868
+ * @example
869
+ * ```typescript
870
+ * // On application startup or request
871
+ * const userDid = req.session.userDid;
872
+ * if (userDid) {
873
+ * const session = await client.restore(userDid);
874
+ * if (session) {
875
+ * // Session restored, user is authenticated
876
+ * req.atprotoSession = session;
877
+ * } else {
878
+ * // No session found, user needs to log in
879
+ * delete req.session.userDid;
880
+ * }
881
+ * }
882
+ * ```
883
+ *
884
+ * @remarks
885
+ * Token refresh is handled automatically by the underlying OAuth client.
886
+ * If the refresh token has expired or been revoked, this method will
887
+ * throw an {@link AuthenticationError}.
888
+ */
889
+ async restore(did) {
890
+ try {
891
+ this.logger?.debug("Restoring session", { did });
892
+ const client = await this.getClient();
893
+ const session = await client.restore(did);
894
+ if (session) {
895
+ this.logger?.debug("Session restored", { did });
896
+ }
897
+ else {
898
+ this.logger?.debug("No session found", { did });
899
+ }
900
+ return session;
901
+ }
902
+ catch (error) {
903
+ this.logger?.error("Failed to restore session", { did, error });
904
+ if (error instanceof NetworkError) {
905
+ throw error;
906
+ }
907
+ throw new AuthenticationError(`Failed to restore session: ${error instanceof Error ? error.message : String(error)}`, error);
908
+ }
909
+ }
910
+ /**
911
+ * Revokes an OAuth session.
912
+ *
913
+ * This method invalidates the session's tokens both locally and
914
+ * (if supported) on the authorization server. After revocation,
915
+ * the session cannot be restored.
916
+ *
917
+ * @param did - The user's DID to revoke
918
+ * @throws {@link AuthenticationError} if revocation fails
919
+ *
920
+ * @example
921
+ * ```typescript
922
+ * // Log out endpoint
923
+ * app.post("/logout", async (req, res) => {
924
+ * const userDid = req.session.userDid;
925
+ * if (userDid) {
926
+ * await client.revoke(userDid);
927
+ * delete req.session.userDid;
928
+ * }
929
+ * res.redirect("/");
930
+ * });
931
+ * ```
932
+ *
933
+ * @remarks
934
+ * Even if revocation fails on the server, the local session is
935
+ * removed. The error is thrown to inform you that remote revocation
936
+ * may not have succeeded.
937
+ */
938
+ async revoke(did) {
939
+ try {
940
+ this.logger?.debug("Revoking session", { did });
941
+ const client = await this.getClient();
942
+ await client.revoke(did);
943
+ this.logger?.info("Session revoked", { did });
944
+ }
945
+ catch (error) {
946
+ this.logger?.error("Failed to revoke session", { did, error });
947
+ throw new AuthenticationError(`Failed to revoke session: ${error instanceof Error ? error.message : String(error)}`, error);
948
+ }
949
+ }
950
+ }
951
+
952
+ /**
953
+ * Registry for managing and validating AT Protocol lexicon schemas.
954
+ *
955
+ * Lexicons are schema definitions that describe the structure of records
956
+ * in the AT Protocol. This registry allows you to:
957
+ *
958
+ * - Register custom lexicons for your application's record types
959
+ * - Validate records against their lexicon schemas
960
+ * - Extend the AT Protocol Agent with custom lexicon support
961
+ *
962
+ * @remarks
963
+ * The SDK automatically registers hypercert lexicons when creating a Repository.
964
+ * You only need to use this class directly if you're working with custom
965
+ * record types.
966
+ *
967
+ * **Lexicon IDs** follow the NSID (Namespaced Identifier) format:
968
+ * `{authority}.{name}` (e.g., `org.hypercerts.hypercert`)
969
+ *
970
+ * @example Registering custom lexicons
971
+ * ```typescript
972
+ * const registry = sdk.getLexiconRegistry();
973
+ *
974
+ * // Register a single lexicon
975
+ * registry.register({
976
+ * lexicon: 1,
977
+ * id: "org.example.myRecord",
978
+ * defs: {
979
+ * main: {
980
+ * type: "record",
981
+ * key: "tid",
982
+ * record: {
983
+ * type: "object",
984
+ * required: ["title", "createdAt"],
985
+ * properties: {
986
+ * title: { type: "string" },
987
+ * description: { type: "string" },
988
+ * createdAt: { type: "string", format: "datetime" },
989
+ * },
990
+ * },
991
+ * },
992
+ * },
993
+ * });
994
+ *
995
+ * // Register multiple lexicons at once
996
+ * registry.registerMany([lexicon1, lexicon2, lexicon3]);
997
+ * ```
998
+ *
999
+ * @example Validating records
1000
+ * ```typescript
1001
+ * const result = registry.validate("org.example.myRecord", {
1002
+ * title: "Test",
1003
+ * createdAt: new Date().toISOString(),
1004
+ * });
1005
+ *
1006
+ * if (!result.valid) {
1007
+ * console.error(`Validation failed: ${result.error}`);
1008
+ * }
1009
+ * ```
1010
+ *
1011
+ * @see https://atproto.com/specs/lexicon for the Lexicon specification
1012
+ */
1013
+ class LexiconRegistry {
1014
+ /**
1015
+ * Creates a new LexiconRegistry.
1016
+ *
1017
+ * The registry starts empty. Use {@link register} or {@link registerMany}
1018
+ * to add lexicons.
1019
+ */
1020
+ constructor() {
1021
+ /** Map of lexicon ID to lexicon document */
1022
+ this.lexicons = new Map();
1023
+ this.lexiconsCollection = new lexicon$1.Lexicons();
1024
+ }
1025
+ /**
1026
+ * Registers a single lexicon schema.
1027
+ *
1028
+ * @param lexicon - The lexicon document to register
1029
+ * @throws {@link ValidationError} if the lexicon doesn't have an `id` field
1030
+ *
1031
+ * @remarks
1032
+ * If a lexicon with the same ID is already registered, it will be
1033
+ * replaced with the new definition. This is useful for testing but
1034
+ * should generally be avoided in production.
1035
+ *
1036
+ * @example
1037
+ * ```typescript
1038
+ * registry.register({
1039
+ * lexicon: 1,
1040
+ * id: "org.example.post",
1041
+ * defs: {
1042
+ * main: {
1043
+ * type: "record",
1044
+ * key: "tid",
1045
+ * record: {
1046
+ * type: "object",
1047
+ * required: ["text", "createdAt"],
1048
+ * properties: {
1049
+ * text: { type: "string", maxLength: 300 },
1050
+ * createdAt: { type: "string", format: "datetime" },
1051
+ * },
1052
+ * },
1053
+ * },
1054
+ * },
1055
+ * });
1056
+ * ```
1057
+ */
1058
+ register(lexicon) {
1059
+ if (!lexicon.id) {
1060
+ throw new ValidationError("Lexicon must have an 'id' field");
1061
+ }
1062
+ // Remove existing lexicon if present (to allow overwriting)
1063
+ if (this.lexicons.has(lexicon.id)) {
1064
+ // Lexicons collection doesn't support removal, so we create a new one
1065
+ // This is a limitation - in practice, lexicons shouldn't be overwritten
1066
+ // But we allow it for testing and flexibility
1067
+ const existingLexicon = this.lexicons.get(lexicon.id);
1068
+ if (existingLexicon) {
1069
+ // Try to remove from collection (may fail if not supported)
1070
+ try {
1071
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1072
+ this.lexiconsCollection.remove?.(lexicon.id);
1073
+ }
1074
+ catch {
1075
+ // If removal fails, create a new collection
1076
+ this.lexiconsCollection = new lexicon$1.Lexicons();
1077
+ // Re-register all other lexicons
1078
+ for (const [id, lex] of this.lexicons.entries()) {
1079
+ if (id !== lexicon.id) {
1080
+ this.lexiconsCollection.add(lex);
1081
+ }
1082
+ }
1083
+ }
1084
+ }
1085
+ }
1086
+ this.lexicons.set(lexicon.id, lexicon);
1087
+ this.lexiconsCollection.add(lexicon);
1088
+ }
1089
+ /**
1090
+ * Registers multiple lexicons at once.
1091
+ *
1092
+ * @param lexicons - Array of lexicon documents to register
1093
+ *
1094
+ * @example
1095
+ * ```typescript
1096
+ * import { HYPERCERT_LEXICONS } from "@hypercerts-org/sdk/lexicons";
1097
+ *
1098
+ * registry.registerMany(HYPERCERT_LEXICONS);
1099
+ * ```
1100
+ */
1101
+ registerMany(lexicons) {
1102
+ for (const lexicon of lexicons) {
1103
+ this.register(lexicon);
1104
+ }
1105
+ }
1106
+ /**
1107
+ * Gets a lexicon document by ID.
1108
+ *
1109
+ * @param id - The lexicon NSID (e.g., "org.hypercerts.hypercert")
1110
+ * @returns The lexicon document, or `undefined` if not registered
1111
+ *
1112
+ * @example
1113
+ * ```typescript
1114
+ * const lexicon = registry.get("org.hypercerts.hypercert");
1115
+ * if (lexicon) {
1116
+ * console.log(`Found lexicon: ${lexicon.id}`);
1117
+ * }
1118
+ * ```
1119
+ */
1120
+ get(id) {
1121
+ return this.lexicons.get(id);
1122
+ }
1123
+ /**
1124
+ * Validates a record against a collection's lexicon schema.
1125
+ *
1126
+ * @param collection - The collection NSID (same as lexicon ID)
1127
+ * @param record - The record data to validate
1128
+ * @returns Validation result with `valid` boolean and optional `error` message
1129
+ *
1130
+ * @remarks
1131
+ * - If no lexicon is registered for the collection, validation passes
1132
+ * (we can't validate against unknown schemas)
1133
+ * - Validation checks required fields and type constraints defined
1134
+ * in the lexicon schema
1135
+ *
1136
+ * @example
1137
+ * ```typescript
1138
+ * const result = registry.validate("org.hypercerts.hypercert", {
1139
+ * title: "My Hypercert",
1140
+ * description: "Description...",
1141
+ * // ... other fields
1142
+ * });
1143
+ *
1144
+ * if (!result.valid) {
1145
+ * throw new Error(`Invalid record: ${result.error}`);
1146
+ * }
1147
+ * ```
1148
+ */
1149
+ validate(collection, record) {
1150
+ // Check if we have a lexicon registered for this collection
1151
+ // Collection format is typically "namespace.collection" (e.g., "app.bsky.feed.post")
1152
+ // Lexicon ID format is the same
1153
+ const lexiconId = collection;
1154
+ const lexicon = this.lexicons.get(lexiconId);
1155
+ if (!lexicon) {
1156
+ // No lexicon registered - validation passes (can't validate unknown schemas)
1157
+ return { valid: true };
1158
+ }
1159
+ // Check required fields if the lexicon defines them
1160
+ const recordDef = lexicon.defs?.record;
1161
+ if (recordDef && typeof recordDef === "object" && "record" in recordDef) {
1162
+ const recordSchema = recordDef.record;
1163
+ if (typeof recordSchema === "object" && "required" in recordSchema && Array.isArray(recordSchema.required)) {
1164
+ const recordObj = record;
1165
+ for (const requiredField of recordSchema.required) {
1166
+ if (typeof requiredField === "string" && !(requiredField in recordObj)) {
1167
+ return {
1168
+ valid: false,
1169
+ error: `Missing required field: ${requiredField}`,
1170
+ };
1171
+ }
1172
+ }
1173
+ }
1174
+ }
1175
+ try {
1176
+ this.lexiconsCollection.assertValidRecord(collection, record);
1177
+ return { valid: true };
1178
+ }
1179
+ catch (error) {
1180
+ // If error indicates lexicon not found, treat as validation pass
1181
+ // (the lexicon might exist in Agent's collection but not ours)
1182
+ const errorMessage = error instanceof Error ? error.message : String(error);
1183
+ if (errorMessage.includes("not found") || errorMessage.includes("Lexicon not found")) {
1184
+ return { valid: true };
1185
+ }
1186
+ return {
1187
+ valid: false,
1188
+ error: errorMessage,
1189
+ };
1190
+ }
1191
+ }
1192
+ /**
1193
+ * Adds all registered lexicons to an AT Protocol Agent instance.
1194
+ *
1195
+ * This allows the Agent to understand custom lexicon types when making
1196
+ * API requests.
1197
+ *
1198
+ * @param agent - The Agent instance to extend
1199
+ *
1200
+ * @remarks
1201
+ * This is called automatically when creating a Repository. You typically
1202
+ * don't need to call this directly unless you're using the Agent
1203
+ * independently.
1204
+ *
1205
+ * @internal
1206
+ */
1207
+ addToAgent(agent) {
1208
+ // Access the internal lexicons collection and merge our lexicons
1209
+ // The Agent's lex property is a Lexicons instance
1210
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1211
+ const agentLex = agent.lex;
1212
+ // Add each registered lexicon to the agent
1213
+ for (const lexicon of this.lexicons.values()) {
1214
+ agentLex.add(lexicon);
1215
+ }
1216
+ }
1217
+ /**
1218
+ * Gets all registered lexicon IDs.
1219
+ *
1220
+ * @returns Array of lexicon NSIDs
1221
+ *
1222
+ * @example
1223
+ * ```typescript
1224
+ * const ids = registry.getRegisteredIds();
1225
+ * console.log(`Registered lexicons: ${ids.join(", ")}`);
1226
+ * ```
1227
+ */
1228
+ getRegisteredIds() {
1229
+ return Array.from(this.lexicons.keys());
1230
+ }
1231
+ /**
1232
+ * Checks if a lexicon is registered.
1233
+ *
1234
+ * @param id - The lexicon NSID to check
1235
+ * @returns `true` if the lexicon is registered
1236
+ *
1237
+ * @example
1238
+ * ```typescript
1239
+ * if (registry.has("org.hypercerts.hypercert")) {
1240
+ * // Hypercert lexicon is available
1241
+ * }
1242
+ * ```
1243
+ */
1244
+ has(id) {
1245
+ return this.lexicons.has(id);
1246
+ }
1247
+ }
1248
+
1249
+ /**
1250
+ * RecordOperationsImpl - Low-level record CRUD operations.
1251
+ *
1252
+ * This module provides the implementation for direct AT Protocol
1253
+ * record operations (create, read, update, delete, list).
1254
+ *
1255
+ * @packageDocumentation
1256
+ */
1257
+ /**
1258
+ * Implementation of low-level AT Protocol record operations.
1259
+ *
1260
+ * This class provides direct access to the AT Protocol repository API
1261
+ * for CRUD operations on records. It handles:
1262
+ *
1263
+ * - Lexicon validation before create/update operations
1264
+ * - Error mapping to SDK error types
1265
+ * - Response normalization
1266
+ *
1267
+ * @remarks
1268
+ * This class is typically not instantiated directly. Access it through
1269
+ * {@link Repository.records}.
1270
+ *
1271
+ * All operations are performed against the repository DID specified
1272
+ * at construction time. To operate on a different repository, create
1273
+ * a new Repository instance using {@link Repository.repo}.
1274
+ *
1275
+ * @example
1276
+ * ```typescript
1277
+ * // Access through Repository
1278
+ * const repo = sdk.repository(session);
1279
+ *
1280
+ * // Create a record
1281
+ * const { uri, cid } = await repo.records.create({
1282
+ * collection: "org.example.myRecord",
1283
+ * record: { title: "Hello", createdAt: new Date().toISOString() },
1284
+ * });
1285
+ *
1286
+ * // Update the record
1287
+ * const rkey = uri.split("/").pop()!;
1288
+ * await repo.records.update({
1289
+ * collection: "org.example.myRecord",
1290
+ * rkey,
1291
+ * record: { title: "Updated", createdAt: new Date().toISOString() },
1292
+ * });
1293
+ * ```
1294
+ *
1295
+ * @internal
1296
+ */
1297
+ class RecordOperationsImpl {
1298
+ /**
1299
+ * Creates a new RecordOperationsImpl.
1300
+ *
1301
+ * @param agent - AT Protocol Agent for making API calls
1302
+ * @param repoDid - DID of the repository to operate on
1303
+ * @param lexiconRegistry - Registry for record validation
1304
+ *
1305
+ * @internal
1306
+ */
1307
+ constructor(agent, repoDid, lexiconRegistry) {
1308
+ this.agent = agent;
1309
+ this.repoDid = repoDid;
1310
+ this.lexiconRegistry = lexiconRegistry;
1311
+ }
1312
+ /**
1313
+ * Creates a new record in the specified collection.
1314
+ *
1315
+ * @param params - Creation parameters
1316
+ * @param params.collection - NSID of the collection (e.g., "org.hypercerts.hypercert")
1317
+ * @param params.record - Record data conforming to the collection's lexicon schema
1318
+ * @param params.rkey - Optional record key. If not provided, a TID (timestamp-based ID)
1319
+ * is automatically generated by the server.
1320
+ * @returns Promise resolving to the created record's URI and CID
1321
+ * @throws {@link ValidationError} if the record doesn't conform to the lexicon schema
1322
+ * @throws {@link NetworkError} if the API request fails
1323
+ *
1324
+ * @remarks
1325
+ * The record is validated against the collection's lexicon before sending
1326
+ * to the server. If no lexicon is registered for the collection, validation
1327
+ * is skipped (allowing custom record types).
1328
+ *
1329
+ * **AT-URI Format**: `at://{did}/{collection}/{rkey}`
1330
+ *
1331
+ * @example
1332
+ * ```typescript
1333
+ * const result = await repo.records.create({
1334
+ * collection: "org.hypercerts.hypercert",
1335
+ * record: {
1336
+ * title: "My Hypercert",
1337
+ * description: "...",
1338
+ * createdAt: new Date().toISOString(),
1339
+ * },
1340
+ * });
1341
+ * console.log(`Created: ${result.uri}`);
1342
+ * // Output: Created: at://did:plc:abc123/org.hypercerts.hypercert/xyz789
1343
+ * ```
1344
+ */
1345
+ async create(params) {
1346
+ const validation = this.lexiconRegistry.validate(params.collection, params.record);
1347
+ if (!validation.valid) {
1348
+ throw new ValidationError(`Invalid record for collection ${params.collection}: ${validation.error}`);
1349
+ }
1350
+ try {
1351
+ const result = await this.agent.com.atproto.repo.createRecord({
1352
+ repo: this.repoDid,
1353
+ collection: params.collection,
1354
+ record: params.record,
1355
+ rkey: params.rkey,
1356
+ });
1357
+ if (!result.success) {
1358
+ throw new NetworkError("Failed to create record");
1359
+ }
1360
+ return { uri: result.data.uri, cid: result.data.cid };
1361
+ }
1362
+ catch (error) {
1363
+ if (error instanceof ValidationError || error instanceof NetworkError)
1364
+ throw error;
1365
+ throw new NetworkError(`Failed to create record: ${error instanceof Error ? error.message : "Unknown error"}`, error);
1366
+ }
1367
+ }
1368
+ /**
1369
+ * Updates an existing record (full replacement).
1370
+ *
1371
+ * @param params - Update parameters
1372
+ * @param params.collection - NSID of the collection
1373
+ * @param params.rkey - Record key (the last segment of the AT-URI)
1374
+ * @param params.record - New record data (completely replaces existing record)
1375
+ * @returns Promise resolving to the updated record's URI and new CID
1376
+ * @throws {@link ValidationError} if the record doesn't conform to the lexicon schema
1377
+ * @throws {@link NetworkError} if the API request fails
1378
+ *
1379
+ * @remarks
1380
+ * This is a full replacement operation, not a partial update. The entire
1381
+ * record is replaced with the new data. To preserve existing fields,
1382
+ * first fetch the record with {@link get}, modify it, then update.
1383
+ *
1384
+ * @example
1385
+ * ```typescript
1386
+ * // Get existing record
1387
+ * const existing = await repo.records.get({
1388
+ * collection: "org.hypercerts.hypercert",
1389
+ * rkey: "xyz789",
1390
+ * });
1391
+ *
1392
+ * // Update with modified data
1393
+ * const updated = await repo.records.update({
1394
+ * collection: "org.hypercerts.hypercert",
1395
+ * rkey: "xyz789",
1396
+ * record: {
1397
+ * ...existing.value,
1398
+ * title: "Updated Title",
1399
+ * },
1400
+ * });
1401
+ * ```
1402
+ */
1403
+ async update(params) {
1404
+ const validation = this.lexiconRegistry.validate(params.collection, params.record);
1405
+ if (!validation.valid) {
1406
+ throw new ValidationError(`Invalid record for collection ${params.collection}: ${validation.error}`);
1407
+ }
1408
+ try {
1409
+ const result = await this.agent.com.atproto.repo.putRecord({
1410
+ repo: this.repoDid,
1411
+ collection: params.collection,
1412
+ rkey: params.rkey,
1413
+ record: params.record,
1414
+ });
1415
+ if (!result.success) {
1416
+ throw new NetworkError("Failed to update record");
1417
+ }
1418
+ return { uri: result.data.uri, cid: result.data.cid };
1419
+ }
1420
+ catch (error) {
1421
+ if (error instanceof ValidationError || error instanceof NetworkError)
1422
+ throw error;
1423
+ throw new NetworkError(`Failed to update record: ${error instanceof Error ? error.message : "Unknown error"}`, error);
1424
+ }
1425
+ }
1426
+ /**
1427
+ * Gets a single record by collection and key.
1428
+ *
1429
+ * @param params - Get parameters
1430
+ * @param params.collection - NSID of the collection
1431
+ * @param params.rkey - Record key
1432
+ * @returns Promise resolving to the record's URI, CID, and value
1433
+ * @throws {@link NetworkError} if the record is not found or request fails
1434
+ *
1435
+ * @example
1436
+ * ```typescript
1437
+ * const record = await repo.records.get({
1438
+ * collection: "org.hypercerts.hypercert",
1439
+ * rkey: "xyz789",
1440
+ * });
1441
+ *
1442
+ * console.log(record.uri); // at://did:plc:abc123/org.hypercerts.hypercert/xyz789
1443
+ * console.log(record.cid); // bafyrei...
1444
+ * console.log(record.value); // { title: "...", description: "...", ... }
1445
+ * ```
1446
+ */
1447
+ async get(params) {
1448
+ try {
1449
+ const result = await this.agent.com.atproto.repo.getRecord({
1450
+ repo: this.repoDid,
1451
+ collection: params.collection,
1452
+ rkey: params.rkey,
1453
+ });
1454
+ if (!result.success) {
1455
+ throw new NetworkError("Failed to get record");
1456
+ }
1457
+ return { uri: result.data.uri, cid: result.data.cid ?? "", value: result.data.value };
1458
+ }
1459
+ catch (error) {
1460
+ if (error instanceof NetworkError)
1461
+ throw error;
1462
+ throw new NetworkError(`Failed to get record: ${error instanceof Error ? error.message : "Unknown error"}`, error);
1463
+ }
1464
+ }
1465
+ /**
1466
+ * Lists records in a collection with pagination.
1467
+ *
1468
+ * @param params - List parameters
1469
+ * @param params.collection - NSID of the collection
1470
+ * @param params.limit - Maximum number of records to return (server may impose its own limit)
1471
+ * @param params.cursor - Pagination cursor from a previous response
1472
+ * @returns Promise resolving to paginated list of records
1473
+ * @throws {@link NetworkError} if the request fails
1474
+ *
1475
+ * @remarks
1476
+ * Records are returned in reverse chronological order (newest first).
1477
+ * Use the `cursor` from the response to fetch subsequent pages.
1478
+ *
1479
+ * @example Paginating through all records
1480
+ * ```typescript
1481
+ * let cursor: string | undefined;
1482
+ * const allRecords = [];
1483
+ *
1484
+ * do {
1485
+ * const page = await repo.records.list({
1486
+ * collection: "org.hypercerts.hypercert",
1487
+ * limit: 100,
1488
+ * cursor,
1489
+ * });
1490
+ * allRecords.push(...page.records);
1491
+ * cursor = page.cursor;
1492
+ * } while (cursor);
1493
+ *
1494
+ * console.log(`Total records: ${allRecords.length}`);
1495
+ * ```
1496
+ */
1497
+ async list(params) {
1498
+ try {
1499
+ const result = await this.agent.com.atproto.repo.listRecords({
1500
+ repo: this.repoDid,
1501
+ collection: params.collection,
1502
+ limit: params.limit,
1503
+ cursor: params.cursor,
1504
+ });
1505
+ if (!result.success) {
1506
+ throw new NetworkError("Failed to list records");
1507
+ }
1508
+ return {
1509
+ records: result.data.records?.map((r) => ({ uri: r.uri, cid: r.cid, value: r.value })) || [],
1510
+ cursor: result.data.cursor ?? undefined,
1511
+ };
1512
+ }
1513
+ catch (error) {
1514
+ if (error instanceof NetworkError)
1515
+ throw error;
1516
+ throw new NetworkError(`Failed to list records: ${error instanceof Error ? error.message : "Unknown error"}`, error);
1517
+ }
1518
+ }
1519
+ /**
1520
+ * Deletes a record from a collection.
1521
+ *
1522
+ * @param params - Delete parameters
1523
+ * @param params.collection - NSID of the collection
1524
+ * @param params.rkey - Record key to delete
1525
+ * @throws {@link NetworkError} if the deletion fails
1526
+ *
1527
+ * @remarks
1528
+ * Deletion is permanent. The record's AT-URI cannot be reused (the same
1529
+ * rkey can be used for a new record, but it will have a different CID).
1530
+ *
1531
+ * @example
1532
+ * ```typescript
1533
+ * await repo.records.delete({
1534
+ * collection: "org.hypercerts.hypercert",
1535
+ * rkey: "xyz789",
1536
+ * });
1537
+ * ```
1538
+ */
1539
+ async delete(params) {
1540
+ try {
1541
+ const result = await this.agent.com.atproto.repo.deleteRecord({
1542
+ repo: this.repoDid,
1543
+ collection: params.collection,
1544
+ rkey: params.rkey,
1545
+ });
1546
+ if (!result.success) {
1547
+ throw new NetworkError("Failed to delete record");
1548
+ }
1549
+ }
1550
+ catch (error) {
1551
+ if (error instanceof NetworkError)
1552
+ throw error;
1553
+ throw new NetworkError(`Failed to delete record: ${error instanceof Error ? error.message : "Unknown error"}`, error);
1554
+ }
1555
+ }
1556
+ }
1557
+
1558
+ /**
1559
+ * BlobOperationsImpl - Blob upload and retrieval operations.
1560
+ *
1561
+ * This module provides the implementation for AT Protocol blob operations,
1562
+ * handling binary data like images and files.
1563
+ *
1564
+ * @packageDocumentation
1565
+ */
1566
+ /**
1567
+ * Implementation of blob operations for binary data handling.
1568
+ *
1569
+ * Blobs in AT Protocol are content-addressed binary objects stored
1570
+ * separately from records. They are referenced in records using a
1571
+ * blob reference object with a CID ($link).
1572
+ *
1573
+ * @remarks
1574
+ * This class is typically not instantiated directly. Access it through
1575
+ * {@link Repository.blobs}.
1576
+ *
1577
+ * **Blob Size Limits**: PDS servers typically impose size limits on blobs.
1578
+ * Common limits are:
1579
+ * - Images: 1MB
1580
+ * - Other files: Varies by server configuration
1581
+ *
1582
+ * **Supported MIME Types**: Any MIME type is technically supported, but
1583
+ * servers may reject certain types. Images (JPEG, PNG, GIF, WebP) are
1584
+ * universally supported.
1585
+ *
1586
+ * @example
1587
+ * ```typescript
1588
+ * // Upload an image blob
1589
+ * const imageBlob = new Blob([imageData], { type: "image/jpeg" });
1590
+ * const { ref, mimeType, size } = await repo.blobs.upload(imageBlob);
1591
+ *
1592
+ * // Use the ref in a record
1593
+ * await repo.records.create({
1594
+ * collection: "org.example.post",
1595
+ * record: {
1596
+ * text: "Check out this image!",
1597
+ * image: ref, // { $link: "bafyrei..." }
1598
+ * createdAt: new Date().toISOString(),
1599
+ * },
1600
+ * });
1601
+ * ```
1602
+ *
1603
+ * @internal
1604
+ */
1605
+ class BlobOperationsImpl {
1606
+ /**
1607
+ * Creates a new BlobOperationsImpl.
1608
+ *
1609
+ * @param agent - AT Protocol Agent for making API calls
1610
+ * @param repoDid - DID of the repository (used for blob retrieval)
1611
+ * @param _serverUrl - Server URL (reserved for future use)
1612
+ *
1613
+ * @internal
1614
+ */
1615
+ constructor(agent, repoDid, _serverUrl) {
1616
+ this.agent = agent;
1617
+ this.repoDid = repoDid;
1618
+ this._serverUrl = _serverUrl;
1619
+ }
1620
+ /**
1621
+ * Uploads a blob to the server.
1622
+ *
1623
+ * @param blob - The blob to upload (File or Blob object)
1624
+ * @returns Promise resolving to blob reference and metadata
1625
+ * @throws {@link NetworkError} if the upload fails
1626
+ *
1627
+ * @remarks
1628
+ * The returned `ref` object should be used directly in records to
1629
+ * reference the blob. The `$link` property contains the blob's CID.
1630
+ *
1631
+ * **MIME Type Detection**: If the blob has no type, it defaults to
1632
+ * `application/octet-stream`. For best results, always specify the
1633
+ * correct MIME type when creating the Blob.
1634
+ *
1635
+ * @example Uploading an image
1636
+ * ```typescript
1637
+ * // From a File input
1638
+ * const file = fileInput.files[0];
1639
+ * const { ref } = await repo.blobs.upload(file);
1640
+ *
1641
+ * // From raw data
1642
+ * const imageBlob = new Blob([uint8Array], { type: "image/png" });
1643
+ * const { ref, mimeType, size } = await repo.blobs.upload(imageBlob);
1644
+ *
1645
+ * console.log(`Uploaded ${size} bytes of ${mimeType}`);
1646
+ * console.log(`CID: ${ref.$link}`);
1647
+ * ```
1648
+ *
1649
+ * @example Using in a hypercert
1650
+ * ```typescript
1651
+ * const coverImage = new Blob([imageData], { type: "image/jpeg" });
1652
+ * const { ref } = await repo.blobs.upload(coverImage);
1653
+ *
1654
+ * // The ref is used directly in the record
1655
+ * await repo.hypercerts.create({
1656
+ * title: "My Hypercert",
1657
+ * // ... other fields
1658
+ * image: coverImage, // HypercertOperations handles upload internally
1659
+ * });
1660
+ * ```
1661
+ */
1662
+ async upload(blob) {
1663
+ try {
1664
+ const arrayBuffer = await blob.arrayBuffer();
1665
+ const uint8Array = new Uint8Array(arrayBuffer);
1666
+ const result = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
1667
+ encoding: blob.type || "application/octet-stream",
1668
+ });
1669
+ if (!result.success) {
1670
+ throw new NetworkError("Failed to upload blob");
1671
+ }
1672
+ return {
1673
+ ref: result.data.blob.ref,
1674
+ mimeType: result.data.blob.mimeType,
1675
+ size: result.data.blob.size,
1676
+ };
1677
+ }
1678
+ catch (error) {
1679
+ if (error instanceof NetworkError)
1680
+ throw error;
1681
+ throw new NetworkError(`Failed to upload blob: ${error instanceof Error ? error.message : "Unknown error"}`, error);
1682
+ }
1683
+ }
1684
+ /**
1685
+ * Retrieves a blob by its CID.
1686
+ *
1687
+ * @param cid - Content Identifier (CID) of the blob, typically from a blob
1688
+ * reference's `$link` property
1689
+ * @returns Promise resolving to blob data and MIME type
1690
+ * @throws {@link NetworkError} if the blob is not found or retrieval fails
1691
+ *
1692
+ * @remarks
1693
+ * The returned data is a Uint8Array which can be converted to other
1694
+ * formats as needed (Blob, ArrayBuffer, Base64, etc.).
1695
+ *
1696
+ * **MIME Type**: The returned MIME type comes from the Content-Type header.
1697
+ * If the server doesn't provide one, it defaults to `application/octet-stream`.
1698
+ *
1699
+ * @example Basic retrieval
1700
+ * ```typescript
1701
+ * // Get a blob from a record's blob reference
1702
+ * const record = await repo.records.get({ collection, rkey });
1703
+ * const blobRef = (record.value as any).image;
1704
+ *
1705
+ * const { data, mimeType } = await repo.blobs.get(blobRef.$link);
1706
+ *
1707
+ * // Convert to a Blob for use in the browser
1708
+ * const blob = new Blob([data], { type: mimeType });
1709
+ * const url = URL.createObjectURL(blob);
1710
+ * ```
1711
+ *
1712
+ * @example Displaying an image
1713
+ * ```typescript
1714
+ * const { data, mimeType } = await repo.blobs.get(imageCid);
1715
+ *
1716
+ * // Create data URL for <img> src
1717
+ * const base64 = btoa(String.fromCharCode(...data));
1718
+ * const dataUrl = `data:${mimeType};base64,${base64}`;
1719
+ *
1720
+ * // Or use object URL
1721
+ * const blob = new Blob([data], { type: mimeType });
1722
+ * img.src = URL.createObjectURL(blob);
1723
+ * ```
1724
+ */
1725
+ async get(cid) {
1726
+ try {
1727
+ const result = await this.agent.com.atproto.sync.getBlob({
1728
+ did: this.repoDid,
1729
+ cid,
1730
+ });
1731
+ if (!result.success) {
1732
+ throw new NetworkError("Failed to get blob");
1733
+ }
1734
+ return {
1735
+ data: result.data,
1736
+ mimeType: result.headers["content-type"] || "application/octet-stream",
1737
+ };
1738
+ }
1739
+ catch (error) {
1740
+ if (error instanceof NetworkError)
1741
+ throw error;
1742
+ throw new NetworkError(`Failed to get blob: ${error instanceof Error ? error.message : "Unknown error"}`, error);
1743
+ }
1744
+ }
1745
+ }
1746
+
1747
+ /**
1748
+ * ProfileOperationsImpl - User profile operations.
1749
+ *
1750
+ * This module provides the implementation for AT Protocol profile
1751
+ * management, including fetching and updating user profiles.
1752
+ *
1753
+ * @packageDocumentation
1754
+ */
1755
+ /**
1756
+ * Implementation of profile operations for user profile management.
1757
+ *
1758
+ * Profiles in AT Protocol are stored as records in the `app.bsky.actor.profile`
1759
+ * collection with the special rkey "self". This class provides a convenient
1760
+ * API for reading and updating profile data.
1761
+ *
1762
+ * @remarks
1763
+ * This class is typically not instantiated directly. Access it through
1764
+ * {@link Repository.profile}.
1765
+ *
1766
+ * **Profile Fields**:
1767
+ * - `handle`: Read-only, managed by the PDS
1768
+ * - `displayName`: User's display name (max 64 chars typically)
1769
+ * - `description`: Profile bio (max 256 chars typically)
1770
+ * - `avatar`: Profile picture blob reference
1771
+ * - `banner`: Banner image blob reference
1772
+ * - `website`: User's website URL (may not be available on all servers)
1773
+ *
1774
+ * @example
1775
+ * ```typescript
1776
+ * // Get profile
1777
+ * const profile = await repo.profile.get();
1778
+ * console.log(`${profile.displayName} (@${profile.handle})`);
1779
+ *
1780
+ * // Update profile
1781
+ * await repo.profile.update({
1782
+ * displayName: "New Name",
1783
+ * description: "Updated bio",
1784
+ * });
1785
+ *
1786
+ * // Update with new avatar
1787
+ * const avatarBlob = new Blob([imageData], { type: "image/png" });
1788
+ * await repo.profile.update({ avatar: avatarBlob });
1789
+ *
1790
+ * // Remove a field
1791
+ * await repo.profile.update({ website: null });
1792
+ * ```
1793
+ *
1794
+ * @internal
1795
+ */
1796
+ class ProfileOperationsImpl {
1797
+ /**
1798
+ * Creates a new ProfileOperationsImpl.
1799
+ *
1800
+ * @param agent - AT Protocol Agent for making API calls
1801
+ * @param repoDid - DID of the repository/user
1802
+ * @param _serverUrl - Server URL (reserved for future use)
1803
+ *
1804
+ * @internal
1805
+ */
1806
+ constructor(agent, repoDid, _serverUrl) {
1807
+ this.agent = agent;
1808
+ this.repoDid = repoDid;
1809
+ this._serverUrl = _serverUrl;
1810
+ }
1811
+ /**
1812
+ * Gets the repository's profile.
1813
+ *
1814
+ * @returns Promise resolving to profile data
1815
+ * @throws {@link NetworkError} if the profile cannot be fetched
1816
+ *
1817
+ * @remarks
1818
+ * This method fetches the full profile using the `getProfile` API,
1819
+ * which includes resolved information like follower counts on some
1820
+ * servers. For hypercerts SDK usage, the basic profile fields are
1821
+ * returned.
1822
+ *
1823
+ * **Note**: The `website` field may not be available on all AT Protocol
1824
+ * servers. Standard Bluesky profiles don't include this field.
1825
+ *
1826
+ * @example
1827
+ * ```typescript
1828
+ * const profile = await repo.profile.get();
1829
+ *
1830
+ * console.log(`Handle: @${profile.handle}`);
1831
+ * console.log(`Name: ${profile.displayName || "(not set)"}`);
1832
+ * console.log(`Bio: ${profile.description || "(no bio)"}`);
1833
+ *
1834
+ * if (profile.avatar) {
1835
+ * // Avatar is a URL or blob reference
1836
+ * console.log(`Avatar: ${profile.avatar}`);
1837
+ * }
1838
+ * ```
1839
+ */
1840
+ async get() {
1841
+ try {
1842
+ const result = await this.agent.getProfile({ actor: this.repoDid });
1843
+ if (!result.success) {
1844
+ throw new NetworkError("Failed to get profile");
1845
+ }
1846
+ return {
1847
+ handle: result.data.handle,
1848
+ displayName: result.data.displayName,
1849
+ description: result.data.description,
1850
+ avatar: result.data.avatar,
1851
+ banner: result.data.banner,
1852
+ // Note: website may not be available in standard profile
1853
+ };
1854
+ }
1855
+ catch (error) {
1856
+ if (error instanceof NetworkError)
1857
+ throw error;
1858
+ throw new NetworkError(`Failed to get profile: ${error instanceof Error ? error.message : "Unknown error"}`, error);
1859
+ }
1860
+ }
1861
+ /**
1862
+ * Updates the repository's profile.
1863
+ *
1864
+ * @param params - Fields to update. Pass `null` to remove a field.
1865
+ * Omitted fields are preserved from the existing profile.
1866
+ * @returns Promise resolving to update result with new URI and CID
1867
+ * @throws {@link NetworkError} if the update fails
1868
+ *
1869
+ * @remarks
1870
+ * This method performs a read-modify-write operation:
1871
+ * 1. Fetches the existing profile record
1872
+ * 2. Merges in the provided updates
1873
+ * 3. Writes the updated profile back
1874
+ *
1875
+ * **Image Handling**: When providing `avatar` or `banner` as a Blob,
1876
+ * the image is automatically uploaded and the blob reference is stored
1877
+ * in the profile.
1878
+ *
1879
+ * **Field Removal**: Pass `null` to explicitly remove a field. Omitting
1880
+ * a field (not including it in params) preserves the existing value.
1881
+ *
1882
+ * @example Update display name and bio
1883
+ * ```typescript
1884
+ * await repo.profile.update({
1885
+ * displayName: "Alice",
1886
+ * description: "Building impact certificates",
1887
+ * });
1888
+ * ```
1889
+ *
1890
+ * @example Update avatar image
1891
+ * ```typescript
1892
+ * // From a file input
1893
+ * const file = document.getElementById("avatar").files[0];
1894
+ * await repo.profile.update({ avatar: file });
1895
+ *
1896
+ * // From raw data
1897
+ * const response = await fetch("https://example.com/my-avatar.png");
1898
+ * const blob = await response.blob();
1899
+ * await repo.profile.update({ avatar: blob });
1900
+ * ```
1901
+ *
1902
+ * @example Remove description
1903
+ * ```typescript
1904
+ * // Removes the description field entirely
1905
+ * await repo.profile.update({ description: null });
1906
+ * ```
1907
+ *
1908
+ * @example Multiple updates at once
1909
+ * ```typescript
1910
+ * const newAvatar = new Blob([avatarData], { type: "image/png" });
1911
+ * const newBanner = new Blob([bannerData], { type: "image/jpeg" });
1912
+ *
1913
+ * await repo.profile.update({
1914
+ * displayName: "New Name",
1915
+ * description: "New bio",
1916
+ * avatar: newAvatar,
1917
+ * banner: newBanner,
1918
+ * });
1919
+ * ```
1920
+ */
1921
+ async update(params) {
1922
+ try {
1923
+ // Get existing profile record
1924
+ const existing = await this.agent.com.atproto.repo.getRecord({
1925
+ repo: this.repoDid,
1926
+ collection: "app.bsky.actor.profile",
1927
+ rkey: "self",
1928
+ });
1929
+ const existingProfile = existing.data.value || {};
1930
+ // Build updated profile
1931
+ const updatedProfile = { ...existingProfile };
1932
+ if (params.displayName !== undefined) {
1933
+ if (params.displayName === null) {
1934
+ delete updatedProfile.displayName;
1935
+ }
1936
+ else {
1937
+ updatedProfile.displayName = params.displayName;
1938
+ }
1939
+ }
1940
+ if (params.description !== undefined) {
1941
+ if (params.description === null) {
1942
+ delete updatedProfile.description;
1943
+ }
1944
+ else {
1945
+ updatedProfile.description = params.description;
1946
+ }
1947
+ }
1948
+ // Handle avatar upload
1949
+ if (params.avatar !== undefined) {
1950
+ if (params.avatar === null) {
1951
+ delete updatedProfile.avatar;
1952
+ }
1953
+ else {
1954
+ const arrayBuffer = await params.avatar.arrayBuffer();
1955
+ const uint8Array = new Uint8Array(arrayBuffer);
1956
+ const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
1957
+ encoding: params.avatar.type || "image/jpeg",
1958
+ });
1959
+ if (uploadResult.success) {
1960
+ updatedProfile.avatar = uploadResult.data.blob;
1961
+ }
1962
+ }
1963
+ }
1964
+ // Handle banner upload
1965
+ if (params.banner !== undefined) {
1966
+ if (params.banner === null) {
1967
+ delete updatedProfile.banner;
1968
+ }
1969
+ else {
1970
+ const arrayBuffer = await params.banner.arrayBuffer();
1971
+ const uint8Array = new Uint8Array(arrayBuffer);
1972
+ const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
1973
+ encoding: params.banner.type || "image/jpeg",
1974
+ });
1975
+ if (uploadResult.success) {
1976
+ updatedProfile.banner = uploadResult.data.blob;
1977
+ }
1978
+ }
1979
+ }
1980
+ const result = await this.agent.com.atproto.repo.putRecord({
1981
+ repo: this.repoDid,
1982
+ collection: "app.bsky.actor.profile",
1983
+ rkey: "self",
1984
+ record: updatedProfile,
1985
+ });
1986
+ if (!result.success) {
1987
+ throw new NetworkError("Failed to update profile");
1988
+ }
1989
+ return { uri: result.data.uri, cid: result.data.cid };
1990
+ }
1991
+ catch (error) {
1992
+ if (error instanceof NetworkError)
1993
+ throw error;
1994
+ throw new NetworkError(`Failed to update profile: ${error instanceof Error ? error.message : "Unknown error"}`, error);
1995
+ }
1996
+ }
1997
+ }
1998
+
1999
+ /**
2000
+ * HypercertOperationsImpl - High-level hypercert operations.
2001
+ *
2002
+ * This module provides the implementation for creating and managing
2003
+ * hypercerts, including related records like rights, locations,
2004
+ * contributions, measurements, and evaluations.
2005
+ *
2006
+ * @packageDocumentation
2007
+ */
2008
+ /**
2009
+ * Implementation of high-level hypercert operations.
2010
+ *
2011
+ * This class provides a convenient API for creating and managing hypercerts
2012
+ * with automatic handling of:
2013
+ *
2014
+ * - Image upload and blob reference management
2015
+ * - Rights record creation and linking
2016
+ * - Location attachment with optional GeoJSON support
2017
+ * - Contribution tracking
2018
+ * - Measurement and evaluation records
2019
+ * - Hypercert collections
2020
+ *
2021
+ * The class extends EventEmitter to provide real-time progress notifications
2022
+ * during complex operations.
2023
+ *
2024
+ * @remarks
2025
+ * This class is typically not instantiated directly. Access it through
2026
+ * {@link Repository.hypercerts}.
2027
+ *
2028
+ * **Record Relationships**:
2029
+ * - Hypercert → Rights (required, 1:1)
2030
+ * - Hypercert → Location (optional, 1:many)
2031
+ * - Hypercert → Contribution (optional, 1:many)
2032
+ * - Hypercert → Measurement (optional, 1:many)
2033
+ * - Hypercert → Evaluation (optional, 1:many)
2034
+ * - Collection → Hypercerts (1:many via claims array)
2035
+ *
2036
+ * @example Creating a hypercert with progress tracking
2037
+ * ```typescript
2038
+ * repo.hypercerts.on("recordCreated", ({ uri }) => {
2039
+ * console.log(`Hypercert created: ${uri}`);
2040
+ * });
2041
+ *
2042
+ * const result = await repo.hypercerts.create({
2043
+ * title: "Climate Impact",
2044
+ * description: "Reduced emissions by 100 tons",
2045
+ * workScope: "Climate",
2046
+ * workTimeframeFrom: "2024-01-01",
2047
+ * workTimeframeTo: "2024-12-31",
2048
+ * rights: { name: "CC-BY", type: "license", description: "..." },
2049
+ * onProgress: (step) => console.log(`${step.name}: ${step.status}`),
2050
+ * });
2051
+ * ```
2052
+ *
2053
+ * @internal
2054
+ */
2055
+ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
2056
+ /**
2057
+ * Creates a new HypercertOperationsImpl.
2058
+ *
2059
+ * @param agent - AT Protocol Agent for making API calls
2060
+ * @param repoDid - DID of the repository to operate on
2061
+ * @param _serverUrl - Server URL (reserved for future use)
2062
+ * @param lexiconRegistry - Registry for record validation
2063
+ * @param logger - Optional logger for debugging
2064
+ *
2065
+ * @internal
2066
+ */
2067
+ constructor(agent, repoDid, _serverUrl, lexiconRegistry, logger) {
2068
+ super();
2069
+ this.agent = agent;
2070
+ this.repoDid = repoDid;
2071
+ this._serverUrl = _serverUrl;
2072
+ this.lexiconRegistry = lexiconRegistry;
2073
+ this.logger = logger;
2074
+ }
2075
+ /**
2076
+ * Emits a progress event to the optional progress handler.
2077
+ *
2078
+ * @param onProgress - Progress callback from create params
2079
+ * @param step - Progress step information
2080
+ * @internal
2081
+ */
2082
+ emitProgress(onProgress, step) {
2083
+ if (onProgress) {
2084
+ try {
2085
+ onProgress(step);
2086
+ }
2087
+ catch (err) {
2088
+ this.logger?.error(`Error in progress handler: ${err instanceof Error ? err.message : "Unknown"}`);
2089
+ }
2090
+ }
2091
+ }
2092
+ /**
2093
+ * Creates a new hypercert with all related records.
2094
+ *
2095
+ * This method orchestrates the creation of a hypercert and its associated
2096
+ * records in the correct order:
2097
+ *
2098
+ * 1. Upload image (if provided)
2099
+ * 2. Create rights record
2100
+ * 3. Create hypercert record (referencing rights)
2101
+ * 4. Attach location (if provided)
2102
+ * 5. Create contributions (if provided)
2103
+ *
2104
+ * @param params - Creation parameters (see {@link CreateHypercertParams})
2105
+ * @returns Promise resolving to URIs and CIDs of all created records
2106
+ * @throws {@link ValidationError} if any record fails validation
2107
+ * @throws {@link NetworkError} if any API call fails
2108
+ *
2109
+ * @remarks
2110
+ * The operation is not atomic - if a later step fails, earlier records
2111
+ * will still exist. The result object will contain URIs for all
2112
+ * successfully created records.
2113
+ *
2114
+ * **Progress Steps**:
2115
+ * - `uploadImage`: Image blob upload
2116
+ * - `createRights`: Rights record creation
2117
+ * - `createHypercert`: Main hypercert record creation
2118
+ * - `attachLocation`: Location record creation
2119
+ * - `createContributions`: Contribution records creation
2120
+ *
2121
+ * @example Minimal hypercert
2122
+ * ```typescript
2123
+ * const result = await repo.hypercerts.create({
2124
+ * title: "My Impact",
2125
+ * description: "Description of impact work",
2126
+ * workScope: "Education",
2127
+ * workTimeframeFrom: "2024-01-01",
2128
+ * workTimeframeTo: "2024-06-30",
2129
+ * rights: {
2130
+ * name: "Attribution",
2131
+ * type: "license",
2132
+ * description: "CC-BY-4.0",
2133
+ * },
2134
+ * });
2135
+ * ```
2136
+ *
2137
+ * @example Full hypercert with all options
2138
+ * ```typescript
2139
+ * const result = await repo.hypercerts.create({
2140
+ * title: "Reforestation Project",
2141
+ * description: "Planted 10,000 trees...",
2142
+ * shortDescription: "10K trees planted",
2143
+ * workScope: "Environment",
2144
+ * workTimeframeFrom: "2024-01-01",
2145
+ * workTimeframeTo: "2024-12-31",
2146
+ * rights: { name: "Open", type: "impact", description: "..." },
2147
+ * image: coverImageBlob,
2148
+ * location: { value: "Amazon, Brazil", name: "Amazon Basin" },
2149
+ * contributions: [
2150
+ * { contributors: ["did:plc:org1"], role: "coordinator" },
2151
+ * { contributors: ["did:plc:org2"], role: "implementer" },
2152
+ * ],
2153
+ * evidence: [{ uri: "https://...", description: "Satellite data" }],
2154
+ * onProgress: console.log,
2155
+ * });
2156
+ * ```
2157
+ */
2158
+ async create(params) {
2159
+ const createdAt = new Date().toISOString();
2160
+ const result = {
2161
+ hypercertUri: "",
2162
+ rightsUri: "",
2163
+ hypercertCid: "",
2164
+ rightsCid: "",
2165
+ };
2166
+ try {
2167
+ // Step 1: Upload image if provided
2168
+ let imageBlobRef;
2169
+ if (params.image) {
2170
+ this.emitProgress(params.onProgress, { name: "uploadImage", status: "start" });
2171
+ try {
2172
+ const arrayBuffer = await params.image.arrayBuffer();
2173
+ const uint8Array = new Uint8Array(arrayBuffer);
2174
+ const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
2175
+ encoding: params.image.type || "image/jpeg",
2176
+ });
2177
+ if (uploadResult.success) {
2178
+ imageBlobRef = {
2179
+ $type: "blob",
2180
+ ref: uploadResult.data.blob.ref,
2181
+ mimeType: uploadResult.data.blob.mimeType,
2182
+ size: uploadResult.data.blob.size,
2183
+ };
2184
+ }
2185
+ this.emitProgress(params.onProgress, {
2186
+ name: "uploadImage",
2187
+ status: "success",
2188
+ data: { size: params.image.size },
2189
+ });
2190
+ }
2191
+ catch (error) {
2192
+ this.emitProgress(params.onProgress, { name: "uploadImage", status: "error", error: error });
2193
+ throw new NetworkError(`Failed to upload image: ${error instanceof Error ? error.message : "Unknown"}`, error);
2194
+ }
2195
+ }
2196
+ // Step 2: Create rights record
2197
+ this.emitProgress(params.onProgress, { name: "createRights", status: "start" });
2198
+ const rightsRecord = {
2199
+ rightsName: params.rights.name,
2200
+ rightsType: params.rights.type,
2201
+ rightsDescription: params.rights.description,
2202
+ createdAt,
2203
+ };
2204
+ const rightsValidation = this.lexiconRegistry.validate(lexicon.HYPERCERT_COLLECTIONS.RIGHTS, rightsRecord);
2205
+ if (!rightsValidation.valid) {
2206
+ throw new ValidationError(`Invalid rights record: ${rightsValidation.error}`);
2207
+ }
2208
+ const rightsResult = await this.agent.com.atproto.repo.createRecord({
2209
+ repo: this.repoDid,
2210
+ collection: lexicon.HYPERCERT_COLLECTIONS.RIGHTS,
2211
+ record: rightsRecord,
2212
+ });
2213
+ if (!rightsResult.success) {
2214
+ throw new NetworkError("Failed to create rights record");
2215
+ }
2216
+ result.rightsUri = rightsResult.data.uri;
2217
+ result.rightsCid = rightsResult.data.cid;
2218
+ this.emit("rightsCreated", { uri: result.rightsUri, cid: result.rightsCid });
2219
+ this.emitProgress(params.onProgress, {
2220
+ name: "createRights",
2221
+ status: "success",
2222
+ data: { uri: result.rightsUri },
2223
+ });
2224
+ // Step 3: Create hypercert record
2225
+ this.emitProgress(params.onProgress, { name: "createHypercert", status: "start" });
2226
+ const hypercertRecord = {
2227
+ title: params.title,
2228
+ description: params.description,
2229
+ workScope: params.workScope,
2230
+ workTimeframeFrom: params.workTimeframeFrom,
2231
+ workTimeframeTo: params.workTimeframeTo,
2232
+ rights: { uri: result.rightsUri, cid: result.rightsCid },
2233
+ createdAt,
2234
+ };
2235
+ if (params.shortDescription) {
2236
+ hypercertRecord.shortDescription = params.shortDescription;
2237
+ }
2238
+ if (imageBlobRef) {
2239
+ hypercertRecord.image = imageBlobRef;
2240
+ }
2241
+ if (params.evidence && params.evidence.length > 0) {
2242
+ hypercertRecord.evidence = params.evidence;
2243
+ }
2244
+ const hypercertValidation = this.lexiconRegistry.validate(lexicon.HYPERCERT_COLLECTIONS.CLAIM, hypercertRecord);
2245
+ if (!hypercertValidation.valid) {
2246
+ throw new ValidationError(`Invalid hypercert record: ${hypercertValidation.error}`);
2247
+ }
2248
+ const hypercertResult = await this.agent.com.atproto.repo.createRecord({
2249
+ repo: this.repoDid,
2250
+ collection: lexicon.HYPERCERT_COLLECTIONS.CLAIM,
2251
+ record: hypercertRecord,
2252
+ });
2253
+ if (!hypercertResult.success) {
2254
+ throw new NetworkError("Failed to create hypercert record");
2255
+ }
2256
+ result.hypercertUri = hypercertResult.data.uri;
2257
+ result.hypercertCid = hypercertResult.data.cid;
2258
+ this.emit("recordCreated", { uri: result.hypercertUri, cid: result.hypercertCid });
2259
+ this.emitProgress(params.onProgress, {
2260
+ name: "createHypercert",
2261
+ status: "success",
2262
+ data: { uri: result.hypercertUri },
2263
+ });
2264
+ // Step 4: Attach location if provided
2265
+ if (params.location) {
2266
+ this.emitProgress(params.onProgress, { name: "attachLocation", status: "start" });
2267
+ try {
2268
+ const locationResult = await this.attachLocation(result.hypercertUri, params.location);
2269
+ result.locationUri = locationResult.uri;
2270
+ this.emitProgress(params.onProgress, {
2271
+ name: "attachLocation",
2272
+ status: "success",
2273
+ data: { uri: result.locationUri },
2274
+ });
2275
+ }
2276
+ catch (error) {
2277
+ this.emitProgress(params.onProgress, { name: "attachLocation", status: "error", error: error });
2278
+ this.logger?.warn(`Failed to attach location: ${error instanceof Error ? error.message : "Unknown"}`);
2279
+ }
2280
+ }
2281
+ // Step 5: Create contributions if provided
2282
+ if (params.contributions && params.contributions.length > 0) {
2283
+ this.emitProgress(params.onProgress, { name: "createContributions", status: "start" });
2284
+ result.contributionUris = [];
2285
+ try {
2286
+ for (const contrib of params.contributions) {
2287
+ const contribResult = await this.addContribution({
2288
+ hypercertUri: result.hypercertUri,
2289
+ contributors: contrib.contributors,
2290
+ role: contrib.role,
2291
+ description: contrib.description,
2292
+ });
2293
+ result.contributionUris.push(contribResult.uri);
2294
+ }
2295
+ this.emitProgress(params.onProgress, {
2296
+ name: "createContributions",
2297
+ status: "success",
2298
+ data: { count: result.contributionUris.length },
2299
+ });
2300
+ }
2301
+ catch (error) {
2302
+ this.emitProgress(params.onProgress, { name: "createContributions", status: "error", error: error });
2303
+ this.logger?.warn(`Failed to create contributions: ${error instanceof Error ? error.message : "Unknown"}`);
2304
+ }
2305
+ }
2306
+ return result;
2307
+ }
2308
+ catch (error) {
2309
+ if (error instanceof ValidationError || error instanceof NetworkError)
2310
+ throw error;
2311
+ throw new NetworkError(`Failed to create hypercert: ${error instanceof Error ? error.message : "Unknown"}`, error);
2312
+ }
2313
+ }
2314
+ /**
2315
+ * Updates an existing hypercert record.
2316
+ *
2317
+ * @param params - Update parameters
2318
+ * @param params.uri - AT-URI of the hypercert to update
2319
+ * @param params.updates - Partial record with fields to update
2320
+ * @param params.image - New image blob, `null` to remove, `undefined` to keep existing
2321
+ * @returns Promise resolving to update result
2322
+ * @throws {@link ValidationError} if the URI format is invalid or record fails validation
2323
+ * @throws {@link NetworkError} if the update fails
2324
+ *
2325
+ * @remarks
2326
+ * This is a partial update - only specified fields are changed.
2327
+ * The `createdAt` and `rights` fields cannot be changed.
2328
+ *
2329
+ * @example Update title and description
2330
+ * ```typescript
2331
+ * await repo.hypercerts.update({
2332
+ * uri: "at://did:plc:abc/org.hypercerts.hypercert/xyz",
2333
+ * updates: {
2334
+ * title: "Updated Title",
2335
+ * description: "New description",
2336
+ * },
2337
+ * });
2338
+ * ```
2339
+ *
2340
+ * @example Update with new image
2341
+ * ```typescript
2342
+ * await repo.hypercerts.update({
2343
+ * uri: hypercertUri,
2344
+ * updates: { title: "New Title" },
2345
+ * image: newImageBlob,
2346
+ * });
2347
+ * ```
2348
+ *
2349
+ * @example Remove image
2350
+ * ```typescript
2351
+ * await repo.hypercerts.update({
2352
+ * uri: hypercertUri,
2353
+ * updates: {},
2354
+ * image: null, // Explicitly remove image
2355
+ * });
2356
+ * ```
2357
+ */
2358
+ async update(params) {
2359
+ try {
2360
+ const uriMatch = params.uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
2361
+ if (!uriMatch) {
2362
+ throw new ValidationError(`Invalid URI format: ${params.uri}`);
2363
+ }
2364
+ const [, , collection, rkey] = uriMatch;
2365
+ const existing = await this.agent.com.atproto.repo.getRecord({
2366
+ repo: this.repoDid,
2367
+ collection,
2368
+ rkey,
2369
+ });
2370
+ // The existing record comes from ATProto, use it directly
2371
+ // TypeScript ensures type safety through the HypercertClaim interface
2372
+ const existingRecord = existing.data.value;
2373
+ const recordForUpdate = {
2374
+ ...existingRecord,
2375
+ ...params.updates,
2376
+ createdAt: existingRecord.createdAt,
2377
+ rights: existingRecord.rights,
2378
+ };
2379
+ // Handle image update
2380
+ delete recordForUpdate.image;
2381
+ if (params.image !== undefined) {
2382
+ if (params.image === null) {
2383
+ // Remove image
2384
+ }
2385
+ else {
2386
+ const arrayBuffer = await params.image.arrayBuffer();
2387
+ const uint8Array = new Uint8Array(arrayBuffer);
2388
+ const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
2389
+ encoding: params.image.type || "image/jpeg",
2390
+ });
2391
+ if (uploadResult.success) {
2392
+ recordForUpdate.image = {
2393
+ $type: "blob",
2394
+ ref: uploadResult.data.blob.ref,
2395
+ mimeType: uploadResult.data.blob.mimeType,
2396
+ size: uploadResult.data.blob.size,
2397
+ };
2398
+ }
2399
+ }
2400
+ }
2401
+ else if (existingRecord.image) {
2402
+ // Preserve existing image
2403
+ recordForUpdate.image = existingRecord.image;
2404
+ }
2405
+ const validation = this.lexiconRegistry.validate(collection, recordForUpdate);
2406
+ if (!validation.valid) {
2407
+ throw new ValidationError(`Invalid hypercert record: ${validation.error}`);
2408
+ }
2409
+ const result = await this.agent.com.atproto.repo.putRecord({
2410
+ repo: this.repoDid,
2411
+ collection,
2412
+ rkey,
2413
+ record: recordForUpdate,
2414
+ });
2415
+ if (!result.success) {
2416
+ throw new NetworkError("Failed to update hypercert");
2417
+ }
2418
+ this.emit("recordUpdated", { uri: result.data.uri, cid: result.data.cid });
2419
+ return { uri: result.data.uri, cid: result.data.cid };
2420
+ }
2421
+ catch (error) {
2422
+ if (error instanceof ValidationError || error instanceof NetworkError)
2423
+ throw error;
2424
+ throw new NetworkError(`Failed to update hypercert: ${error instanceof Error ? error.message : "Unknown"}`, error);
2425
+ }
2426
+ }
2427
+ /**
2428
+ * Gets a hypercert by its AT-URI.
2429
+ *
2430
+ * @param uri - AT-URI of the hypercert (e.g., "at://did:plc:abc/org.hypercerts.hypercert/xyz")
2431
+ * @returns Promise resolving to hypercert URI, CID, and parsed record
2432
+ * @throws {@link ValidationError} if the URI format is invalid or record doesn't match schema
2433
+ * @throws {@link NetworkError} if the record cannot be fetched
2434
+ *
2435
+ * @example
2436
+ * ```typescript
2437
+ * const { uri, cid, record } = await repo.hypercerts.get(hypercertUri);
2438
+ * console.log(`${record.title}: ${record.description}`);
2439
+ * ```
2440
+ */
2441
+ async get(uri) {
2442
+ try {
2443
+ const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
2444
+ if (!uriMatch) {
2445
+ throw new ValidationError(`Invalid URI format: ${uri}`);
2446
+ }
2447
+ const [, , collection, rkey] = uriMatch;
2448
+ const result = await this.agent.com.atproto.repo.getRecord({
2449
+ repo: this.repoDid,
2450
+ collection,
2451
+ rkey,
2452
+ });
2453
+ if (!result.success) {
2454
+ throw new NetworkError("Failed to get hypercert");
2455
+ }
2456
+ // Validate with lexicon registry (more lenient - doesn't require $type)
2457
+ const validation = this.lexiconRegistry.validate(lexicon.HYPERCERT_COLLECTIONS.CLAIM, result.data.value);
2458
+ if (!validation.valid) {
2459
+ throw new ValidationError(`Invalid hypercert record format: ${validation.error}`);
2460
+ }
2461
+ return {
2462
+ uri: result.data.uri,
2463
+ cid: result.data.cid ?? "",
2464
+ record: result.data.value,
2465
+ };
2466
+ }
2467
+ catch (error) {
2468
+ if (error instanceof ValidationError || error instanceof NetworkError)
2469
+ throw error;
2470
+ throw new NetworkError(`Failed to get hypercert: ${error instanceof Error ? error.message : "Unknown"}`, error);
2471
+ }
2472
+ }
2473
+ /**
2474
+ * Lists hypercerts in the repository with pagination.
2475
+ *
2476
+ * @param params - Optional pagination parameters
2477
+ * @returns Promise resolving to paginated list of hypercerts
2478
+ * @throws {@link NetworkError} if the list operation fails
2479
+ *
2480
+ * @example
2481
+ * ```typescript
2482
+ * // Get first page
2483
+ * const { records, cursor } = await repo.hypercerts.list({ limit: 20 });
2484
+ *
2485
+ * // Get next page
2486
+ * if (cursor) {
2487
+ * const nextPage = await repo.hypercerts.list({ limit: 20, cursor });
2488
+ * }
2489
+ * ```
2490
+ */
2491
+ async list(params) {
2492
+ try {
2493
+ const result = await this.agent.com.atproto.repo.listRecords({
2494
+ repo: this.repoDid,
2495
+ collection: lexicon.HYPERCERT_COLLECTIONS.CLAIM,
2496
+ limit: params?.limit,
2497
+ cursor: params?.cursor,
2498
+ });
2499
+ if (!result.success) {
2500
+ throw new NetworkError("Failed to list hypercerts");
2501
+ }
2502
+ return {
2503
+ records: result.data.records?.map((r) => ({
2504
+ uri: r.uri,
2505
+ cid: r.cid,
2506
+ record: r.value,
2507
+ })) || [],
2508
+ cursor: result.data.cursor ?? undefined,
2509
+ };
2510
+ }
2511
+ catch (error) {
2512
+ if (error instanceof NetworkError)
2513
+ throw error;
2514
+ throw new NetworkError(`Failed to list hypercerts: ${error instanceof Error ? error.message : "Unknown"}`, error);
2515
+ }
2516
+ }
2517
+ /**
2518
+ * Deletes a hypercert record.
2519
+ *
2520
+ * @param uri - AT-URI of the hypercert to delete
2521
+ * @throws {@link ValidationError} if the URI format is invalid
2522
+ * @throws {@link NetworkError} if the deletion fails
2523
+ *
2524
+ * @remarks
2525
+ * This only deletes the hypercert record itself. Related records
2526
+ * (rights, locations, contributions) are not automatically deleted.
2527
+ *
2528
+ * @example
2529
+ * ```typescript
2530
+ * await repo.hypercerts.delete(hypercertUri);
2531
+ * ```
2532
+ */
2533
+ async delete(uri) {
2534
+ try {
2535
+ const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
2536
+ if (!uriMatch) {
2537
+ throw new ValidationError(`Invalid URI format: ${uri}`);
2538
+ }
2539
+ const [, , collection, rkey] = uriMatch;
2540
+ const result = await this.agent.com.atproto.repo.deleteRecord({
2541
+ repo: this.repoDid,
2542
+ collection,
2543
+ rkey,
2544
+ });
2545
+ if (!result.success) {
2546
+ throw new NetworkError("Failed to delete hypercert");
2547
+ }
2548
+ }
2549
+ catch (error) {
2550
+ if (error instanceof ValidationError || error instanceof NetworkError)
2551
+ throw error;
2552
+ throw new NetworkError(`Failed to delete hypercert: ${error instanceof Error ? error.message : "Unknown"}`, error);
2553
+ }
2554
+ }
2555
+ /**
2556
+ * Attaches a location to an existing hypercert.
2557
+ *
2558
+ * @param hypercertUri - AT-URI of the hypercert to attach location to
2559
+ * @param location - Location data
2560
+ * @param location.value - Location value (address, coordinates, or description)
2561
+ * @param location.name - Optional human-readable name
2562
+ * @param location.description - Optional description
2563
+ * @param location.srs - Spatial Reference System (e.g., "EPSG:4326")
2564
+ * @param location.geojson - Optional GeoJSON blob for precise boundaries
2565
+ * @returns Promise resolving to location record URI and CID
2566
+ * @throws {@link ValidationError} if validation fails
2567
+ * @throws {@link NetworkError} if the operation fails
2568
+ *
2569
+ * @example Simple location
2570
+ * ```typescript
2571
+ * await repo.hypercerts.attachLocation(hypercertUri, {
2572
+ * value: "San Francisco, CA",
2573
+ * name: "SF Bay Area",
2574
+ * });
2575
+ * ```
2576
+ *
2577
+ * @example Location with GeoJSON
2578
+ * ```typescript
2579
+ * const geojsonBlob = new Blob([JSON.stringify(geojson)], {
2580
+ * type: "application/geo+json"
2581
+ * });
2582
+ *
2583
+ * await repo.hypercerts.attachLocation(hypercertUri, {
2584
+ * value: "Custom Region",
2585
+ * srs: "EPSG:4326",
2586
+ * geojson: geojsonBlob,
2587
+ * });
2588
+ * ```
2589
+ */
2590
+ async attachLocation(hypercertUri, location) {
2591
+ try {
2592
+ // Get hypercert to get CID
2593
+ const hypercert = await this.get(hypercertUri);
2594
+ const createdAt = new Date().toISOString();
2595
+ let locationValue = location.value;
2596
+ if (location.geojson) {
2597
+ const arrayBuffer = await location.geojson.arrayBuffer();
2598
+ const uint8Array = new Uint8Array(arrayBuffer);
2599
+ const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
2600
+ encoding: location.geojson.type || "application/geo+json",
2601
+ });
2602
+ if (uploadResult.success) {
2603
+ locationValue = {
2604
+ $type: "blob",
2605
+ ref: uploadResult.data.blob.ref,
2606
+ mimeType: uploadResult.data.blob.mimeType,
2607
+ size: uploadResult.data.blob.size,
2608
+ };
2609
+ }
2610
+ }
2611
+ const locationRecord = {
2612
+ hypercert: { uri: hypercert.uri, cid: hypercert.cid },
2613
+ value: locationValue,
2614
+ createdAt,
2615
+ name: location.name,
2616
+ description: location.description,
2617
+ srs: location.srs,
2618
+ };
2619
+ const validation = this.lexiconRegistry.validate(lexicon.HYPERCERT_COLLECTIONS.LOCATION, locationRecord);
2620
+ if (!validation.valid) {
2621
+ throw new ValidationError(`Invalid location record: ${validation.error}`);
2622
+ }
2623
+ const result = await this.agent.com.atproto.repo.createRecord({
2624
+ repo: this.repoDid,
2625
+ collection: lexicon.HYPERCERT_COLLECTIONS.LOCATION,
2626
+ record: locationRecord,
2627
+ });
2628
+ if (!result.success) {
2629
+ throw new NetworkError("Failed to attach location");
2630
+ }
2631
+ this.emit("locationAttached", { uri: result.data.uri, cid: result.data.cid, hypercertUri });
2632
+ return { uri: result.data.uri, cid: result.data.cid };
2633
+ }
2634
+ catch (error) {
2635
+ if (error instanceof ValidationError || error instanceof NetworkError)
2636
+ throw error;
2637
+ throw new NetworkError(`Failed to attach location: ${error instanceof Error ? error.message : "Unknown"}`, error);
2638
+ }
2639
+ }
2640
+ /**
2641
+ * Adds evidence to an existing hypercert.
2642
+ *
2643
+ * @param hypercertUri - AT-URI of the hypercert
2644
+ * @param evidence - Array of evidence items to add
2645
+ * @returns Promise resolving to update result
2646
+ * @throws {@link ValidationError} if validation fails
2647
+ * @throws {@link NetworkError} if the operation fails
2648
+ *
2649
+ * @remarks
2650
+ * Evidence is appended to existing evidence, not replaced.
2651
+ *
2652
+ * @example
2653
+ * ```typescript
2654
+ * await repo.hypercerts.addEvidence(hypercertUri, [
2655
+ * { uri: "https://example.com/report.pdf", description: "Impact report" },
2656
+ * { uri: "https://example.com/data.csv", description: "Raw data" },
2657
+ * ]);
2658
+ * ```
2659
+ */
2660
+ async addEvidence(hypercertUri, evidence) {
2661
+ try {
2662
+ const existing = await this.get(hypercertUri);
2663
+ const existingEvidence = existing.record.evidence || [];
2664
+ const updatedEvidence = [...existingEvidence, ...evidence];
2665
+ const result = await this.update({
2666
+ uri: hypercertUri,
2667
+ updates: { evidence: updatedEvidence },
2668
+ });
2669
+ this.emit("evidenceAdded", { uri: result.uri, cid: result.cid });
2670
+ return result;
2671
+ }
2672
+ catch (error) {
2673
+ if (error instanceof ValidationError || error instanceof NetworkError)
2674
+ throw error;
2675
+ throw new NetworkError(`Failed to add evidence: ${error instanceof Error ? error.message : "Unknown"}`, error);
2676
+ }
2677
+ }
2678
+ /**
2679
+ * Creates a contribution record.
2680
+ *
2681
+ * @param params - Contribution parameters
2682
+ * @param params.hypercertUri - Optional hypercert to link (can be standalone)
2683
+ * @param params.contributors - Array of contributor DIDs
2684
+ * @param params.role - Role of the contributors (e.g., "coordinator", "implementer")
2685
+ * @param params.description - Optional description of the contribution
2686
+ * @returns Promise resolving to contribution record URI and CID
2687
+ * @throws {@link ValidationError} if validation fails
2688
+ * @throws {@link NetworkError} if the operation fails
2689
+ *
2690
+ * @example
2691
+ * ```typescript
2692
+ * await repo.hypercerts.addContribution({
2693
+ * hypercertUri: hypercertUri,
2694
+ * contributors: ["did:plc:alice", "did:plc:bob"],
2695
+ * role: "implementer",
2696
+ * description: "On-ground implementation team",
2697
+ * });
2698
+ * ```
2699
+ */
2700
+ async addContribution(params) {
2701
+ try {
2702
+ const createdAt = new Date().toISOString();
2703
+ const contributionRecord = {
2704
+ contributors: params.contributors,
2705
+ role: params.role,
2706
+ createdAt,
2707
+ description: params.description,
2708
+ };
2709
+ if (params.hypercertUri) {
2710
+ const hypercert = await this.get(params.hypercertUri);
2711
+ contributionRecord.hypercert = { uri: hypercert.uri, cid: hypercert.cid };
2712
+ }
2713
+ const validation = this.lexiconRegistry.validate(lexicon.HYPERCERT_COLLECTIONS.CONTRIBUTION, contributionRecord);
2714
+ if (!validation.valid) {
2715
+ throw new ValidationError(`Invalid contribution record: ${validation.error}`);
2716
+ }
2717
+ const result = await this.agent.com.atproto.repo.createRecord({
2718
+ repo: this.repoDid,
2719
+ collection: lexicon.HYPERCERT_COLLECTIONS.CONTRIBUTION,
2720
+ record: contributionRecord,
2721
+ });
2722
+ if (!result.success) {
2723
+ throw new NetworkError("Failed to create contribution");
2724
+ }
2725
+ this.emit("contributionCreated", { uri: result.data.uri, cid: result.data.cid });
2726
+ return { uri: result.data.uri, cid: result.data.cid };
2727
+ }
2728
+ catch (error) {
2729
+ if (error instanceof ValidationError || error instanceof NetworkError)
2730
+ throw error;
2731
+ throw new NetworkError(`Failed to add contribution: ${error instanceof Error ? error.message : "Unknown"}`, error);
2732
+ }
2733
+ }
2734
+ /**
2735
+ * Creates a measurement record for a hypercert.
2736
+ *
2737
+ * Measurements quantify the impact claimed in a hypercert with
2738
+ * specific metrics and values.
2739
+ *
2740
+ * @param params - Measurement parameters
2741
+ * @param params.hypercertUri - AT-URI of the hypercert being measured
2742
+ * @param params.measurers - DIDs of entities who performed the measurement
2743
+ * @param params.metric - Name of the metric (e.g., "CO2 Reduced", "Trees Planted")
2744
+ * @param params.value - Measured value with units (e.g., "100 tons", "10000")
2745
+ * @param params.methodUri - Optional URI describing the measurement methodology
2746
+ * @param params.evidenceUris - Optional URIs to supporting evidence
2747
+ * @returns Promise resolving to measurement record URI and CID
2748
+ * @throws {@link ValidationError} if validation fails
2749
+ * @throws {@link NetworkError} if the operation fails
2750
+ *
2751
+ * @example
2752
+ * ```typescript
2753
+ * await repo.hypercerts.addMeasurement({
2754
+ * hypercertUri: hypercertUri,
2755
+ * measurers: ["did:plc:auditor"],
2756
+ * metric: "Carbon Offset",
2757
+ * value: "150 tons CO2e",
2758
+ * methodUri: "https://example.com/methodology",
2759
+ * evidenceUris: ["https://example.com/audit-report"],
2760
+ * });
2761
+ * ```
2762
+ */
2763
+ async addMeasurement(params) {
2764
+ try {
2765
+ const hypercert = await this.get(params.hypercertUri);
2766
+ const createdAt = new Date().toISOString();
2767
+ const measurementRecord = {
2768
+ hypercert: { uri: hypercert.uri, cid: hypercert.cid },
2769
+ measurers: params.measurers,
2770
+ metric: params.metric,
2771
+ value: params.value,
2772
+ createdAt,
2773
+ measurementMethodURI: params.methodUri,
2774
+ evidenceURI: params.evidenceUris,
2775
+ };
2776
+ const validation = this.lexiconRegistry.validate(lexicon.HYPERCERT_COLLECTIONS.MEASUREMENT, measurementRecord);
2777
+ if (!validation.valid) {
2778
+ throw new ValidationError(`Invalid measurement record: ${validation.error}`);
2779
+ }
2780
+ const result = await this.agent.com.atproto.repo.createRecord({
2781
+ repo: this.repoDid,
2782
+ collection: lexicon.HYPERCERT_COLLECTIONS.MEASUREMENT,
2783
+ record: measurementRecord,
2784
+ });
2785
+ if (!result.success) {
2786
+ throw new NetworkError("Failed to create measurement");
2787
+ }
2788
+ return { uri: result.data.uri, cid: result.data.cid };
2789
+ }
2790
+ catch (error) {
2791
+ if (error instanceof ValidationError || error instanceof NetworkError)
2792
+ throw error;
2793
+ throw new NetworkError(`Failed to add measurement: ${error instanceof Error ? error.message : "Unknown"}`, error);
2794
+ }
2795
+ }
2796
+ /**
2797
+ * Creates an evaluation record for a hypercert or other subject.
2798
+ *
2799
+ * Evaluations provide third-party assessments of impact claims.
2800
+ *
2801
+ * @param params - Evaluation parameters
2802
+ * @param params.subjectUri - AT-URI of the record being evaluated
2803
+ * @param params.evaluators - DIDs of evaluating entities
2804
+ * @param params.summary - Summary of the evaluation findings
2805
+ * @returns Promise resolving to evaluation record URI and CID
2806
+ * @throws {@link ValidationError} if validation fails
2807
+ * @throws {@link NetworkError} if the operation fails
2808
+ *
2809
+ * @example
2810
+ * ```typescript
2811
+ * await repo.hypercerts.addEvaluation({
2812
+ * subjectUri: hypercertUri,
2813
+ * evaluators: ["did:plc:evaluator-org"],
2814
+ * summary: "Verified impact claims through site visit and data analysis",
2815
+ * });
2816
+ * ```
2817
+ */
2818
+ async addEvaluation(params) {
2819
+ try {
2820
+ const subject = await this.get(params.subjectUri);
2821
+ const createdAt = new Date().toISOString();
2822
+ const evaluationRecord = {
2823
+ subject: { uri: subject.uri, cid: subject.cid },
2824
+ evaluators: params.evaluators,
2825
+ summary: params.summary,
2826
+ createdAt,
2827
+ };
2828
+ const validation = this.lexiconRegistry.validate(lexicon.HYPERCERT_COLLECTIONS.EVALUATION, evaluationRecord);
2829
+ if (!validation.valid) {
2830
+ throw new ValidationError(`Invalid evaluation record: ${validation.error}`);
2831
+ }
2832
+ const result = await this.agent.com.atproto.repo.createRecord({
2833
+ repo: this.repoDid,
2834
+ collection: lexicon.HYPERCERT_COLLECTIONS.EVALUATION,
2835
+ record: evaluationRecord,
2836
+ });
2837
+ if (!result.success) {
2838
+ throw new NetworkError("Failed to create evaluation");
2839
+ }
2840
+ return { uri: result.data.uri, cid: result.data.cid };
2841
+ }
2842
+ catch (error) {
2843
+ if (error instanceof ValidationError || error instanceof NetworkError)
2844
+ throw error;
2845
+ throw new NetworkError(`Failed to add evaluation: ${error instanceof Error ? error.message : "Unknown"}`, error);
2846
+ }
2847
+ }
2848
+ /**
2849
+ * Creates a collection of hypercerts.
2850
+ *
2851
+ * Collections group related hypercerts with optional weights
2852
+ * for relative importance.
2853
+ *
2854
+ * @param params - Collection parameters
2855
+ * @param params.title - Collection title
2856
+ * @param params.claims - Array of hypercert references with weights
2857
+ * @param params.shortDescription - Optional short description
2858
+ * @param params.coverPhoto - Optional cover image blob
2859
+ * @returns Promise resolving to collection record URI and CID
2860
+ * @throws {@link ValidationError} if validation fails
2861
+ * @throws {@link NetworkError} if the operation fails
2862
+ *
2863
+ * @example
2864
+ * ```typescript
2865
+ * const collection = await repo.hypercerts.createCollection({
2866
+ * title: "Climate Projects 2024",
2867
+ * shortDescription: "Our climate impact portfolio",
2868
+ * claims: [
2869
+ * { uri: hypercert1Uri, cid: hypercert1Cid, weight: "0.5" },
2870
+ * { uri: hypercert2Uri, cid: hypercert2Cid, weight: "0.3" },
2871
+ * { uri: hypercert3Uri, cid: hypercert3Cid, weight: "0.2" },
2872
+ * ],
2873
+ * coverPhoto: coverImageBlob,
2874
+ * });
2875
+ * ```
2876
+ */
2877
+ async createCollection(params) {
2878
+ try {
2879
+ const createdAt = new Date().toISOString();
2880
+ let coverPhotoRef;
2881
+ if (params.coverPhoto) {
2882
+ const arrayBuffer = await params.coverPhoto.arrayBuffer();
2883
+ const uint8Array = new Uint8Array(arrayBuffer);
2884
+ const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
2885
+ encoding: params.coverPhoto.type || "image/jpeg",
2886
+ });
2887
+ if (uploadResult.success) {
2888
+ coverPhotoRef = {
2889
+ $type: "blob",
2890
+ ref: uploadResult.data.blob.ref,
2891
+ mimeType: uploadResult.data.blob.mimeType,
2892
+ size: uploadResult.data.blob.size,
2893
+ };
2894
+ }
2895
+ }
2896
+ const collectionRecord = {
2897
+ title: params.title,
2898
+ claims: params.claims.map((c) => ({ claim: { uri: c.uri, cid: c.cid }, weight: c.weight })),
2899
+ createdAt,
2900
+ };
2901
+ if (params.shortDescription) {
2902
+ collectionRecord.shortDescription = params.shortDescription;
2903
+ }
2904
+ if (coverPhotoRef) {
2905
+ collectionRecord.coverPhoto = coverPhotoRef;
2906
+ }
2907
+ const validation = this.lexiconRegistry.validate(lexicon.HYPERCERT_COLLECTIONS.COLLECTION, collectionRecord);
2908
+ if (!validation.valid) {
2909
+ throw new ValidationError(`Invalid collection record: ${validation.error}`);
2910
+ }
2911
+ const result = await this.agent.com.atproto.repo.createRecord({
2912
+ repo: this.repoDid,
2913
+ collection: lexicon.HYPERCERT_COLLECTIONS.COLLECTION,
2914
+ record: collectionRecord,
2915
+ });
2916
+ if (!result.success) {
2917
+ throw new NetworkError("Failed to create collection");
2918
+ }
2919
+ this.emit("collectionCreated", { uri: result.data.uri, cid: result.data.cid });
2920
+ return { uri: result.data.uri, cid: result.data.cid };
2921
+ }
2922
+ catch (error) {
2923
+ if (error instanceof ValidationError || error instanceof NetworkError)
2924
+ throw error;
2925
+ throw new NetworkError(`Failed to create collection: ${error instanceof Error ? error.message : "Unknown"}`, error);
2926
+ }
2927
+ }
2928
+ /**
2929
+ * Gets a collection by its AT-URI.
2930
+ *
2931
+ * @param uri - AT-URI of the collection
2932
+ * @returns Promise resolving to collection URI, CID, and parsed record
2933
+ * @throws {@link ValidationError} if the URI format is invalid or record doesn't match schema
2934
+ * @throws {@link NetworkError} if the record cannot be fetched
2935
+ *
2936
+ * @example
2937
+ * ```typescript
2938
+ * const { record } = await repo.hypercerts.getCollection(collectionUri);
2939
+ * console.log(`Collection: ${record.title}`);
2940
+ * console.log(`Contains ${record.claims.length} hypercerts`);
2941
+ * ```
2942
+ */
2943
+ async getCollection(uri) {
2944
+ try {
2945
+ const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
2946
+ if (!uriMatch) {
2947
+ throw new ValidationError(`Invalid URI format: ${uri}`);
2948
+ }
2949
+ const [, , collection, rkey] = uriMatch;
2950
+ const result = await this.agent.com.atproto.repo.getRecord({
2951
+ repo: this.repoDid,
2952
+ collection,
2953
+ rkey,
2954
+ });
2955
+ if (!result.success) {
2956
+ throw new NetworkError("Failed to get collection");
2957
+ }
2958
+ // Validate with lexicon registry (more lenient - doesn't require $type)
2959
+ const validation = this.lexiconRegistry.validate(lexicon.HYPERCERT_COLLECTIONS.COLLECTION, result.data.value);
2960
+ if (!validation.valid) {
2961
+ throw new ValidationError(`Invalid collection record format: ${validation.error}`);
2962
+ }
2963
+ return {
2964
+ uri: result.data.uri,
2965
+ cid: result.data.cid ?? "",
2966
+ record: result.data.value,
2967
+ };
2968
+ }
2969
+ catch (error) {
2970
+ if (error instanceof ValidationError || error instanceof NetworkError)
2971
+ throw error;
2972
+ throw new NetworkError(`Failed to get collection: ${error instanceof Error ? error.message : "Unknown"}`, error);
2973
+ }
2974
+ }
2975
+ /**
2976
+ * Lists collections in the repository with pagination.
2977
+ *
2978
+ * @param params - Optional pagination parameters
2979
+ * @returns Promise resolving to paginated list of collections
2980
+ * @throws {@link NetworkError} if the list operation fails
2981
+ *
2982
+ * @example
2983
+ * ```typescript
2984
+ * const { records } = await repo.hypercerts.listCollections();
2985
+ * for (const { record } of records) {
2986
+ * console.log(`${record.title}: ${record.claims.length} claims`);
2987
+ * }
2988
+ * ```
2989
+ */
2990
+ async listCollections(params) {
2991
+ try {
2992
+ const result = await this.agent.com.atproto.repo.listRecords({
2993
+ repo: this.repoDid,
2994
+ collection: lexicon.HYPERCERT_COLLECTIONS.COLLECTION,
2995
+ limit: params?.limit,
2996
+ cursor: params?.cursor,
2997
+ });
2998
+ if (!result.success) {
2999
+ throw new NetworkError("Failed to list collections");
3000
+ }
3001
+ return {
3002
+ records: result.data.records?.map((r) => ({
3003
+ uri: r.uri,
3004
+ cid: r.cid,
3005
+ record: r.value,
3006
+ })) || [],
3007
+ cursor: result.data.cursor ?? undefined,
3008
+ };
3009
+ }
3010
+ catch (error) {
3011
+ if (error instanceof NetworkError)
3012
+ throw error;
3013
+ throw new NetworkError(`Failed to list collections: ${error instanceof Error ? error.message : "Unknown"}`, error);
3014
+ }
3015
+ }
3016
+ }
3017
+
3018
+ /**
3019
+ * CollaboratorOperationsImpl - SDS collaborator management operations.
3020
+ *
3021
+ * This module provides the implementation for managing collaborator
3022
+ * access on Shared Data Server (SDS) repositories.
3023
+ *
3024
+ * @packageDocumentation
3025
+ */
3026
+ /**
3027
+ * Implementation of collaborator operations for SDS access control.
3028
+ *
3029
+ * This class manages access permissions for shared repositories on
3030
+ * Shared Data Servers (SDS). It provides role-based access control
3031
+ * with predefined permission sets.
3032
+ *
3033
+ * @remarks
3034
+ * This class is typically not instantiated directly. Access it through
3035
+ * {@link Repository.collaborators} on an SDS-connected repository.
3036
+ *
3037
+ * **Role Hierarchy**:
3038
+ * - `viewer`: Read-only access
3039
+ * - `editor`: Read + Create + Update
3040
+ * - `admin`: All permissions except ownership transfer
3041
+ * - `owner`: Full control including ownership management
3042
+ *
3043
+ * **SDS API Endpoints Used**:
3044
+ * - `com.atproto.sds.grantAccess`: Grant access to a user
3045
+ * - `com.atproto.sds.revokeAccess`: Revoke access from a user
3046
+ * - `com.atproto.sds.listCollaborators`: List all collaborators
3047
+ *
3048
+ * @example
3049
+ * ```typescript
3050
+ * // Get SDS repository
3051
+ * const sdsRepo = sdk.repository(session, { server: "sds" });
3052
+ *
3053
+ * // Grant editor access
3054
+ * await sdsRepo.collaborators.grant({
3055
+ * userDid: "did:plc:new-user",
3056
+ * role: "editor",
3057
+ * });
3058
+ *
3059
+ * // List all collaborators
3060
+ * const collaborators = await sdsRepo.collaborators.list();
3061
+ *
3062
+ * // Check specific user
3063
+ * const hasAccess = await sdsRepo.collaborators.hasAccess("did:plc:someone");
3064
+ * const role = await sdsRepo.collaborators.getRole("did:plc:someone");
3065
+ * ```
3066
+ *
3067
+ * @internal
3068
+ */
3069
+ class CollaboratorOperationsImpl {
3070
+ /**
3071
+ * Creates a new CollaboratorOperationsImpl.
3072
+ *
3073
+ * @param session - Authenticated OAuth session with fetchHandler
3074
+ * @param repoDid - DID of the repository to manage
3075
+ * @param serverUrl - SDS server URL
3076
+ *
3077
+ * @internal
3078
+ */
3079
+ constructor(session, repoDid, serverUrl) {
3080
+ this.session = session;
3081
+ this.repoDid = repoDid;
3082
+ this.serverUrl = serverUrl;
3083
+ }
3084
+ /**
3085
+ * Converts a role to its corresponding permissions object.
3086
+ *
3087
+ * @param role - The role to convert
3088
+ * @returns Permission flags for the role
3089
+ * @internal
3090
+ */
3091
+ roleToPermissions(role) {
3092
+ switch (role) {
3093
+ case "viewer":
3094
+ return { read: true, create: false, update: false, delete: false, admin: false, owner: false };
3095
+ case "editor":
3096
+ return { read: true, create: true, update: true, delete: false, admin: false, owner: false };
3097
+ case "admin":
3098
+ return { read: true, create: true, update: true, delete: true, admin: true, owner: false };
3099
+ case "owner":
3100
+ return { read: true, create: true, update: true, delete: true, admin: true, owner: true };
3101
+ }
3102
+ }
3103
+ /**
3104
+ * Determines the role from a permissions object.
3105
+ *
3106
+ * @param permissions - The permissions to analyze
3107
+ * @returns The highest role matching the permissions
3108
+ * @internal
3109
+ */
3110
+ permissionsToRole(permissions) {
3111
+ if (permissions.owner)
3112
+ return "owner";
3113
+ if (permissions.admin)
3114
+ return "admin";
3115
+ if (permissions.create || permissions.update)
3116
+ return "editor";
3117
+ return "viewer";
3118
+ }
3119
+ /**
3120
+ * Grants repository access to a user.
3121
+ *
3122
+ * @param params - Grant parameters
3123
+ * @param params.userDid - DID of the user to grant access to
3124
+ * @param params.role - Role to assign (determines permissions)
3125
+ * @throws {@link NetworkError} if the grant operation fails
3126
+ *
3127
+ * @remarks
3128
+ * If the user already has access, their permissions are updated
3129
+ * to the new role.
3130
+ *
3131
+ * @example
3132
+ * ```typescript
3133
+ * // Grant viewer access
3134
+ * await repo.collaborators.grant({
3135
+ * userDid: "did:plc:viewer-user",
3136
+ * role: "viewer",
3137
+ * });
3138
+ *
3139
+ * // Upgrade to editor
3140
+ * await repo.collaborators.grant({
3141
+ * userDid: "did:plc:viewer-user",
3142
+ * role: "editor",
3143
+ * });
3144
+ * ```
3145
+ */
3146
+ async grant(params) {
3147
+ const permissions = this.roleToPermissions(params.role);
3148
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.grantAccess`, {
3149
+ method: "POST",
3150
+ headers: { "Content-Type": "application/json" },
3151
+ body: JSON.stringify({
3152
+ repo: this.repoDid,
3153
+ userDid: params.userDid,
3154
+ permissions,
3155
+ }),
3156
+ });
3157
+ if (!response.ok) {
3158
+ throw new NetworkError(`Failed to grant access: ${response.statusText}`);
3159
+ }
3160
+ }
3161
+ /**
3162
+ * Revokes repository access from a user.
3163
+ *
3164
+ * @param params - Revoke parameters
3165
+ * @param params.userDid - DID of the user to revoke access from
3166
+ * @throws {@link NetworkError} if the revoke operation fails
3167
+ *
3168
+ * @remarks
3169
+ * - Cannot revoke access from the repository owner
3170
+ * - Revoked access is recorded with a `revokedAt` timestamp
3171
+ *
3172
+ * @example
3173
+ * ```typescript
3174
+ * await repo.collaborators.revoke({
3175
+ * userDid: "did:plc:former-collaborator",
3176
+ * });
3177
+ * ```
3178
+ */
3179
+ async revoke(params) {
3180
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.revokeAccess`, {
3181
+ method: "POST",
3182
+ headers: { "Content-Type": "application/json" },
3183
+ body: JSON.stringify({
3184
+ repo: this.repoDid,
3185
+ userDid: params.userDid,
3186
+ }),
3187
+ });
3188
+ if (!response.ok) {
3189
+ throw new NetworkError(`Failed to revoke access: ${response.statusText}`);
3190
+ }
3191
+ }
3192
+ /**
3193
+ * Lists all collaborators on the repository.
3194
+ *
3195
+ * @returns Promise resolving to array of access grants
3196
+ * @throws {@link NetworkError} if the list operation fails
3197
+ *
3198
+ * @remarks
3199
+ * The list includes both active and revoked collaborators.
3200
+ * Check `revokedAt` to filter active collaborators.
3201
+ *
3202
+ * @example
3203
+ * ```typescript
3204
+ * const collaborators = await repo.collaborators.list();
3205
+ *
3206
+ * // Filter active collaborators
3207
+ * const active = collaborators.filter(c => !c.revokedAt);
3208
+ *
3209
+ * // Group by role
3210
+ * const byRole = {
3211
+ * owners: active.filter(c => c.role === "owner"),
3212
+ * admins: active.filter(c => c.role === "admin"),
3213
+ * editors: active.filter(c => c.role === "editor"),
3214
+ * viewers: active.filter(c => c.role === "viewer"),
3215
+ * };
3216
+ * ```
3217
+ */
3218
+ async list() {
3219
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.listCollaborators?repo=${encodeURIComponent(this.repoDid)}`, { method: "GET" });
3220
+ if (!response.ok) {
3221
+ throw new NetworkError(`Failed to list collaborators: ${response.statusText}`);
3222
+ }
3223
+ const data = await response.json();
3224
+ return (data.collaborators || []).map((c) => ({
3225
+ userDid: c.userDid,
3226
+ role: this.permissionsToRole(c.permissions),
3227
+ permissions: c.permissions,
3228
+ grantedBy: c.grantedBy,
3229
+ grantedAt: c.grantedAt,
3230
+ revokedAt: c.revokedAt,
3231
+ }));
3232
+ }
3233
+ /**
3234
+ * Checks if a user has any access to the repository.
3235
+ *
3236
+ * @param userDid - DID of the user to check
3237
+ * @returns Promise resolving to `true` if user has active access
3238
+ *
3239
+ * @remarks
3240
+ * Returns `false` if:
3241
+ * - User was never granted access
3242
+ * - User's access was revoked
3243
+ * - The list operation fails (error is suppressed)
3244
+ *
3245
+ * @example
3246
+ * ```typescript
3247
+ * if (await repo.collaborators.hasAccess("did:plc:someone")) {
3248
+ * console.log("User has access");
3249
+ * }
3250
+ * ```
3251
+ */
3252
+ async hasAccess(userDid) {
3253
+ try {
3254
+ const collaborators = await this.list();
3255
+ return collaborators.some((c) => c.userDid === userDid && !c.revokedAt);
3256
+ }
3257
+ catch {
3258
+ return false;
3259
+ }
3260
+ }
3261
+ /**
3262
+ * Gets the role assigned to a user.
3263
+ *
3264
+ * @param userDid - DID of the user to check
3265
+ * @returns Promise resolving to the user's role, or `null` if no active access
3266
+ *
3267
+ * @example
3268
+ * ```typescript
3269
+ * const role = await repo.collaborators.getRole("did:plc:someone");
3270
+ * if (role === "admin" || role === "owner") {
3271
+ * // User can manage other collaborators
3272
+ * }
3273
+ * ```
3274
+ */
3275
+ async getRole(userDid) {
3276
+ const collaborators = await this.list();
3277
+ const collab = collaborators.find((c) => c.userDid === userDid && !c.revokedAt);
3278
+ return collab?.role ?? null;
3279
+ }
3280
+ }
3281
+
3282
+ /**
3283
+ * OrganizationOperationsImpl - SDS organization management operations.
3284
+ *
3285
+ * This module provides the implementation for creating and managing
3286
+ * organizations on Shared Data Server (SDS) instances.
3287
+ *
3288
+ * @packageDocumentation
3289
+ */
3290
+ /**
3291
+ * Implementation of organization operations for SDS management.
3292
+ *
3293
+ * Organizations on SDS provide a way to create shared repositories
3294
+ * that multiple users can collaborate on. Each organization has:
3295
+ *
3296
+ * - A unique DID (Decentralized Identifier)
3297
+ * - A handle for human-readable identification
3298
+ * - An owner and optional collaborators
3299
+ * - Its own repository for storing records
3300
+ *
3301
+ * @remarks
3302
+ * This class is typically not instantiated directly. Access it through
3303
+ * {@link Repository.organizations} on an SDS-connected repository.
3304
+ *
3305
+ * **SDS API Endpoints Used**:
3306
+ * - `com.atproto.sds.createRepository`: Create a new organization
3307
+ * - `com.atproto.sds.listRepositories`: List accessible organizations
3308
+ *
3309
+ * **Access Types**:
3310
+ * - `"owner"`: User created or owns the organization
3311
+ * - `"collaborator"`: User was invited with specific permissions
3312
+ *
3313
+ * @example
3314
+ * ```typescript
3315
+ * // Get SDS repository
3316
+ * const sdsRepo = sdk.repository(session, { server: "sds" });
3317
+ *
3318
+ * // Create an organization
3319
+ * const org = await sdsRepo.organizations.create({
3320
+ * name: "My Team",
3321
+ * description: "A team for impact projects",
3322
+ * });
3323
+ *
3324
+ * // List organizations you have access to
3325
+ * const orgs = await sdsRepo.organizations.list();
3326
+ *
3327
+ * // Get specific organization
3328
+ * const orgInfo = await sdsRepo.organizations.get(org.did);
3329
+ * ```
3330
+ *
3331
+ * @internal
3332
+ */
3333
+ class OrganizationOperationsImpl {
3334
+ /**
3335
+ * Creates a new OrganizationOperationsImpl.
3336
+ *
3337
+ * @param session - Authenticated OAuth session with fetchHandler
3338
+ * @param _repoDid - DID of the user's repository (reserved for future use)
3339
+ * @param serverUrl - SDS server URL
3340
+ * @param _logger - Optional logger for debugging (reserved for future use)
3341
+ *
3342
+ * @internal
3343
+ */
3344
+ constructor(session, _repoDid, serverUrl, _logger) {
3345
+ this.session = session;
3346
+ this._repoDid = _repoDid;
3347
+ this.serverUrl = serverUrl;
3348
+ this._logger = _logger;
3349
+ }
3350
+ /**
3351
+ * Creates a new organization.
3352
+ *
3353
+ * @param params - Organization parameters
3354
+ * @param params.name - Display name for the organization
3355
+ * @param params.description - Optional description of the organization's purpose
3356
+ * @param params.handle - Optional custom handle. If not provided, one is auto-generated.
3357
+ * @returns Promise resolving to the created organization info
3358
+ * @throws {@link NetworkError} if organization creation fails
3359
+ *
3360
+ * @remarks
3361
+ * The creating user automatically becomes the owner with full permissions.
3362
+ *
3363
+ * **Handle Format**: Handles are typically formatted as
3364
+ * `{name}.sds.{domain}` (e.g., "my-team.sds.hypercerts.org").
3365
+ * If you provide a custom handle, it must be unique on the SDS.
3366
+ *
3367
+ * @example Basic organization
3368
+ * ```typescript
3369
+ * const org = await repo.organizations.create({
3370
+ * name: "Climate Action Team",
3371
+ * });
3372
+ * console.log(`Created org: ${org.did}`);
3373
+ * ```
3374
+ *
3375
+ * @example With description and custom handle
3376
+ * ```typescript
3377
+ * const org = await repo.organizations.create({
3378
+ * name: "Reforestation Initiative",
3379
+ * description: "Coordinating tree planting projects worldwide",
3380
+ * handle: "reforestation",
3381
+ * });
3382
+ * ```
3383
+ */
3384
+ async create(params) {
3385
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.createRepository`, {
3386
+ method: "POST",
3387
+ headers: { "Content-Type": "application/json" },
3388
+ body: JSON.stringify(params),
3389
+ });
3390
+ if (!response.ok) {
3391
+ throw new NetworkError(`Failed to create organization: ${response.statusText}`);
3392
+ }
3393
+ const data = await response.json();
3394
+ return {
3395
+ did: data.did,
3396
+ handle: data.handle,
3397
+ name: data.name,
3398
+ description: data.description,
3399
+ createdAt: data.createdAt || new Date().toISOString(),
3400
+ accessType: "owner",
3401
+ permissions: { read: true, create: true, update: true, delete: true, admin: true, owner: true },
3402
+ };
3403
+ }
3404
+ /**
3405
+ * Gets an organization by its DID.
3406
+ *
3407
+ * @param did - The organization's DID
3408
+ * @returns Promise resolving to organization info, or `null` if not found
3409
+ *
3410
+ * @remarks
3411
+ * This method searches through the user's accessible organizations.
3412
+ * If the organization exists but the user doesn't have access,
3413
+ * it will return `null`.
3414
+ *
3415
+ * @example
3416
+ * ```typescript
3417
+ * const org = await repo.organizations.get("did:plc:org123");
3418
+ * if (org) {
3419
+ * console.log(`Found: ${org.name}`);
3420
+ * console.log(`Your role: ${org.accessType}`);
3421
+ * } else {
3422
+ * console.log("Organization not found or no access");
3423
+ * }
3424
+ * ```
3425
+ */
3426
+ async get(did) {
3427
+ try {
3428
+ const orgs = await this.list();
3429
+ return orgs.find((o) => o.did === did) ?? null;
3430
+ }
3431
+ catch {
3432
+ return null;
3433
+ }
3434
+ }
3435
+ /**
3436
+ * Lists organizations the current user has access to.
3437
+ *
3438
+ * @returns Promise resolving to array of organization info
3439
+ * @throws {@link NetworkError} if the list operation fails
3440
+ *
3441
+ * @remarks
3442
+ * Returns organizations where the user is either:
3443
+ * - The owner
3444
+ * - A collaborator with any permission level
3445
+ *
3446
+ * The `accessType` field indicates the user's relationship to each organization.
3447
+ *
3448
+ * @example
3449
+ * ```typescript
3450
+ * const orgs = await repo.organizations.list();
3451
+ *
3452
+ * // Filter by access type
3453
+ * const owned = orgs.filter(o => o.accessType === "owner");
3454
+ * const collaborated = orgs.filter(o => o.accessType === "collaborator");
3455
+ *
3456
+ * console.log(`You own ${owned.length} organizations`);
3457
+ * console.log(`You collaborate on ${collaborated.length} organizations`);
3458
+ * ```
3459
+ *
3460
+ * @example Display organization details
3461
+ * ```typescript
3462
+ * const orgs = await repo.organizations.list();
3463
+ *
3464
+ * for (const org of orgs) {
3465
+ * console.log(`${org.name} (@${org.handle})`);
3466
+ * console.log(` DID: ${org.did}`);
3467
+ * console.log(` Access: ${org.accessType}`);
3468
+ * if (org.description) {
3469
+ * console.log(` Description: ${org.description}`);
3470
+ * }
3471
+ * }
3472
+ * ```
3473
+ */
3474
+ async list() {
3475
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.listRepositories?userDid=${encodeURIComponent(this.session.did || this.session.sub)}`, { method: "GET" });
3476
+ if (!response.ok) {
3477
+ throw new NetworkError(`Failed to list organizations: ${response.statusText}`);
3478
+ }
3479
+ const data = await response.json();
3480
+ return (data.repositories || []).map((r) => ({
3481
+ did: r.did,
3482
+ handle: r.handle,
3483
+ name: r.name,
3484
+ description: r.description,
3485
+ createdAt: new Date().toISOString(), // SDS may not return this
3486
+ accessType: r.accessType,
3487
+ permissions: r.permissions,
3488
+ }));
3489
+ }
3490
+ }
3491
+
3492
+ /**
3493
+ * Repository - Unified fluent API for ATProto repository operations.
3494
+ *
3495
+ * This module provides the main interface for interacting with AT Protocol
3496
+ * data servers (PDS and SDS) through a consistent, fluent API.
3497
+ *
3498
+ * @packageDocumentation
3499
+ */
3500
+ /**
3501
+ * Repository provides a fluent API for AT Protocol data operations.
3502
+ *
3503
+ * This class is the primary interface for working with data in the AT Protocol
3504
+ * ecosystem. It provides organized access to:
3505
+ *
3506
+ * - **Records**: Low-level CRUD operations for any AT Protocol record type
3507
+ * - **Blobs**: Binary data upload and retrieval (images, files)
3508
+ * - **Profile**: User profile management
3509
+ * - **Hypercerts**: High-level hypercert creation and management
3510
+ * - **Collaborators**: Access control for shared repositories (SDS only)
3511
+ * - **Organizations**: Organization management (SDS only)
3512
+ *
3513
+ * @remarks
3514
+ * The Repository uses lazy initialization for operation handlers - they are
3515
+ * created only when first accessed. This improves performance when you only
3516
+ * need a subset of operations.
3517
+ *
3518
+ * **PDS vs SDS:**
3519
+ * - **PDS (Personal Data Server)**: User's own data storage. All operations
3520
+ * except collaborators and organizations are available.
3521
+ * - **SDS (Shared Data Server)**: Collaborative data storage with access
3522
+ * control. All operations including collaborators and organizations.
3523
+ *
3524
+ * @example Basic usage
3525
+ * ```typescript
3526
+ * // Get a repository from the SDK
3527
+ * const repo = sdk.repository(session);
3528
+ *
3529
+ * // Access user profile
3530
+ * const profile = await repo.profile.get();
3531
+ *
3532
+ * // Create a hypercert
3533
+ * const result = await repo.hypercerts.create({
3534
+ * title: "My Impact",
3535
+ * description: "Description of the impact",
3536
+ * workScope: "Climate Action",
3537
+ * workTimeframeFrom: "2024-01-01",
3538
+ * workTimeframeTo: "2024-12-31",
3539
+ * rights: {
3540
+ * name: "Attribution",
3541
+ * type: "license",
3542
+ * description: "CC-BY-4.0",
3543
+ * },
3544
+ * });
3545
+ * ```
3546
+ *
3547
+ * @example Working with a different user's repository
3548
+ * ```typescript
3549
+ * // Get the current user's repo
3550
+ * const myRepo = sdk.repository(session);
3551
+ *
3552
+ * // Get another user's repo (read-only for most operations)
3553
+ * const otherRepo = myRepo.repo("did:plc:other-user-did");
3554
+ * const theirProfile = await otherRepo.profile.get();
3555
+ * ```
3556
+ *
3557
+ * @example SDS operations
3558
+ * ```typescript
3559
+ * // Get SDS repository for collaborator features
3560
+ * const sdsRepo = sdk.repository(session, { server: "sds" });
3561
+ *
3562
+ * // Manage collaborators
3563
+ * await sdsRepo.collaborators.grant({
3564
+ * userDid: "did:plc:collaborator",
3565
+ * role: "editor",
3566
+ * });
3567
+ *
3568
+ * // List organizations
3569
+ * const orgs = await sdsRepo.organizations.list();
3570
+ * ```
3571
+ *
3572
+ * @see {@link ATProtoSDK.repository} for creating Repository instances
3573
+ */
3574
+ class Repository {
3575
+ /**
3576
+ * Creates a new Repository instance.
3577
+ *
3578
+ * @param session - Authenticated OAuth session
3579
+ * @param serverUrl - Base URL of the AT Protocol server
3580
+ * @param repoDid - DID of the repository to operate on
3581
+ * @param lexiconRegistry - Registry for lexicon validation
3582
+ * @param isSDS - Whether this is a Shared Data Server
3583
+ * @param logger - Optional logger for debugging
3584
+ *
3585
+ * @remarks
3586
+ * This constructor is typically not called directly. Use
3587
+ * {@link ATProtoSDK.repository} to create Repository instances.
3588
+ *
3589
+ * @internal
3590
+ */
3591
+ constructor(session, serverUrl, repoDid, lexiconRegistry, isSDS, logger) {
3592
+ this.session = session;
3593
+ this.serverUrl = serverUrl;
3594
+ this.repoDid = repoDid;
3595
+ this.lexiconRegistry = lexiconRegistry;
3596
+ this._isSDS = isSDS;
3597
+ this.logger = logger;
3598
+ // Create Agent with OAuth session
3599
+ this.agent = new api.Agent(session);
3600
+ this.lexiconRegistry.addToAgent(this.agent);
3601
+ // Register hypercert lexicons
3602
+ this.lexiconRegistry.registerMany(lexicon.HYPERCERT_LEXICONS);
3603
+ }
3604
+ /**
3605
+ * The DID (Decentralized Identifier) of this repository.
3606
+ *
3607
+ * This is the user or organization that owns the repository data.
3608
+ *
3609
+ * @example
3610
+ * ```typescript
3611
+ * console.log(`Working with repo: ${repo.did}`);
3612
+ * // Output: Working with repo: did:plc:abc123xyz...
3613
+ * ```
3614
+ */
3615
+ get did() {
3616
+ return this.repoDid;
3617
+ }
3618
+ /**
3619
+ * Whether this repository is on a Shared Data Server (SDS).
3620
+ *
3621
+ * SDS servers support additional features like collaborators and
3622
+ * organizations. Attempting to use these features on a PDS will
3623
+ * throw {@link SDSRequiredError}.
3624
+ *
3625
+ * @example
3626
+ * ```typescript
3627
+ * if (repo.isSDS) {
3628
+ * const collaborators = await repo.collaborators.list();
3629
+ * }
3630
+ * ```
3631
+ */
3632
+ get isSDS() {
3633
+ return this._isSDS;
3634
+ }
3635
+ /**
3636
+ * Gets the server URL this repository connects to.
3637
+ *
3638
+ * @returns The base URL of the AT Protocol server
3639
+ *
3640
+ * @example
3641
+ * ```typescript
3642
+ * console.log(repo.getServerUrl());
3643
+ * // Output: https://bsky.social or https://sds.hypercerts.org
3644
+ * ```
3645
+ */
3646
+ getServerUrl() {
3647
+ return this.serverUrl;
3648
+ }
3649
+ /**
3650
+ * Creates a Repository instance for a different DID on the same server.
3651
+ *
3652
+ * This allows you to read data from other users' repositories while
3653
+ * maintaining your authenticated session.
3654
+ *
3655
+ * @param did - The DID of the repository to access
3656
+ * @returns A new Repository instance for the specified DID
3657
+ *
3658
+ * @remarks
3659
+ * Write operations on another user's repository will typically fail
3660
+ * unless you have been granted collaborator access (SDS only).
3661
+ *
3662
+ * @example
3663
+ * ```typescript
3664
+ * // Read another user's profile
3665
+ * const otherRepo = repo.repo("did:plc:other-user");
3666
+ * const profile = await otherRepo.profile.get();
3667
+ *
3668
+ * // List their public hypercerts
3669
+ * const hypercerts = await otherRepo.hypercerts.list();
3670
+ * ```
3671
+ */
3672
+ repo(did) {
3673
+ return new Repository(this.session, this.serverUrl, did, this.lexiconRegistry, this._isSDS, this.logger);
3674
+ }
3675
+ /**
3676
+ * Low-level record operations for CRUD on any AT Protocol record type.
3677
+ *
3678
+ * Use this for direct access to AT Protocol records when the high-level
3679
+ * APIs don't meet your needs.
3680
+ *
3681
+ * @returns {@link RecordOperations} interface for record CRUD
3682
+ *
3683
+ * @example
3684
+ * ```typescript
3685
+ * // Create a custom record
3686
+ * const result = await repo.records.create({
3687
+ * collection: "org.example.myRecord",
3688
+ * record: { foo: "bar" },
3689
+ * });
3690
+ *
3691
+ * // List records in a collection
3692
+ * const list = await repo.records.list({
3693
+ * collection: "org.example.myRecord",
3694
+ * limit: 50,
3695
+ * });
3696
+ *
3697
+ * // Get a specific record
3698
+ * const record = await repo.records.get({
3699
+ * collection: "org.example.myRecord",
3700
+ * rkey: "abc123",
3701
+ * });
3702
+ * ```
3703
+ */
3704
+ get records() {
3705
+ if (!this._records) {
3706
+ this._records = new RecordOperationsImpl(this.agent, this.repoDid, this.lexiconRegistry);
3707
+ }
3708
+ return this._records;
3709
+ }
3710
+ /**
3711
+ * Blob operations for uploading and retrieving binary data.
3712
+ *
3713
+ * Blobs are used for images, files, and other binary content associated
3714
+ * with records.
3715
+ *
3716
+ * @returns {@link BlobOperations} interface for blob management
3717
+ *
3718
+ * @example
3719
+ * ```typescript
3720
+ * // Upload an image
3721
+ * const imageBlob = new Blob([imageData], { type: "image/png" });
3722
+ * const uploadResult = await repo.blobs.upload(imageBlob);
3723
+ *
3724
+ * // The ref can be used in records
3725
+ * console.log(uploadResult.ref.$link); // CID of the blob
3726
+ *
3727
+ * // Retrieve a blob by CID
3728
+ * const { data, mimeType } = await repo.blobs.get(cid);
3729
+ * ```
3730
+ */
3731
+ get blobs() {
3732
+ if (!this._blobs) {
3733
+ this._blobs = new BlobOperationsImpl(this.agent, this.repoDid, this.serverUrl);
3734
+ }
3735
+ return this._blobs;
3736
+ }
3737
+ /**
3738
+ * Profile operations for managing user profiles.
3739
+ *
3740
+ * @returns {@link ProfileOperations} interface for profile management
3741
+ *
3742
+ * @example
3743
+ * ```typescript
3744
+ * // Get current profile
3745
+ * const profile = await repo.profile.get();
3746
+ * console.log(profile.displayName);
3747
+ *
3748
+ * // Update profile
3749
+ * await repo.profile.update({
3750
+ * displayName: "New Name",
3751
+ * description: "Updated bio",
3752
+ * avatar: avatarBlob, // Optional: update avatar image
3753
+ * });
3754
+ * ```
3755
+ */
3756
+ get profile() {
3757
+ if (!this._profile) {
3758
+ this._profile = new ProfileOperationsImpl(this.agent, this.repoDid, this.serverUrl);
3759
+ }
3760
+ return this._profile;
3761
+ }
3762
+ /**
3763
+ * High-level hypercert operations.
3764
+ *
3765
+ * Provides a convenient API for creating and managing hypercerts,
3766
+ * including related records like locations, contributions, and evidence.
3767
+ *
3768
+ * @returns {@link HypercertOperations} interface with EventEmitter capabilities
3769
+ *
3770
+ * @example Creating a hypercert
3771
+ * ```typescript
3772
+ * const result = await repo.hypercerts.create({
3773
+ * title: "Climate Action Project",
3774
+ * description: "Reduced carbon emissions by 1000 tons",
3775
+ * workScope: "Climate Action",
3776
+ * workTimeframeFrom: "2024-01-01",
3777
+ * workTimeframeTo: "2024-06-30",
3778
+ * rights: {
3779
+ * name: "Public Domain",
3780
+ * type: "license",
3781
+ * description: "CC0 - No Rights Reserved",
3782
+ * },
3783
+ * image: imageBlob, // Optional cover image
3784
+ * location: {
3785
+ * value: "San Francisco, CA",
3786
+ * name: "SF Bay Area",
3787
+ * },
3788
+ * });
3789
+ * console.log(`Created: ${result.hypercertUri}`);
3790
+ * ```
3791
+ *
3792
+ * @example Listening to events
3793
+ * ```typescript
3794
+ * repo.hypercerts.on("recordCreated", ({ uri, cid }) => {
3795
+ * console.log(`Record created: ${uri}`);
3796
+ * });
3797
+ * ```
3798
+ */
3799
+ get hypercerts() {
3800
+ if (!this._hypercerts) {
3801
+ this._hypercerts = new HypercertOperationsImpl(this.agent, this.repoDid, this.serverUrl, this.lexiconRegistry, this.logger);
3802
+ }
3803
+ return this._hypercerts;
3804
+ }
3805
+ /**
3806
+ * Collaborator operations for managing repository access.
3807
+ *
3808
+ * **SDS Only**: This property throws {@link SDSRequiredError} if accessed
3809
+ * on a PDS repository.
3810
+ *
3811
+ * @returns {@link CollaboratorOperations} interface for access control
3812
+ * @throws {@link SDSRequiredError} if not connected to an SDS server
3813
+ *
3814
+ * @example
3815
+ * ```typescript
3816
+ * // Ensure we're on SDS
3817
+ * const sdsRepo = sdk.repository(session, { server: "sds" });
3818
+ *
3819
+ * // Grant editor access
3820
+ * await sdsRepo.collaborators.grant({
3821
+ * userDid: "did:plc:new-collaborator",
3822
+ * role: "editor",
3823
+ * });
3824
+ *
3825
+ * // List all collaborators
3826
+ * const collaborators = await sdsRepo.collaborators.list();
3827
+ *
3828
+ * // Check access
3829
+ * const hasAccess = await sdsRepo.collaborators.hasAccess("did:plc:someone");
3830
+ *
3831
+ * // Revoke access
3832
+ * await sdsRepo.collaborators.revoke({ userDid: "did:plc:former-collaborator" });
3833
+ * ```
3834
+ */
3835
+ get collaborators() {
3836
+ if (!this._isSDS) {
3837
+ throw new SDSRequiredError("Collaborator operations are only available on SDS servers");
3838
+ }
3839
+ if (!this._collaborators) {
3840
+ this._collaborators = new CollaboratorOperationsImpl(this.session, this.repoDid, this.serverUrl);
3841
+ }
3842
+ return this._collaborators;
3843
+ }
3844
+ /**
3845
+ * Organization operations for creating and managing organizations.
3846
+ *
3847
+ * **SDS Only**: This property throws {@link SDSRequiredError} if accessed
3848
+ * on a PDS repository.
3849
+ *
3850
+ * @returns {@link OrganizationOperations} interface for organization management
3851
+ * @throws {@link SDSRequiredError} if not connected to an SDS server
3852
+ *
3853
+ * @example
3854
+ * ```typescript
3855
+ * // Ensure we're on SDS
3856
+ * const sdsRepo = sdk.repository(session, { server: "sds" });
3857
+ *
3858
+ * // Create an organization
3859
+ * const org = await sdsRepo.organizations.create({
3860
+ * name: "My Organization",
3861
+ * description: "A team working on impact certificates",
3862
+ * handle: "my-org", // Optional custom handle
3863
+ * });
3864
+ *
3865
+ * // List organizations you have access to
3866
+ * const orgs = await sdsRepo.organizations.list();
3867
+ *
3868
+ * // Get specific organization
3869
+ * const orgInfo = await sdsRepo.organizations.get(org.did);
3870
+ * ```
3871
+ */
3872
+ get organizations() {
3873
+ if (!this._isSDS) {
3874
+ throw new SDSRequiredError("Organization operations are only available on SDS servers");
3875
+ }
3876
+ if (!this._organizations) {
3877
+ this._organizations = new OrganizationOperationsImpl(this.session, this.repoDid, this.serverUrl, this.logger);
3878
+ }
3879
+ return this._organizations;
3880
+ }
3881
+ }
3882
+
3883
+ /**
3884
+ * Zod schema for OAuth configuration validation.
3885
+ *
3886
+ * @remarks
3887
+ * All URLs must be valid and use HTTPS in production. The `jwkPrivate` field
3888
+ * should contain the private key in JWK (JSON Web Key) format as a string.
3889
+ */
3890
+ const OAuthConfigSchema = zod.z.object({
3891
+ /**
3892
+ * URL to the OAuth client metadata JSON document.
3893
+ * This document describes your application to the authorization server.
3894
+ *
3895
+ * @see https://atproto.com/specs/oauth#client-metadata
3896
+ */
3897
+ clientId: zod.z.string().url(),
3898
+ /**
3899
+ * URL where users are redirected after authentication.
3900
+ * Must match one of the redirect URIs in your client metadata.
3901
+ */
3902
+ redirectUri: zod.z.string().url(),
3903
+ /**
3904
+ * OAuth scopes to request, space-separated.
3905
+ * Common scopes: "atproto", "transition:generic"
3906
+ */
3907
+ scope: zod.z.string(),
3908
+ /**
3909
+ * URL to your public JWKS (JSON Web Key Set) endpoint.
3910
+ * Used by the authorization server to verify your client's signatures.
3911
+ */
3912
+ jwksUri: zod.z.string().url(),
3913
+ /**
3914
+ * Private JWK (JSON Web Key) as a JSON string.
3915
+ * Used for signing DPoP proofs and client assertions.
3916
+ *
3917
+ * @remarks
3918
+ * This should be kept secret and never exposed to clients.
3919
+ * Typically loaded from environment variables or a secrets manager.
3920
+ */
3921
+ jwkPrivate: zod.z.string(),
3922
+ });
3923
+ /**
3924
+ * Zod schema for server URL configuration.
3925
+ *
3926
+ * @remarks
3927
+ * At least one server (PDS or SDS) should be configured for the SDK to be useful.
3928
+ */
3929
+ const ServerConfigSchema = zod.z.object({
3930
+ /**
3931
+ * Personal Data Server URL - the user's own AT Protocol server.
3932
+ * This is the primary server for user data operations.
3933
+ *
3934
+ * @example "https://bsky.social"
3935
+ */
3936
+ pds: zod.z.string().url().optional(),
3937
+ /**
3938
+ * Shared Data Server URL - for collaborative data storage.
3939
+ * Required for collaborator and organization operations.
3940
+ *
3941
+ * @example "https://sds.hypercerts.org"
3942
+ */
3943
+ sds: zod.z.string().url().optional(),
3944
+ });
3945
+ /**
3946
+ * Zod schema for timeout configuration.
3947
+ *
3948
+ * @remarks
3949
+ * All timeout values are in milliseconds.
3950
+ */
3951
+ const TimeoutConfigSchema = zod.z.object({
3952
+ /**
3953
+ * Timeout for fetching PDS metadata during identity resolution.
3954
+ * @default 5000 (5 seconds, set by OAuthClient)
3955
+ */
3956
+ pdsMetadata: zod.z.number().positive().optional(),
3957
+ /**
3958
+ * Timeout for general API requests to PDS/SDS.
3959
+ * @default 30000 (30 seconds)
3960
+ */
3961
+ apiRequests: zod.z.number().positive().optional(),
3962
+ });
3963
+ /**
3964
+ * Zod schema for SDK configuration validation.
3965
+ *
3966
+ * @remarks
3967
+ * This schema validates only the primitive/serializable parts of the configuration.
3968
+ * Storage interfaces ({@link SessionStore}, {@link StateStore}) cannot be validated
3969
+ * with Zod as they are runtime objects.
3970
+ */
3971
+ const ATProtoSDKConfigSchema = zod.z.object({
3972
+ oauth: OAuthConfigSchema,
3973
+ servers: ServerConfigSchema.optional(),
3974
+ timeouts: TimeoutConfigSchema.optional(),
3975
+ });
3976
+
3977
+ /**
3978
+ * Main ATProto SDK class providing OAuth authentication and repository access.
3979
+ *
3980
+ * This is the primary entry point for interacting with AT Protocol servers.
3981
+ * It handles the OAuth 2.0 flow with DPoP (Demonstrating Proof of Possession)
3982
+ * and provides access to repository operations for managing records, blobs,
3983
+ * and profiles.
3984
+ *
3985
+ * @example Basic usage with OAuth flow
3986
+ * ```typescript
3987
+ * import { ATProtoSDK, InMemorySessionStore, InMemoryStateStore } from "@hypercerts-org/sdk";
3988
+ *
3989
+ * const sdk = new ATProtoSDK({
3990
+ * oauth: {
3991
+ * clientId: "https://my-app.com/client-metadata.json",
3992
+ * redirectUri: "https://my-app.com/callback",
3993
+ * scope: "atproto transition:generic",
3994
+ * jwksUri: "https://my-app.com/.well-known/jwks.json",
3995
+ * jwkPrivate: process.env.JWK_PRIVATE_KEY!,
3996
+ * },
3997
+ * servers: {
3998
+ * pds: "https://bsky.social",
3999
+ * sds: "https://sds.hypercerts.org",
4000
+ * },
4001
+ * });
4002
+ *
4003
+ * // Start OAuth flow - redirect user to this URL
4004
+ * const authUrl = await sdk.authorize("user.bsky.social");
4005
+ *
4006
+ * // After user returns, handle the callback
4007
+ * const session = await sdk.callback(new URLSearchParams(window.location.search));
4008
+ *
4009
+ * // Get a repository to work with data
4010
+ * const repo = sdk.repository(session);
4011
+ * ```
4012
+ *
4013
+ * @example Restoring an existing session
4014
+ * ```typescript
4015
+ * // Restore a previous session by DID
4016
+ * const session = await sdk.restoreSession("did:plc:abc123...");
4017
+ * if (session) {
4018
+ * const repo = sdk.repository(session);
4019
+ * // Continue working with the restored session
4020
+ * }
4021
+ * ```
4022
+ *
4023
+ * @see {@link ATProtoSDKConfig} for configuration options
4024
+ * @see {@link Repository} for data operations
4025
+ * @see {@link OAuthClient} for OAuth implementation details
4026
+ */
4027
+ class ATProtoSDK {
4028
+ /**
4029
+ * Creates a new ATProto SDK instance.
4030
+ *
4031
+ * @param config - SDK configuration including OAuth credentials, server URLs, and optional storage adapters
4032
+ * @throws {@link ValidationError} if the configuration is invalid (e.g., malformed URLs, missing required fields)
4033
+ *
4034
+ * @remarks
4035
+ * If no storage adapters are provided, in-memory implementations are used.
4036
+ * These are suitable for development and testing but **not recommended for production**
4037
+ * as sessions will be lost on restart.
4038
+ *
4039
+ * @example
4040
+ * ```typescript
4041
+ * // Minimal configuration (uses in-memory storage)
4042
+ * const sdk = new ATProtoSDK({
4043
+ * oauth: {
4044
+ * clientId: "https://my-app.com/client-metadata.json",
4045
+ * redirectUri: "https://my-app.com/callback",
4046
+ * scope: "atproto",
4047
+ * jwksUri: "https://my-app.com/.well-known/jwks.json",
4048
+ * jwkPrivate: privateKeyJwk,
4049
+ * },
4050
+ * servers: { pds: "https://bsky.social" },
4051
+ * });
4052
+ * ```
4053
+ */
4054
+ constructor(config) {
4055
+ // Validate configuration
4056
+ const validationResult = ATProtoSDKConfigSchema.safeParse(config);
4057
+ if (!validationResult.success) {
4058
+ throw new ValidationError(`Invalid SDK configuration: ${validationResult.error.message}`, validationResult.error);
4059
+ }
4060
+ // Apply defaults for optional storage
4061
+ const configWithDefaults = {
4062
+ ...config,
4063
+ storage: {
4064
+ sessionStore: config.storage?.sessionStore ?? new InMemorySessionStore(),
4065
+ stateStore: config.storage?.stateStore ?? new InMemoryStateStore(),
4066
+ },
4067
+ };
4068
+ this.config = configWithDefaults;
4069
+ this.logger = config.logger;
4070
+ // Initialize OAuth client
4071
+ this.oauthClient = new OAuthClient(configWithDefaults);
4072
+ // Initialize lexicon registry
4073
+ this.lexiconRegistry = new LexiconRegistry();
4074
+ this.logger?.info("ATProto SDK initialized");
4075
+ }
4076
+ /**
4077
+ * Initiates the OAuth authorization flow.
4078
+ *
4079
+ * This method starts the OAuth 2.0 authorization flow by resolving the user's
4080
+ * identity and generating an authorization URL. The user should be redirected
4081
+ * to this URL to authenticate.
4082
+ *
4083
+ * @param identifier - The user's ATProto identifier. Can be:
4084
+ * - A handle (e.g., `"user.bsky.social"`)
4085
+ * - A DID (e.g., `"did:plc:abc123..."`)
4086
+ * - A PDS URL (e.g., `"https://bsky.social"`)
4087
+ * @param options - Optional authorization settings
4088
+ * @returns A Promise resolving to the authorization URL to redirect the user to
4089
+ * @throws {@link ValidationError} if the identifier is empty or invalid
4090
+ * @throws {@link NetworkError} if the identity cannot be resolved
4091
+ *
4092
+ * @example
4093
+ * ```typescript
4094
+ * // Using a handle
4095
+ * const authUrl = await sdk.authorize("alice.bsky.social");
4096
+ *
4097
+ * // Using a DID directly
4098
+ * const authUrl = await sdk.authorize("did:plc:abc123xyz");
4099
+ *
4100
+ * // With custom scope
4101
+ * const authUrl = await sdk.authorize("alice.bsky.social", {
4102
+ * scope: "atproto transition:generic"
4103
+ * });
4104
+ *
4105
+ * // Redirect user to authUrl
4106
+ * window.location.href = authUrl;
4107
+ * ```
4108
+ */
4109
+ async authorize(identifier, options) {
4110
+ if (!identifier || !identifier.trim()) {
4111
+ throw new ValidationError("ATProto identifier is required");
4112
+ }
4113
+ return this.oauthClient.authorize(identifier.trim(), options);
4114
+ }
4115
+ /**
4116
+ * Handles the OAuth callback and exchanges the authorization code for tokens.
4117
+ *
4118
+ * Call this method when the user is redirected back to your application
4119
+ * after authenticating. It validates the OAuth state, exchanges the
4120
+ * authorization code for access/refresh tokens, and creates a session.
4121
+ *
4122
+ * @param params - URL search parameters from the callback URL
4123
+ * @returns A Promise resolving to the authenticated OAuth session
4124
+ * @throws {@link AuthenticationError} if the callback parameters are invalid or the code exchange fails
4125
+ * @throws {@link ValidationError} if required parameters are missing
4126
+ *
4127
+ * @example
4128
+ * ```typescript
4129
+ * // In your callback route handler
4130
+ * const params = new URLSearchParams(window.location.search);
4131
+ * // params contains: code, state, iss (issuer)
4132
+ *
4133
+ * const session = await sdk.callback(params);
4134
+ * console.log(`Authenticated as ${session.did}`);
4135
+ *
4136
+ * // Store the DID to restore the session later
4137
+ * localStorage.setItem("userDid", session.did);
4138
+ * ```
4139
+ */
4140
+ async callback(params) {
4141
+ return this.oauthClient.callback(params);
4142
+ }
4143
+ /**
4144
+ * Restores an existing OAuth session by DID.
4145
+ *
4146
+ * Use this method to restore a previously authenticated session, typically
4147
+ * on application startup. The method retrieves the stored session and
4148
+ * automatically refreshes expired tokens if needed.
4149
+ *
4150
+ * @param did - The user's Decentralized Identifier (DID), e.g., `"did:plc:abc123..."`
4151
+ * @returns A Promise resolving to the restored session, or `null` if no session exists
4152
+ * @throws {@link ValidationError} if the DID is empty
4153
+ * @throws {@link SessionExpiredError} if the session cannot be refreshed
4154
+ *
4155
+ * @example
4156
+ * ```typescript
4157
+ * // On application startup
4158
+ * const savedDid = localStorage.getItem("userDid");
4159
+ * if (savedDid) {
4160
+ * const session = await sdk.restoreSession(savedDid);
4161
+ * if (session) {
4162
+ * // User is still authenticated
4163
+ * const repo = sdk.repository(session);
4164
+ * } else {
4165
+ * // Session not found, user needs to re-authenticate
4166
+ * const authUrl = await sdk.authorize(savedDid);
4167
+ * }
4168
+ * }
4169
+ * ```
4170
+ */
4171
+ async restoreSession(did) {
4172
+ if (!did || !did.trim()) {
4173
+ throw new ValidationError("DID is required");
4174
+ }
4175
+ return this.oauthClient.restore(did.trim());
4176
+ }
4177
+ /**
4178
+ * Revokes an OAuth session, logging the user out.
4179
+ *
4180
+ * This method invalidates the session's tokens and removes it from storage.
4181
+ * After revocation, the session can no longer be used or restored.
4182
+ *
4183
+ * @param did - The user's DID to revoke the session for
4184
+ * @throws {@link ValidationError} if the DID is empty
4185
+ *
4186
+ * @example
4187
+ * ```typescript
4188
+ * // Log out the user
4189
+ * await sdk.revokeSession(session.did);
4190
+ * localStorage.removeItem("userDid");
4191
+ * ```
4192
+ */
4193
+ async revokeSession(did) {
4194
+ if (!did || !did.trim()) {
4195
+ throw new ValidationError("DID is required");
4196
+ }
4197
+ return this.oauthClient.revoke(did.trim());
4198
+ }
4199
+ /**
4200
+ * Creates a repository instance for data operations.
4201
+ *
4202
+ * The repository provides a fluent API for working with AT Protocol data
4203
+ * including records, blobs, profiles, and domain-specific operations like
4204
+ * hypercerts and collaborators.
4205
+ *
4206
+ * @param session - An authenticated OAuth session
4207
+ * @param options - Repository configuration options
4208
+ * @returns A {@link Repository} instance configured for the specified server
4209
+ * @throws {@link ValidationError} if the session is invalid or server URL is not configured
4210
+ *
4211
+ * @remarks
4212
+ * - **PDS (Personal Data Server)**: User's own data storage, default for most operations
4213
+ * - **SDS (Shared Data Server)**: Shared data storage with collaborator support
4214
+ *
4215
+ * @example Using default PDS
4216
+ * ```typescript
4217
+ * const repo = sdk.repository(session);
4218
+ * const profile = await repo.profile.get();
4219
+ * ```
4220
+ *
4221
+ * @example Using configured SDS
4222
+ * ```typescript
4223
+ * const sdsRepo = sdk.repository(session, { server: "sds" });
4224
+ * const collaborators = await sdsRepo.collaborators.list();
4225
+ * ```
4226
+ *
4227
+ * @example Using custom server URL
4228
+ * ```typescript
4229
+ * const customRepo = sdk.repository(session, {
4230
+ * serverUrl: "https://custom.atproto.server"
4231
+ * });
4232
+ * ```
4233
+ */
4234
+ repository(session, options) {
4235
+ if (!session) {
4236
+ throw new ValidationError("Session is required");
4237
+ }
4238
+ // Determine server URL
4239
+ let serverUrl;
4240
+ let isSDS = false;
4241
+ if (options?.serverUrl) {
4242
+ // Custom URL provided
4243
+ serverUrl = options.serverUrl;
4244
+ // Check if it matches configured SDS
4245
+ isSDS = this.config.servers?.sds === serverUrl;
4246
+ }
4247
+ else if (options?.server === "sds") {
4248
+ // Use configured SDS
4249
+ if (!this.config.servers?.sds) {
4250
+ throw new ValidationError("SDS server URL not configured");
4251
+ }
4252
+ serverUrl = this.config.servers.sds;
4253
+ isSDS = true;
4254
+ }
4255
+ else if (options?.server === "pds" || !options?.server) {
4256
+ // Use configured PDS (default)
4257
+ if (!this.config.servers?.pds) {
4258
+ throw new ValidationError("PDS server URL not configured");
4259
+ }
4260
+ serverUrl = this.config.servers.pds;
4261
+ isSDS = false;
4262
+ }
4263
+ else {
4264
+ // Custom server string (treat as URL)
4265
+ serverUrl = options.server;
4266
+ isSDS = this.config.servers?.sds === serverUrl;
4267
+ }
4268
+ // Get repository DID (default to session DID)
4269
+ const repoDid = session.did || session.sub;
4270
+ return new Repository(session, serverUrl, repoDid, this.lexiconRegistry, isSDS, this.logger);
4271
+ }
4272
+ /**
4273
+ * Gets the lexicon registry for schema validation.
4274
+ *
4275
+ * The lexicon registry manages AT Protocol lexicon schemas used for
4276
+ * validating record data. You can register custom lexicons to extend
4277
+ * the SDK's capabilities.
4278
+ *
4279
+ * @returns The {@link LexiconRegistry} instance
4280
+ *
4281
+ * @example
4282
+ * ```typescript
4283
+ * const registry = sdk.getLexiconRegistry();
4284
+ *
4285
+ * // Register custom lexicons
4286
+ * registry.register(myCustomLexicons);
4287
+ *
4288
+ * // Check if a lexicon is registered
4289
+ * const hasLexicon = registry.has("org.example.myRecord");
4290
+ * ```
4291
+ */
4292
+ getLexiconRegistry() {
4293
+ return this.lexiconRegistry;
4294
+ }
4295
+ /**
4296
+ * The configured PDS (Personal Data Server) URL.
4297
+ *
4298
+ * @returns The PDS URL if configured, otherwise `undefined`
4299
+ */
4300
+ get pdsUrl() {
4301
+ return this.config.servers?.pds;
4302
+ }
4303
+ /**
4304
+ * The configured SDS (Shared Data Server) URL.
4305
+ *
4306
+ * @returns The SDS URL if configured, otherwise `undefined`
4307
+ */
4308
+ get sdsUrl() {
4309
+ return this.config.servers?.sds;
4310
+ }
4311
+ }
4312
+ /**
4313
+ * Factory function to create an ATProto SDK instance.
4314
+ *
4315
+ * This is a convenience function equivalent to `new ATProtoSDK(config)`.
4316
+ *
4317
+ * @param config - SDK configuration
4318
+ * @returns A new {@link ATProtoSDK} instance
4319
+ *
4320
+ * @example
4321
+ * ```typescript
4322
+ * import { createATProtoSDK } from "@hypercerts-org/sdk";
4323
+ *
4324
+ * const sdk = createATProtoSDK({
4325
+ * oauth: { ... },
4326
+ * servers: { pds: "https://bsky.social" },
4327
+ * });
4328
+ * ```
4329
+ */
4330
+ function createATProtoSDK(config) {
4331
+ return new ATProtoSDK(config);
4332
+ }
4333
+
4334
+ /**
4335
+ * Zod schema for collaborator permissions in SDS repositories.
4336
+ *
4337
+ * Defines the granular permissions a collaborator can have on a shared repository.
4338
+ * Permissions follow a hierarchical model where higher-level permissions
4339
+ * typically imply lower-level ones.
4340
+ */
4341
+ const CollaboratorPermissionsSchema = zod.z.object({
4342
+ /**
4343
+ * Can read/view records in the repository.
4344
+ * This is the most basic permission level.
4345
+ */
4346
+ read: zod.z.boolean(),
4347
+ /**
4348
+ * Can create new records in the repository.
4349
+ * Typically implies `read` permission.
4350
+ */
4351
+ create: zod.z.boolean(),
4352
+ /**
4353
+ * Can modify existing records in the repository.
4354
+ * Typically implies `read` and `create` permissions.
4355
+ */
4356
+ update: zod.z.boolean(),
4357
+ /**
4358
+ * Can delete records from the repository.
4359
+ * Typically implies `read`, `create`, and `update` permissions.
4360
+ */
4361
+ delete: zod.z.boolean(),
4362
+ /**
4363
+ * Can manage collaborators and their permissions.
4364
+ * Administrative permission that allows inviting/removing collaborators.
4365
+ */
4366
+ admin: zod.z.boolean(),
4367
+ /**
4368
+ * Full ownership of the repository.
4369
+ * Owners have all permissions and cannot be removed by other admins.
4370
+ * There must always be at least one owner.
4371
+ */
4372
+ owner: zod.z.boolean(),
4373
+ });
4374
+ /**
4375
+ * Zod schema for SDS organization data.
4376
+ *
4377
+ * Organizations are top-level entities in SDS that can own repositories
4378
+ * and have multiple collaborators with different permission levels.
4379
+ */
4380
+ const OrganizationSchema = zod.z.object({
4381
+ /**
4382
+ * The organization's DID - unique identifier.
4383
+ * Format: "did:plc:..." or "did:web:..."
4384
+ */
4385
+ did: zod.z.string(),
4386
+ /**
4387
+ * The organization's handle - human-readable identifier.
4388
+ * Format: "orgname.sds.hypercerts.org" or similar
4389
+ */
4390
+ handle: zod.z.string(),
4391
+ /**
4392
+ * Display name for the organization.
4393
+ */
4394
+ name: zod.z.string(),
4395
+ /**
4396
+ * Optional description of the organization's purpose.
4397
+ */
4398
+ description: zod.z.string().optional(),
4399
+ /**
4400
+ * ISO 8601 timestamp when the organization was created.
4401
+ * Format: "2024-01-15T10:30:00.000Z"
4402
+ */
4403
+ createdAt: zod.z.string(),
4404
+ /**
4405
+ * The current user's permissions within this organization.
4406
+ */
4407
+ permissions: CollaboratorPermissionsSchema,
4408
+ /**
4409
+ * How the current user relates to this organization.
4410
+ * - `"owner"`: User created or owns the organization
4411
+ * - `"collaborator"`: User was invited to collaborate
4412
+ */
4413
+ accessType: zod.z.enum(["owner", "collaborator"]),
4414
+ });
4415
+ /**
4416
+ * Zod schema for collaborator data.
4417
+ *
4418
+ * Represents a user who has been granted access to a shared repository
4419
+ * or organization with specific permissions.
4420
+ */
4421
+ const CollaboratorSchema = zod.z.object({
4422
+ /**
4423
+ * The collaborator's DID - their unique identifier.
4424
+ * Format: "did:plc:..." or "did:web:..."
4425
+ */
4426
+ userDid: zod.z.string(),
4427
+ /**
4428
+ * The permissions granted to this collaborator.
4429
+ */
4430
+ permissions: CollaboratorPermissionsSchema,
4431
+ /**
4432
+ * DID of the user who granted these permissions.
4433
+ * Useful for audit trails.
4434
+ */
4435
+ grantedBy: zod.z.string(),
4436
+ /**
4437
+ * ISO 8601 timestamp when permissions were granted.
4438
+ * Format: "2024-01-15T10:30:00.000Z"
4439
+ */
4440
+ grantedAt: zod.z.string(),
4441
+ /**
4442
+ * ISO 8601 timestamp when permissions were revoked, if applicable.
4443
+ * Undefined if the collaborator is still active.
4444
+ */
4445
+ revokedAt: zod.z.string().optional(),
4446
+ });
4447
+
4448
+ Object.defineProperty(exports, "AppCertifiedLocation", {
4449
+ enumerable: true,
4450
+ get: function () { return lexicon.AppCertifiedLocation; }
4451
+ });
4452
+ Object.defineProperty(exports, "ComAtprotoRepoStrongRef", {
4453
+ enumerable: true,
4454
+ get: function () { return lexicon.ComAtprotoRepoStrongRef; }
4455
+ });
4456
+ Object.defineProperty(exports, "HYPERCERT_COLLECTIONS", {
4457
+ enumerable: true,
4458
+ get: function () { return lexicon.HYPERCERT_COLLECTIONS; }
4459
+ });
4460
+ Object.defineProperty(exports, "HYPERCERT_LEXICONS", {
4461
+ enumerable: true,
4462
+ get: function () { return lexicon.HYPERCERT_LEXICONS; }
4463
+ });
4464
+ Object.defineProperty(exports, "OrgHypercertsClaim", {
4465
+ enumerable: true,
4466
+ get: function () { return lexicon.OrgHypercertsClaim; }
4467
+ });
4468
+ Object.defineProperty(exports, "OrgHypercertsClaimContribution", {
4469
+ enumerable: true,
4470
+ get: function () { return lexicon.OrgHypercertsClaimContribution; }
4471
+ });
4472
+ Object.defineProperty(exports, "OrgHypercertsClaimEvaluation", {
4473
+ enumerable: true,
4474
+ get: function () { return lexicon.OrgHypercertsClaimEvaluation; }
4475
+ });
4476
+ Object.defineProperty(exports, "OrgHypercertsClaimEvidence", {
4477
+ enumerable: true,
4478
+ get: function () { return lexicon.OrgHypercertsClaimEvidence; }
4479
+ });
4480
+ Object.defineProperty(exports, "OrgHypercertsClaimMeasurement", {
4481
+ enumerable: true,
4482
+ get: function () { return lexicon.OrgHypercertsClaimMeasurement; }
4483
+ });
4484
+ Object.defineProperty(exports, "OrgHypercertsClaimRights", {
4485
+ enumerable: true,
4486
+ get: function () { return lexicon.OrgHypercertsClaimRights; }
4487
+ });
4488
+ Object.defineProperty(exports, "OrgHypercertsCollection", {
4489
+ enumerable: true,
4490
+ get: function () { return lexicon.OrgHypercertsCollection; }
4491
+ });
4492
+ Object.defineProperty(exports, "ids", {
4493
+ enumerable: true,
4494
+ get: function () { return lexicon.ids; }
4495
+ });
4496
+ Object.defineProperty(exports, "lexicons", {
4497
+ enumerable: true,
4498
+ get: function () { return lexicon.lexicons; }
4499
+ });
4500
+ Object.defineProperty(exports, "schemaDict", {
4501
+ enumerable: true,
4502
+ get: function () { return lexicon.schemaDict; }
4503
+ });
4504
+ Object.defineProperty(exports, "schemas", {
4505
+ enumerable: true,
4506
+ get: function () { return lexicon.schemas; }
4507
+ });
4508
+ Object.defineProperty(exports, "validate", {
4509
+ enumerable: true,
4510
+ get: function () { return lexicon.validate; }
4511
+ });
4512
+ exports.ATProtoSDK = ATProtoSDK;
4513
+ exports.ATProtoSDKConfigSchema = ATProtoSDKConfigSchema;
4514
+ exports.ATProtoSDKError = ATProtoSDKError;
4515
+ exports.AuthenticationError = AuthenticationError;
4516
+ exports.CollaboratorPermissionsSchema = CollaboratorPermissionsSchema;
4517
+ exports.CollaboratorSchema = CollaboratorSchema;
4518
+ exports.InMemorySessionStore = InMemorySessionStore;
4519
+ exports.InMemoryStateStore = InMemoryStateStore;
4520
+ exports.LexiconRegistry = LexiconRegistry;
4521
+ exports.NetworkError = NetworkError;
4522
+ exports.OAuthConfigSchema = OAuthConfigSchema;
4523
+ exports.OrganizationSchema = OrganizationSchema;
4524
+ exports.Repository = Repository;
4525
+ exports.SDSRequiredError = SDSRequiredError;
4526
+ exports.ServerConfigSchema = ServerConfigSchema;
4527
+ exports.SessionExpiredError = SessionExpiredError;
4528
+ exports.TimeoutConfigSchema = TimeoutConfigSchema;
4529
+ exports.ValidationError = ValidationError;
4530
+ exports.createATProtoSDK = createATProtoSDK;
4531
+ //# sourceMappingURL=index.cjs.map