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