@hypercerts-org/sdk-core 0.4.0-beta.0 → 0.6.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 (62) hide show
  1. package/README.md +459 -79
  2. package/dist/index.cjs +128 -47
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.ts +28 -9
  5. package/dist/index.mjs +128 -47
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/types.cjs +3 -2
  8. package/dist/types.cjs.map +1 -1
  9. package/dist/types.d.ts +28 -9
  10. package/dist/types.mjs +3 -2
  11. package/dist/types.mjs.map +1 -1
  12. package/package.json +9 -5
  13. package/.turbo/turbo-build.log +0 -328
  14. package/.turbo/turbo-test.log +0 -118
  15. package/CHANGELOG.md +0 -22
  16. package/eslint.config.mjs +0 -22
  17. package/rollup.config.js +0 -75
  18. package/src/auth/OAuthClient.ts +0 -497
  19. package/src/core/SDK.ts +0 -410
  20. package/src/core/config.ts +0 -243
  21. package/src/core/errors.ts +0 -257
  22. package/src/core/interfaces.ts +0 -324
  23. package/src/core/types.ts +0 -281
  24. package/src/errors.ts +0 -57
  25. package/src/index.ts +0 -107
  26. package/src/lexicons.ts +0 -64
  27. package/src/repository/BlobOperationsImpl.ts +0 -199
  28. package/src/repository/CollaboratorOperationsImpl.ts +0 -396
  29. package/src/repository/HypercertOperationsImpl.ts +0 -1146
  30. package/src/repository/LexiconRegistry.ts +0 -332
  31. package/src/repository/OrganizationOperationsImpl.ts +0 -234
  32. package/src/repository/ProfileOperationsImpl.ts +0 -281
  33. package/src/repository/RecordOperationsImpl.ts +0 -340
  34. package/src/repository/Repository.ts +0 -482
  35. package/src/repository/interfaces.ts +0 -897
  36. package/src/repository/types.ts +0 -111
  37. package/src/services/hypercerts/types.ts +0 -87
  38. package/src/storage/InMemorySessionStore.ts +0 -127
  39. package/src/storage/InMemoryStateStore.ts +0 -146
  40. package/src/storage.ts +0 -63
  41. package/src/testing/index.ts +0 -67
  42. package/src/testing/mocks.ts +0 -142
  43. package/src/testing/stores.ts +0 -285
  44. package/src/testing.ts +0 -64
  45. package/src/types.ts +0 -86
  46. package/tests/auth/OAuthClient.test.ts +0 -164
  47. package/tests/core/SDK.test.ts +0 -176
  48. package/tests/core/errors.test.ts +0 -81
  49. package/tests/repository/BlobOperationsImpl.test.ts +0 -154
  50. package/tests/repository/CollaboratorOperationsImpl.test.ts +0 -438
  51. package/tests/repository/HypercertOperationsImpl.test.ts +0 -652
  52. package/tests/repository/LexiconRegistry.test.ts +0 -192
  53. package/tests/repository/OrganizationOperationsImpl.test.ts +0 -242
  54. package/tests/repository/ProfileOperationsImpl.test.ts +0 -254
  55. package/tests/repository/RecordOperationsImpl.test.ts +0 -375
  56. package/tests/repository/Repository.test.ts +0 -149
  57. package/tests/utils/fixtures.ts +0 -117
  58. package/tests/utils/mocks.ts +0 -109
  59. package/tests/utils/repository-fixtures.ts +0 -78
  60. package/tsconfig.json +0 -11
  61. package/tsconfig.tsbuildinfo +0 -1
  62. package/vitest.config.ts +0 -30
@@ -1,1146 +0,0 @@
1
- /**
2
- * HypercertOperationsImpl - High-level hypercert operations.
3
- *
4
- * This module provides the implementation for creating and managing
5
- * hypercerts, including related records like rights, locations,
6
- * contributions, measurements, and evaluations.
7
- *
8
- * @packageDocumentation
9
- */
10
-
11
- import type { Agent } from "@atproto/api";
12
- import { EventEmitter } from "eventemitter3";
13
- import { NetworkError, ValidationError } from "../core/errors.js";
14
- import type { LoggerInterface } from "../core/interfaces.js";
15
- import type { LexiconRegistry } from "./LexiconRegistry.js";
16
- import {
17
- HYPERCERT_COLLECTIONS,
18
- type BlobRef,
19
- type HypercertEvidence,
20
- type HypercertClaim,
21
- type HypercertRights,
22
- type HypercertContribution,
23
- type HypercertMeasurement,
24
- type HypercertEvaluation,
25
- type HypercertCollection,
26
- type HypercertLocation,
27
- } from "../services/hypercerts/types.js";
28
- import type {
29
- HypercertOperations,
30
- HypercertEvents,
31
- CreateHypercertParams,
32
- CreateHypercertResult,
33
- } from "./interfaces.js";
34
- import type { CreateResult, UpdateResult, PaginatedList, ListParams, ProgressStep } from "./types.js";
35
-
36
- /**
37
- * Implementation of high-level hypercert operations.
38
- *
39
- * This class provides a convenient API for creating and managing hypercerts
40
- * with automatic handling of:
41
- *
42
- * - Image upload and blob reference management
43
- * - Rights record creation and linking
44
- * - Location attachment with optional GeoJSON support
45
- * - Contribution tracking
46
- * - Measurement and evaluation records
47
- * - Hypercert collections
48
- *
49
- * The class extends EventEmitter to provide real-time progress notifications
50
- * during complex operations.
51
- *
52
- * @remarks
53
- * This class is typically not instantiated directly. Access it through
54
- * {@link Repository.hypercerts}.
55
- *
56
- * **Record Relationships**:
57
- * - Hypercert → Rights (required, 1:1)
58
- * - Hypercert → Location (optional, 1:many)
59
- * - Hypercert → Contribution (optional, 1:many)
60
- * - Hypercert → Measurement (optional, 1:many)
61
- * - Hypercert → Evaluation (optional, 1:many)
62
- * - Collection → Hypercerts (1:many via claims array)
63
- *
64
- * @example Creating a hypercert with progress tracking
65
- * ```typescript
66
- * repo.hypercerts.on("recordCreated", ({ uri }) => {
67
- * console.log(`Hypercert created: ${uri}`);
68
- * });
69
- *
70
- * const result = await repo.hypercerts.create({
71
- * title: "Climate Impact",
72
- * description: "Reduced emissions by 100 tons",
73
- * workScope: "Climate",
74
- * workTimeframeFrom: "2024-01-01",
75
- * workTimeframeTo: "2024-12-31",
76
- * rights: { name: "CC-BY", type: "license", description: "..." },
77
- * onProgress: (step) => console.log(`${step.name}: ${step.status}`),
78
- * });
79
- * ```
80
- *
81
- * @internal
82
- */
83
- export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> implements HypercertOperations {
84
- /**
85
- * Creates a new HypercertOperationsImpl.
86
- *
87
- * @param agent - AT Protocol Agent for making API calls
88
- * @param repoDid - DID of the repository to operate on
89
- * @param _serverUrl - Server URL (reserved for future use)
90
- * @param lexiconRegistry - Registry for record validation
91
- * @param logger - Optional logger for debugging
92
- *
93
- * @internal
94
- */
95
- constructor(
96
- private agent: Agent,
97
- private repoDid: string,
98
- private _serverUrl: string,
99
- private lexiconRegistry: LexiconRegistry,
100
- private logger?: LoggerInterface,
101
- ) {
102
- super();
103
- }
104
-
105
- /**
106
- * Emits a progress event to the optional progress handler.
107
- *
108
- * @param onProgress - Progress callback from create params
109
- * @param step - Progress step information
110
- * @internal
111
- */
112
- private emitProgress(onProgress: ((step: ProgressStep) => void) | undefined, step: ProgressStep): void {
113
- if (onProgress) {
114
- try {
115
- onProgress(step);
116
- } catch (err) {
117
- this.logger?.error(`Error in progress handler: ${err instanceof Error ? err.message : "Unknown"}`);
118
- }
119
- }
120
- }
121
-
122
- /**
123
- * Creates a new hypercert with all related records.
124
- *
125
- * This method orchestrates the creation of a hypercert and its associated
126
- * records in the correct order:
127
- *
128
- * 1. Upload image (if provided)
129
- * 2. Create rights record
130
- * 3. Create hypercert record (referencing rights)
131
- * 4. Attach location (if provided)
132
- * 5. Create contributions (if provided)
133
- *
134
- * @param params - Creation parameters (see {@link CreateHypercertParams})
135
- * @returns Promise resolving to URIs and CIDs of all created records
136
- * @throws {@link ValidationError} if any record fails validation
137
- * @throws {@link NetworkError} if any API call fails
138
- *
139
- * @remarks
140
- * The operation is not atomic - if a later step fails, earlier records
141
- * will still exist. The result object will contain URIs for all
142
- * successfully created records.
143
- *
144
- * **Progress Steps**:
145
- * - `uploadImage`: Image blob upload
146
- * - `createRights`: Rights record creation
147
- * - `createHypercert`: Main hypercert record creation
148
- * - `attachLocation`: Location record creation
149
- * - `createContributions`: Contribution records creation
150
- *
151
- * @example Minimal hypercert
152
- * ```typescript
153
- * const result = await repo.hypercerts.create({
154
- * title: "My Impact",
155
- * description: "Description of impact work",
156
- * workScope: "Education",
157
- * workTimeframeFrom: "2024-01-01",
158
- * workTimeframeTo: "2024-06-30",
159
- * rights: {
160
- * name: "Attribution",
161
- * type: "license",
162
- * description: "CC-BY-4.0",
163
- * },
164
- * });
165
- * ```
166
- *
167
- * @example Full hypercert with all options
168
- * ```typescript
169
- * const result = await repo.hypercerts.create({
170
- * title: "Reforestation Project",
171
- * description: "Planted 10,000 trees...",
172
- * shortDescription: "10K trees planted",
173
- * workScope: "Environment",
174
- * workTimeframeFrom: "2024-01-01",
175
- * workTimeframeTo: "2024-12-31",
176
- * rights: { name: "Open", type: "impact", description: "..." },
177
- * image: coverImageBlob,
178
- * location: { value: "Amazon, Brazil", name: "Amazon Basin" },
179
- * contributions: [
180
- * { contributors: ["did:plc:org1"], role: "coordinator" },
181
- * { contributors: ["did:plc:org2"], role: "implementer" },
182
- * ],
183
- * evidence: [{ uri: "https://...", description: "Satellite data" }],
184
- * onProgress: console.log,
185
- * });
186
- * ```
187
- */
188
- async create(params: CreateHypercertParams): Promise<CreateHypercertResult> {
189
- const createdAt = new Date().toISOString();
190
- const result: CreateHypercertResult = {
191
- hypercertUri: "",
192
- rightsUri: "",
193
- hypercertCid: "",
194
- rightsCid: "",
195
- };
196
-
197
- try {
198
- // Step 1: Upload image if provided
199
- let imageBlobRef: BlobRef | undefined;
200
- if (params.image) {
201
- this.emitProgress(params.onProgress, { name: "uploadImage", status: "start" });
202
- try {
203
- const arrayBuffer = await params.image.arrayBuffer();
204
- const uint8Array = new Uint8Array(arrayBuffer);
205
- const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
206
- encoding: params.image.type || "image/jpeg",
207
- });
208
- if (uploadResult.success) {
209
- imageBlobRef = {
210
- $type: "blob",
211
- ref: uploadResult.data.blob.ref,
212
- mimeType: uploadResult.data.blob.mimeType,
213
- size: uploadResult.data.blob.size,
214
- };
215
- }
216
- this.emitProgress(params.onProgress, {
217
- name: "uploadImage",
218
- status: "success",
219
- data: { size: params.image.size },
220
- });
221
- } catch (error) {
222
- this.emitProgress(params.onProgress, { name: "uploadImage", status: "error", error: error as Error });
223
- throw new NetworkError(
224
- `Failed to upload image: ${error instanceof Error ? error.message : "Unknown"}`,
225
- error,
226
- );
227
- }
228
- }
229
-
230
- // Step 2: Create rights record
231
- this.emitProgress(params.onProgress, { name: "createRights", status: "start" });
232
- const rightsRecord: Omit<HypercertRights, "$type"> = {
233
- rightsName: params.rights.name,
234
- rightsType: params.rights.type,
235
- rightsDescription: params.rights.description,
236
- createdAt,
237
- };
238
-
239
- const rightsValidation = this.lexiconRegistry.validate(HYPERCERT_COLLECTIONS.RIGHTS, rightsRecord);
240
- if (!rightsValidation.valid) {
241
- throw new ValidationError(`Invalid rights record: ${rightsValidation.error}`);
242
- }
243
-
244
- const rightsResult = await this.agent.com.atproto.repo.createRecord({
245
- repo: this.repoDid,
246
- collection: HYPERCERT_COLLECTIONS.RIGHTS,
247
- record: rightsRecord as Record<string, unknown>,
248
- });
249
-
250
- if (!rightsResult.success) {
251
- throw new NetworkError("Failed to create rights record");
252
- }
253
-
254
- result.rightsUri = rightsResult.data.uri;
255
- result.rightsCid = rightsResult.data.cid;
256
- this.emit("rightsCreated", { uri: result.rightsUri, cid: result.rightsCid });
257
- this.emitProgress(params.onProgress, {
258
- name: "createRights",
259
- status: "success",
260
- data: { uri: result.rightsUri },
261
- });
262
-
263
- // Step 3: Create hypercert record
264
- this.emitProgress(params.onProgress, { name: "createHypercert", status: "start" });
265
- const hypercertRecord: Record<string, unknown> = {
266
- title: params.title,
267
- description: params.description,
268
- workScope: params.workScope,
269
- workTimeframeFrom: params.workTimeframeFrom,
270
- workTimeframeTo: params.workTimeframeTo,
271
- rights: { uri: result.rightsUri, cid: result.rightsCid },
272
- createdAt,
273
- };
274
-
275
- if (params.shortDescription) {
276
- hypercertRecord.shortDescription = params.shortDescription;
277
- }
278
-
279
- if (imageBlobRef) {
280
- hypercertRecord.image = imageBlobRef;
281
- }
282
-
283
- if (params.evidence && params.evidence.length > 0) {
284
- hypercertRecord.evidence = params.evidence;
285
- }
286
-
287
- const hypercertValidation = this.lexiconRegistry.validate(HYPERCERT_COLLECTIONS.CLAIM, hypercertRecord);
288
- if (!hypercertValidation.valid) {
289
- throw new ValidationError(`Invalid hypercert record: ${hypercertValidation.error}`);
290
- }
291
-
292
- const hypercertResult = await this.agent.com.atproto.repo.createRecord({
293
- repo: this.repoDid,
294
- collection: HYPERCERT_COLLECTIONS.CLAIM,
295
- record: hypercertRecord,
296
- });
297
-
298
- if (!hypercertResult.success) {
299
- throw new NetworkError("Failed to create hypercert record");
300
- }
301
-
302
- result.hypercertUri = hypercertResult.data.uri;
303
- result.hypercertCid = hypercertResult.data.cid;
304
- this.emit("recordCreated", { uri: result.hypercertUri, cid: result.hypercertCid });
305
- this.emitProgress(params.onProgress, {
306
- name: "createHypercert",
307
- status: "success",
308
- data: { uri: result.hypercertUri },
309
- });
310
-
311
- // Step 4: Attach location if provided
312
- if (params.location) {
313
- this.emitProgress(params.onProgress, { name: "attachLocation", status: "start" });
314
- try {
315
- const locationResult = await this.attachLocation(result.hypercertUri, params.location);
316
- result.locationUri = locationResult.uri;
317
- this.emitProgress(params.onProgress, {
318
- name: "attachLocation",
319
- status: "success",
320
- data: { uri: result.locationUri },
321
- });
322
- } catch (error) {
323
- this.emitProgress(params.onProgress, { name: "attachLocation", status: "error", error: error as Error });
324
- this.logger?.warn(`Failed to attach location: ${error instanceof Error ? error.message : "Unknown"}`);
325
- }
326
- }
327
-
328
- // Step 5: Create contributions if provided
329
- if (params.contributions && params.contributions.length > 0) {
330
- this.emitProgress(params.onProgress, { name: "createContributions", status: "start" });
331
- result.contributionUris = [];
332
- try {
333
- for (const contrib of params.contributions) {
334
- const contribResult = await this.addContribution({
335
- hypercertUri: result.hypercertUri,
336
- contributors: contrib.contributors,
337
- role: contrib.role,
338
- description: contrib.description,
339
- });
340
- result.contributionUris.push(contribResult.uri);
341
- }
342
- this.emitProgress(params.onProgress, {
343
- name: "createContributions",
344
- status: "success",
345
- data: { count: result.contributionUris.length },
346
- });
347
- } catch (error) {
348
- this.emitProgress(params.onProgress, { name: "createContributions", status: "error", error: error as Error });
349
- this.logger?.warn(`Failed to create contributions: ${error instanceof Error ? error.message : "Unknown"}`);
350
- }
351
- }
352
-
353
- return result;
354
- } catch (error) {
355
- if (error instanceof ValidationError || error instanceof NetworkError) throw error;
356
- throw new NetworkError(
357
- `Failed to create hypercert: ${error instanceof Error ? error.message : "Unknown"}`,
358
- error,
359
- );
360
- }
361
- }
362
-
363
- /**
364
- * Updates an existing hypercert record.
365
- *
366
- * @param params - Update parameters
367
- * @param params.uri - AT-URI of the hypercert to update
368
- * @param params.updates - Partial record with fields to update
369
- * @param params.image - New image blob, `null` to remove, `undefined` to keep existing
370
- * @returns Promise resolving to update result
371
- * @throws {@link ValidationError} if the URI format is invalid or record fails validation
372
- * @throws {@link NetworkError} if the update fails
373
- *
374
- * @remarks
375
- * This is a partial update - only specified fields are changed.
376
- * The `createdAt` and `rights` fields cannot be changed.
377
- *
378
- * @example Update title and description
379
- * ```typescript
380
- * await repo.hypercerts.update({
381
- * uri: "at://did:plc:abc/org.hypercerts.hypercert/xyz",
382
- * updates: {
383
- * title: "Updated Title",
384
- * description: "New description",
385
- * },
386
- * });
387
- * ```
388
- *
389
- * @example Update with new image
390
- * ```typescript
391
- * await repo.hypercerts.update({
392
- * uri: hypercertUri,
393
- * updates: { title: "New Title" },
394
- * image: newImageBlob,
395
- * });
396
- * ```
397
- *
398
- * @example Remove image
399
- * ```typescript
400
- * await repo.hypercerts.update({
401
- * uri: hypercertUri,
402
- * updates: {},
403
- * image: null, // Explicitly remove image
404
- * });
405
- * ```
406
- */
407
- async update(params: {
408
- uri: string;
409
- updates: Partial<Omit<HypercertClaim, "$type" | "createdAt" | "rights">>;
410
- image?: Blob | null;
411
- }): Promise<UpdateResult> {
412
- try {
413
- const uriMatch = params.uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
414
- if (!uriMatch) {
415
- throw new ValidationError(`Invalid URI format: ${params.uri}`);
416
- }
417
- const [, , collection, rkey] = uriMatch;
418
-
419
- const existing = await this.agent.com.atproto.repo.getRecord({
420
- repo: this.repoDid,
421
- collection,
422
- rkey,
423
- });
424
-
425
- // The existing record comes from ATProto, use it directly
426
- // TypeScript ensures type safety through the HypercertClaim interface
427
- const existingRecord = existing.data.value as HypercertClaim;
428
-
429
- const recordForUpdate: Record<string, unknown> = {
430
- ...existingRecord,
431
- ...params.updates,
432
- createdAt: existingRecord.createdAt,
433
- rights: existingRecord.rights,
434
- };
435
-
436
- // Handle image update
437
- delete (recordForUpdate as { image?: unknown }).image;
438
- if (params.image !== undefined) {
439
- if (params.image === null) {
440
- // Remove image
441
- } else {
442
- const arrayBuffer = await params.image.arrayBuffer();
443
- const uint8Array = new Uint8Array(arrayBuffer);
444
- const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
445
- encoding: params.image.type || "image/jpeg",
446
- });
447
- if (uploadResult.success) {
448
- recordForUpdate.image = {
449
- $type: "blob",
450
- ref: uploadResult.data.blob.ref,
451
- mimeType: uploadResult.data.blob.mimeType,
452
- size: uploadResult.data.blob.size,
453
- };
454
- }
455
- }
456
- } else if (existingRecord.image) {
457
- // Preserve existing image
458
- recordForUpdate.image = existingRecord.image;
459
- }
460
-
461
- const validation = this.lexiconRegistry.validate(collection, recordForUpdate);
462
- if (!validation.valid) {
463
- throw new ValidationError(`Invalid hypercert record: ${validation.error}`);
464
- }
465
-
466
- const result = await this.agent.com.atproto.repo.putRecord({
467
- repo: this.repoDid,
468
- collection,
469
- rkey,
470
- record: recordForUpdate,
471
- });
472
-
473
- if (!result.success) {
474
- throw new NetworkError("Failed to update hypercert");
475
- }
476
-
477
- this.emit("recordUpdated", { uri: result.data.uri, cid: result.data.cid });
478
- return { uri: result.data.uri, cid: result.data.cid };
479
- } catch (error) {
480
- if (error instanceof ValidationError || error instanceof NetworkError) throw error;
481
- throw new NetworkError(
482
- `Failed to update hypercert: ${error instanceof Error ? error.message : "Unknown"}`,
483
- error,
484
- );
485
- }
486
- }
487
-
488
- /**
489
- * Gets a hypercert by its AT-URI.
490
- *
491
- * @param uri - AT-URI of the hypercert (e.g., "at://did:plc:abc/org.hypercerts.hypercert/xyz")
492
- * @returns Promise resolving to hypercert URI, CID, and parsed record
493
- * @throws {@link ValidationError} if the URI format is invalid or record doesn't match schema
494
- * @throws {@link NetworkError} if the record cannot be fetched
495
- *
496
- * @example
497
- * ```typescript
498
- * const { uri, cid, record } = await repo.hypercerts.get(hypercertUri);
499
- * console.log(`${record.title}: ${record.description}`);
500
- * ```
501
- */
502
- async get(uri: string): Promise<{ uri: string; cid: string; record: HypercertClaim }> {
503
- try {
504
- const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
505
- if (!uriMatch) {
506
- throw new ValidationError(`Invalid URI format: ${uri}`);
507
- }
508
- const [, , collection, rkey] = uriMatch;
509
-
510
- const result = await this.agent.com.atproto.repo.getRecord({
511
- repo: this.repoDid,
512
- collection,
513
- rkey,
514
- });
515
-
516
- if (!result.success) {
517
- throw new NetworkError("Failed to get hypercert");
518
- }
519
-
520
- // Validate with lexicon registry (more lenient - doesn't require $type)
521
- const validation = this.lexiconRegistry.validate(HYPERCERT_COLLECTIONS.CLAIM, result.data.value);
522
- if (!validation.valid) {
523
- throw new ValidationError(`Invalid hypercert record format: ${validation.error}`);
524
- }
525
-
526
- return {
527
- uri: result.data.uri,
528
- cid: result.data.cid ?? "",
529
- record: result.data.value as HypercertClaim,
530
- };
531
- } catch (error) {
532
- if (error instanceof ValidationError || error instanceof NetworkError) throw error;
533
- throw new NetworkError(`Failed to get hypercert: ${error instanceof Error ? error.message : "Unknown"}`, error);
534
- }
535
- }
536
-
537
- /**
538
- * Lists hypercerts in the repository with pagination.
539
- *
540
- * @param params - Optional pagination parameters
541
- * @returns Promise resolving to paginated list of hypercerts
542
- * @throws {@link NetworkError} if the list operation fails
543
- *
544
- * @example
545
- * ```typescript
546
- * // Get first page
547
- * const { records, cursor } = await repo.hypercerts.list({ limit: 20 });
548
- *
549
- * // Get next page
550
- * if (cursor) {
551
- * const nextPage = await repo.hypercerts.list({ limit: 20, cursor });
552
- * }
553
- * ```
554
- */
555
- async list(params?: ListParams): Promise<PaginatedList<{ uri: string; cid: string; record: HypercertClaim }>> {
556
- try {
557
- const result = await this.agent.com.atproto.repo.listRecords({
558
- repo: this.repoDid,
559
- collection: HYPERCERT_COLLECTIONS.CLAIM,
560
- limit: params?.limit,
561
- cursor: params?.cursor,
562
- });
563
-
564
- if (!result.success) {
565
- throw new NetworkError("Failed to list hypercerts");
566
- }
567
-
568
- return {
569
- records:
570
- result.data.records?.map((r) => ({
571
- uri: r.uri,
572
- cid: r.cid,
573
- record: r.value as HypercertClaim,
574
- })) || [],
575
- cursor: result.data.cursor ?? undefined,
576
- };
577
- } catch (error) {
578
- if (error instanceof NetworkError) throw error;
579
- throw new NetworkError(`Failed to list hypercerts: ${error instanceof Error ? error.message : "Unknown"}`, error);
580
- }
581
- }
582
-
583
- /**
584
- * Deletes a hypercert record.
585
- *
586
- * @param uri - AT-URI of the hypercert to delete
587
- * @throws {@link ValidationError} if the URI format is invalid
588
- * @throws {@link NetworkError} if the deletion fails
589
- *
590
- * @remarks
591
- * This only deletes the hypercert record itself. Related records
592
- * (rights, locations, contributions) are not automatically deleted.
593
- *
594
- * @example
595
- * ```typescript
596
- * await repo.hypercerts.delete(hypercertUri);
597
- * ```
598
- */
599
- async delete(uri: string): Promise<void> {
600
- try {
601
- const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
602
- if (!uriMatch) {
603
- throw new ValidationError(`Invalid URI format: ${uri}`);
604
- }
605
- const [, , collection, rkey] = uriMatch;
606
-
607
- const result = await this.agent.com.atproto.repo.deleteRecord({
608
- repo: this.repoDid,
609
- collection,
610
- rkey,
611
- });
612
-
613
- if (!result.success) {
614
- throw new NetworkError("Failed to delete hypercert");
615
- }
616
- } catch (error) {
617
- if (error instanceof ValidationError || error instanceof NetworkError) throw error;
618
- throw new NetworkError(
619
- `Failed to delete hypercert: ${error instanceof Error ? error.message : "Unknown"}`,
620
- error,
621
- );
622
- }
623
- }
624
-
625
- /**
626
- * Attaches a location to an existing hypercert.
627
- *
628
- * @param hypercertUri - AT-URI of the hypercert to attach location to
629
- * @param location - Location data
630
- * @param location.value - Location value (address, coordinates, or description)
631
- * @param location.name - Optional human-readable name
632
- * @param location.description - Optional description
633
- * @param location.srs - Spatial Reference System (e.g., "EPSG:4326")
634
- * @param location.geojson - Optional GeoJSON blob for precise boundaries
635
- * @returns Promise resolving to location record URI and CID
636
- * @throws {@link ValidationError} if validation fails
637
- * @throws {@link NetworkError} if the operation fails
638
- *
639
- * @example Simple location
640
- * ```typescript
641
- * await repo.hypercerts.attachLocation(hypercertUri, {
642
- * value: "San Francisco, CA",
643
- * name: "SF Bay Area",
644
- * });
645
- * ```
646
- *
647
- * @example Location with GeoJSON
648
- * ```typescript
649
- * const geojsonBlob = new Blob([JSON.stringify(geojson)], {
650
- * type: "application/geo+json"
651
- * });
652
- *
653
- * await repo.hypercerts.attachLocation(hypercertUri, {
654
- * value: "Custom Region",
655
- * srs: "EPSG:4326",
656
- * geojson: geojsonBlob,
657
- * });
658
- * ```
659
- */
660
- async attachLocation(
661
- hypercertUri: string,
662
- location: { value: string; name?: string; description?: string; srs?: string; geojson?: Blob },
663
- ): Promise<CreateResult> {
664
- try {
665
- // Get hypercert to get CID
666
- const hypercert = await this.get(hypercertUri);
667
- const createdAt = new Date().toISOString();
668
-
669
- let locationValue: string | BlobRef = location.value;
670
- if (location.geojson) {
671
- const arrayBuffer = await location.geojson.arrayBuffer();
672
- const uint8Array = new Uint8Array(arrayBuffer);
673
- const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
674
- encoding: location.geojson.type || "application/geo+json",
675
- });
676
- if (uploadResult.success) {
677
- locationValue = {
678
- $type: "blob",
679
- ref: uploadResult.data.blob.ref,
680
- mimeType: uploadResult.data.blob.mimeType,
681
- size: uploadResult.data.blob.size,
682
- };
683
- }
684
- }
685
-
686
- const locationRecord: Omit<HypercertLocation, "$type"> = {
687
- hypercert: { uri: hypercert.uri, cid: hypercert.cid },
688
- value: locationValue,
689
- createdAt,
690
- name: location.name,
691
- description: location.description,
692
- srs: location.srs,
693
- };
694
-
695
- const validation = this.lexiconRegistry.validate(HYPERCERT_COLLECTIONS.LOCATION, locationRecord);
696
- if (!validation.valid) {
697
- throw new ValidationError(`Invalid location record: ${validation.error}`);
698
- }
699
-
700
- const result = await this.agent.com.atproto.repo.createRecord({
701
- repo: this.repoDid,
702
- collection: HYPERCERT_COLLECTIONS.LOCATION,
703
- record: locationRecord as Record<string, unknown>,
704
- });
705
-
706
- if (!result.success) {
707
- throw new NetworkError("Failed to attach location");
708
- }
709
-
710
- this.emit("locationAttached", { uri: result.data.uri, cid: result.data.cid, hypercertUri });
711
- return { uri: result.data.uri, cid: result.data.cid };
712
- } catch (error) {
713
- if (error instanceof ValidationError || error instanceof NetworkError) throw error;
714
- throw new NetworkError(`Failed to attach location: ${error instanceof Error ? error.message : "Unknown"}`, error);
715
- }
716
- }
717
-
718
- /**
719
- * Adds evidence to an existing hypercert.
720
- *
721
- * @param hypercertUri - AT-URI of the hypercert
722
- * @param evidence - Array of evidence items to add
723
- * @returns Promise resolving to update result
724
- * @throws {@link ValidationError} if validation fails
725
- * @throws {@link NetworkError} if the operation fails
726
- *
727
- * @remarks
728
- * Evidence is appended to existing evidence, not replaced.
729
- *
730
- * @example
731
- * ```typescript
732
- * await repo.hypercerts.addEvidence(hypercertUri, [
733
- * { uri: "https://example.com/report.pdf", description: "Impact report" },
734
- * { uri: "https://example.com/data.csv", description: "Raw data" },
735
- * ]);
736
- * ```
737
- */
738
- async addEvidence(hypercertUri: string, evidence: HypercertEvidence[]): Promise<UpdateResult> {
739
- try {
740
- const existing = await this.get(hypercertUri);
741
- const existingEvidence = existing.record.evidence || [];
742
- const updatedEvidence = [...existingEvidence, ...evidence];
743
-
744
- const result = await this.update({
745
- uri: hypercertUri,
746
- updates: { evidence: updatedEvidence },
747
- });
748
-
749
- this.emit("evidenceAdded", { uri: result.uri, cid: result.cid });
750
- return result;
751
- } catch (error) {
752
- if (error instanceof ValidationError || error instanceof NetworkError) throw error;
753
- throw new NetworkError(`Failed to add evidence: ${error instanceof Error ? error.message : "Unknown"}`, error);
754
- }
755
- }
756
-
757
- /**
758
- * Creates a contribution record.
759
- *
760
- * @param params - Contribution parameters
761
- * @param params.hypercertUri - Optional hypercert to link (can be standalone)
762
- * @param params.contributors - Array of contributor DIDs
763
- * @param params.role - Role of the contributors (e.g., "coordinator", "implementer")
764
- * @param params.description - Optional description of the contribution
765
- * @returns Promise resolving to contribution record URI and CID
766
- * @throws {@link ValidationError} if validation fails
767
- * @throws {@link NetworkError} if the operation fails
768
- *
769
- * @example
770
- * ```typescript
771
- * await repo.hypercerts.addContribution({
772
- * hypercertUri: hypercertUri,
773
- * contributors: ["did:plc:alice", "did:plc:bob"],
774
- * role: "implementer",
775
- * description: "On-ground implementation team",
776
- * });
777
- * ```
778
- */
779
- async addContribution(params: {
780
- hypercertUri?: string;
781
- contributors: string[];
782
- role: string;
783
- description?: string;
784
- }): Promise<CreateResult> {
785
- try {
786
- const createdAt = new Date().toISOString();
787
- const contributionRecord: Omit<HypercertContribution, "$type"> = {
788
- contributors: params.contributors,
789
- role: params.role,
790
- createdAt,
791
- description: params.description,
792
- };
793
-
794
- if (params.hypercertUri) {
795
- const hypercert = await this.get(params.hypercertUri);
796
- contributionRecord.hypercert = { uri: hypercert.uri, cid: hypercert.cid };
797
- }
798
-
799
- const validation = this.lexiconRegistry.validate(HYPERCERT_COLLECTIONS.CONTRIBUTION, contributionRecord);
800
- if (!validation.valid) {
801
- throw new ValidationError(`Invalid contribution record: ${validation.error}`);
802
- }
803
-
804
- const result = await this.agent.com.atproto.repo.createRecord({
805
- repo: this.repoDid,
806
- collection: HYPERCERT_COLLECTIONS.CONTRIBUTION,
807
- record: contributionRecord as Record<string, unknown>,
808
- });
809
-
810
- if (!result.success) {
811
- throw new NetworkError("Failed to create contribution");
812
- }
813
-
814
- this.emit("contributionCreated", { uri: result.data.uri, cid: result.data.cid });
815
- return { uri: result.data.uri, cid: result.data.cid };
816
- } catch (error) {
817
- if (error instanceof ValidationError || error instanceof NetworkError) throw error;
818
- throw new NetworkError(
819
- `Failed to add contribution: ${error instanceof Error ? error.message : "Unknown"}`,
820
- error,
821
- );
822
- }
823
- }
824
-
825
- /**
826
- * Creates a measurement record for a hypercert.
827
- *
828
- * Measurements quantify the impact claimed in a hypercert with
829
- * specific metrics and values.
830
- *
831
- * @param params - Measurement parameters
832
- * @param params.hypercertUri - AT-URI of the hypercert being measured
833
- * @param params.measurers - DIDs of entities who performed the measurement
834
- * @param params.metric - Name of the metric (e.g., "CO2 Reduced", "Trees Planted")
835
- * @param params.value - Measured value with units (e.g., "100 tons", "10000")
836
- * @param params.methodUri - Optional URI describing the measurement methodology
837
- * @param params.evidenceUris - Optional URIs to supporting evidence
838
- * @returns Promise resolving to measurement record URI and CID
839
- * @throws {@link ValidationError} if validation fails
840
- * @throws {@link NetworkError} if the operation fails
841
- *
842
- * @example
843
- * ```typescript
844
- * await repo.hypercerts.addMeasurement({
845
- * hypercertUri: hypercertUri,
846
- * measurers: ["did:plc:auditor"],
847
- * metric: "Carbon Offset",
848
- * value: "150 tons CO2e",
849
- * methodUri: "https://example.com/methodology",
850
- * evidenceUris: ["https://example.com/audit-report"],
851
- * });
852
- * ```
853
- */
854
- async addMeasurement(params: {
855
- hypercertUri: string;
856
- measurers: string[];
857
- metric: string;
858
- value: string;
859
- methodUri?: string;
860
- evidenceUris?: string[];
861
- }): Promise<CreateResult> {
862
- try {
863
- const hypercert = await this.get(params.hypercertUri);
864
- const createdAt = new Date().toISOString();
865
-
866
- const measurementRecord: Omit<HypercertMeasurement, "$type"> = {
867
- hypercert: { uri: hypercert.uri, cid: hypercert.cid },
868
- measurers: params.measurers,
869
- metric: params.metric,
870
- value: params.value,
871
- createdAt,
872
- measurementMethodURI: params.methodUri,
873
- evidenceURI: params.evidenceUris,
874
- };
875
-
876
- const validation = this.lexiconRegistry.validate(HYPERCERT_COLLECTIONS.MEASUREMENT, measurementRecord);
877
- if (!validation.valid) {
878
- throw new ValidationError(`Invalid measurement record: ${validation.error}`);
879
- }
880
-
881
- const result = await this.agent.com.atproto.repo.createRecord({
882
- repo: this.repoDid,
883
- collection: HYPERCERT_COLLECTIONS.MEASUREMENT,
884
- record: measurementRecord as Record<string, unknown>,
885
- });
886
-
887
- if (!result.success) {
888
- throw new NetworkError("Failed to create measurement");
889
- }
890
-
891
- return { uri: result.data.uri, cid: result.data.cid };
892
- } catch (error) {
893
- if (error instanceof ValidationError || error instanceof NetworkError) throw error;
894
- throw new NetworkError(`Failed to add measurement: ${error instanceof Error ? error.message : "Unknown"}`, error);
895
- }
896
- }
897
-
898
- /**
899
- * Creates an evaluation record for a hypercert or other subject.
900
- *
901
- * Evaluations provide third-party assessments of impact claims.
902
- *
903
- * @param params - Evaluation parameters
904
- * @param params.subjectUri - AT-URI of the record being evaluated
905
- * @param params.evaluators - DIDs of evaluating entities
906
- * @param params.summary - Summary of the evaluation findings
907
- * @returns Promise resolving to evaluation record URI and CID
908
- * @throws {@link ValidationError} if validation fails
909
- * @throws {@link NetworkError} if the operation fails
910
- *
911
- * @example
912
- * ```typescript
913
- * await repo.hypercerts.addEvaluation({
914
- * subjectUri: hypercertUri,
915
- * evaluators: ["did:plc:evaluator-org"],
916
- * summary: "Verified impact claims through site visit and data analysis",
917
- * });
918
- * ```
919
- */
920
- async addEvaluation(params: { subjectUri: string; evaluators: string[]; summary: string }): Promise<CreateResult> {
921
- try {
922
- const subject = await this.get(params.subjectUri);
923
- const createdAt = new Date().toISOString();
924
-
925
- const evaluationRecord: Omit<HypercertEvaluation, "$type"> = {
926
- subject: { uri: subject.uri, cid: subject.cid },
927
- evaluators: params.evaluators,
928
- summary: params.summary,
929
- createdAt,
930
- };
931
-
932
- const validation = this.lexiconRegistry.validate(HYPERCERT_COLLECTIONS.EVALUATION, evaluationRecord);
933
- if (!validation.valid) {
934
- throw new ValidationError(`Invalid evaluation record: ${validation.error}`);
935
- }
936
-
937
- const result = await this.agent.com.atproto.repo.createRecord({
938
- repo: this.repoDid,
939
- collection: HYPERCERT_COLLECTIONS.EVALUATION,
940
- record: evaluationRecord as Record<string, unknown>,
941
- });
942
-
943
- if (!result.success) {
944
- throw new NetworkError("Failed to create evaluation");
945
- }
946
-
947
- return { uri: result.data.uri, cid: result.data.cid };
948
- } catch (error) {
949
- if (error instanceof ValidationError || error instanceof NetworkError) throw error;
950
- throw new NetworkError(`Failed to add evaluation: ${error instanceof Error ? error.message : "Unknown"}`, error);
951
- }
952
- }
953
-
954
- /**
955
- * Creates a collection of hypercerts.
956
- *
957
- * Collections group related hypercerts with optional weights
958
- * for relative importance.
959
- *
960
- * @param params - Collection parameters
961
- * @param params.title - Collection title
962
- * @param params.claims - Array of hypercert references with weights
963
- * @param params.shortDescription - Optional short description
964
- * @param params.coverPhoto - Optional cover image blob
965
- * @returns Promise resolving to collection record URI and CID
966
- * @throws {@link ValidationError} if validation fails
967
- * @throws {@link NetworkError} if the operation fails
968
- *
969
- * @example
970
- * ```typescript
971
- * const collection = await repo.hypercerts.createCollection({
972
- * title: "Climate Projects 2024",
973
- * shortDescription: "Our climate impact portfolio",
974
- * claims: [
975
- * { uri: hypercert1Uri, cid: hypercert1Cid, weight: "0.5" },
976
- * { uri: hypercert2Uri, cid: hypercert2Cid, weight: "0.3" },
977
- * { uri: hypercert3Uri, cid: hypercert3Cid, weight: "0.2" },
978
- * ],
979
- * coverPhoto: coverImageBlob,
980
- * });
981
- * ```
982
- */
983
- async createCollection(params: {
984
- title: string;
985
- claims: Array<{ uri: string; cid: string; weight: string }>;
986
- shortDescription?: string;
987
- coverPhoto?: Blob;
988
- }): Promise<CreateResult> {
989
- try {
990
- const createdAt = new Date().toISOString();
991
-
992
- let coverPhotoRef: BlobRef | undefined;
993
- if (params.coverPhoto) {
994
- const arrayBuffer = await params.coverPhoto.arrayBuffer();
995
- const uint8Array = new Uint8Array(arrayBuffer);
996
- const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
997
- encoding: params.coverPhoto.type || "image/jpeg",
998
- });
999
- if (uploadResult.success) {
1000
- coverPhotoRef = {
1001
- $type: "blob",
1002
- ref: uploadResult.data.blob.ref,
1003
- mimeType: uploadResult.data.blob.mimeType,
1004
- size: uploadResult.data.blob.size,
1005
- };
1006
- }
1007
- }
1008
-
1009
- const collectionRecord: Record<string, unknown> = {
1010
- title: params.title,
1011
- claims: params.claims.map((c) => ({ claim: { uri: c.uri, cid: c.cid }, weight: c.weight })),
1012
- createdAt,
1013
- };
1014
-
1015
- if (params.shortDescription) {
1016
- collectionRecord.shortDescription = params.shortDescription;
1017
- }
1018
-
1019
- if (coverPhotoRef) {
1020
- collectionRecord.coverPhoto = coverPhotoRef;
1021
- }
1022
-
1023
- const validation = this.lexiconRegistry.validate(HYPERCERT_COLLECTIONS.COLLECTION, collectionRecord);
1024
- if (!validation.valid) {
1025
- throw new ValidationError(`Invalid collection record: ${validation.error}`);
1026
- }
1027
-
1028
- const result = await this.agent.com.atproto.repo.createRecord({
1029
- repo: this.repoDid,
1030
- collection: HYPERCERT_COLLECTIONS.COLLECTION,
1031
- record: collectionRecord,
1032
- });
1033
-
1034
- if (!result.success) {
1035
- throw new NetworkError("Failed to create collection");
1036
- }
1037
-
1038
- this.emit("collectionCreated", { uri: result.data.uri, cid: result.data.cid });
1039
- return { uri: result.data.uri, cid: result.data.cid };
1040
- } catch (error) {
1041
- if (error instanceof ValidationError || error instanceof NetworkError) throw error;
1042
- throw new NetworkError(
1043
- `Failed to create collection: ${error instanceof Error ? error.message : "Unknown"}`,
1044
- error,
1045
- );
1046
- }
1047
- }
1048
-
1049
- /**
1050
- * Gets a collection by its AT-URI.
1051
- *
1052
- * @param uri - AT-URI of the collection
1053
- * @returns Promise resolving to collection URI, CID, and parsed record
1054
- * @throws {@link ValidationError} if the URI format is invalid or record doesn't match schema
1055
- * @throws {@link NetworkError} if the record cannot be fetched
1056
- *
1057
- * @example
1058
- * ```typescript
1059
- * const { record } = await repo.hypercerts.getCollection(collectionUri);
1060
- * console.log(`Collection: ${record.title}`);
1061
- * console.log(`Contains ${record.claims.length} hypercerts`);
1062
- * ```
1063
- */
1064
- async getCollection(uri: string): Promise<{ uri: string; cid: string; record: HypercertCollection }> {
1065
- try {
1066
- const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
1067
- if (!uriMatch) {
1068
- throw new ValidationError(`Invalid URI format: ${uri}`);
1069
- }
1070
- const [, , collection, rkey] = uriMatch;
1071
-
1072
- const result = await this.agent.com.atproto.repo.getRecord({
1073
- repo: this.repoDid,
1074
- collection,
1075
- rkey,
1076
- });
1077
-
1078
- if (!result.success) {
1079
- throw new NetworkError("Failed to get collection");
1080
- }
1081
-
1082
- // Validate with lexicon registry (more lenient - doesn't require $type)
1083
- const validation = this.lexiconRegistry.validate(HYPERCERT_COLLECTIONS.COLLECTION, result.data.value);
1084
- if (!validation.valid) {
1085
- throw new ValidationError(`Invalid collection record format: ${validation.error}`);
1086
- }
1087
-
1088
- return {
1089
- uri: result.data.uri,
1090
- cid: result.data.cid ?? "",
1091
- record: result.data.value as HypercertCollection,
1092
- };
1093
- } catch (error) {
1094
- if (error instanceof ValidationError || error instanceof NetworkError) throw error;
1095
- throw new NetworkError(`Failed to get collection: ${error instanceof Error ? error.message : "Unknown"}`, error);
1096
- }
1097
- }
1098
-
1099
- /**
1100
- * Lists collections in the repository with pagination.
1101
- *
1102
- * @param params - Optional pagination parameters
1103
- * @returns Promise resolving to paginated list of collections
1104
- * @throws {@link NetworkError} if the list operation fails
1105
- *
1106
- * @example
1107
- * ```typescript
1108
- * const { records } = await repo.hypercerts.listCollections();
1109
- * for (const { record } of records) {
1110
- * console.log(`${record.title}: ${record.claims.length} claims`);
1111
- * }
1112
- * ```
1113
- */
1114
- async listCollections(
1115
- params?: ListParams,
1116
- ): Promise<PaginatedList<{ uri: string; cid: string; record: HypercertCollection }>> {
1117
- try {
1118
- const result = await this.agent.com.atproto.repo.listRecords({
1119
- repo: this.repoDid,
1120
- collection: HYPERCERT_COLLECTIONS.COLLECTION,
1121
- limit: params?.limit,
1122
- cursor: params?.cursor,
1123
- });
1124
-
1125
- if (!result.success) {
1126
- throw new NetworkError("Failed to list collections");
1127
- }
1128
-
1129
- return {
1130
- records:
1131
- result.data.records?.map((r) => ({
1132
- uri: r.uri,
1133
- cid: r.cid,
1134
- record: r.value as HypercertCollection,
1135
- })) || [],
1136
- cursor: result.data.cursor ?? undefined,
1137
- };
1138
- } catch (error) {
1139
- if (error instanceof NetworkError) throw error;
1140
- throw new NetworkError(
1141
- `Failed to list collections: ${error instanceof Error ? error.message : "Unknown"}`,
1142
- error,
1143
- );
1144
- }
1145
- }
1146
- }