@hypercerts-org/sdk-core 0.10.0-beta.5 → 0.10.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/README.md +178 -39
- package/dist/index.cjs +393 -268
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +197 -149
- package/dist/index.mjs +393 -268
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.ts +197 -148
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -4198,47 +4198,25 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4198
4198
|
*/
|
|
4199
4199
|
async attachLocation(hypercertUri, location) {
|
|
4200
4200
|
try {
|
|
4201
|
-
if (!location.srs) {
|
|
4202
|
-
throw new ValidationError("srs (Spatial Reference System) is required. Example: 'EPSG:4326' for WGS84 coordinates, or 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' for CRS84.");
|
|
4203
|
-
}
|
|
4204
4201
|
// Validate that hypercert exists (unused but confirms hypercert is valid)
|
|
4205
4202
|
await this.get(hypercertUri);
|
|
4206
|
-
const
|
|
4207
|
-
const locationData = await this.resolveUriOrBlob(location.location, "application/geo+json");
|
|
4208
|
-
const locationRecord = {
|
|
4209
|
-
$type: HYPERCERT_COLLECTIONS.LOCATION,
|
|
4210
|
-
lpVersion: location.lpVersion || "1.0",
|
|
4211
|
-
srs: location.srs,
|
|
4212
|
-
locationType: location.locationType,
|
|
4213
|
-
location: locationData,
|
|
4214
|
-
createdAt,
|
|
4215
|
-
name: location.name,
|
|
4216
|
-
description: location.description,
|
|
4217
|
-
};
|
|
4218
|
-
const validation = lexicon.validate(locationRecord, HYPERCERT_COLLECTIONS.LOCATION, "main", false);
|
|
4219
|
-
if (!validation.success) {
|
|
4220
|
-
throw new ValidationError(`Invalid location record: ${validation.error?.message}`);
|
|
4221
|
-
}
|
|
4222
|
-
const result = await this.agent.com.atproto.repo.createRecord({
|
|
4223
|
-
repo: this.repoDid,
|
|
4224
|
-
collection: HYPERCERT_COLLECTIONS.LOCATION,
|
|
4225
|
-
record: locationRecord,
|
|
4226
|
-
});
|
|
4227
|
-
if (!result.success) {
|
|
4228
|
-
throw new NetworkError("Failed to attach location");
|
|
4229
|
-
}
|
|
4203
|
+
const resolvedLocation = await this.resolveLocation(location);
|
|
4230
4204
|
await this.update({
|
|
4231
4205
|
uri: hypercertUri,
|
|
4232
4206
|
updates: {
|
|
4233
4207
|
location: {
|
|
4234
4208
|
$type: "com.atproto.repo.strongRef",
|
|
4235
|
-
uri:
|
|
4236
|
-
cid:
|
|
4209
|
+
uri: resolvedLocation.uri,
|
|
4210
|
+
cid: resolvedLocation.cid,
|
|
4237
4211
|
},
|
|
4238
4212
|
},
|
|
4239
4213
|
});
|
|
4240
|
-
this.emit("locationAttached", {
|
|
4241
|
-
|
|
4214
|
+
this.emit("locationAttached", {
|
|
4215
|
+
uri: resolvedLocation.uri,
|
|
4216
|
+
cid: resolvedLocation.cid,
|
|
4217
|
+
hypercertUri,
|
|
4218
|
+
});
|
|
4219
|
+
return { uri: resolvedLocation.uri, cid: resolvedLocation.cid };
|
|
4242
4220
|
}
|
|
4243
4221
|
catch (error) {
|
|
4244
4222
|
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
@@ -4251,23 +4229,86 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4251
4229
|
*
|
|
4252
4230
|
* @param content - Either a URI string or a Blob to upload
|
|
4253
4231
|
* @param fallbackMimeType - MIME type to use if Blob.type is empty
|
|
4254
|
-
* @returns Promise resolving to either a URI ref or blob ref
|
|
4232
|
+
* @returns Promise resolving to either a URI ref or blob ref
|
|
4255
4233
|
* @internal
|
|
4256
4234
|
*/
|
|
4257
4235
|
async resolveUriOrBlob(content, fallbackMimeType) {
|
|
4258
4236
|
if (typeof content === "string") {
|
|
4259
|
-
|
|
4237
|
+
const uriRef = {
|
|
4260
4238
|
$type: "org.hypercerts.defs#uri",
|
|
4261
4239
|
uri: content,
|
|
4262
4240
|
};
|
|
4241
|
+
return uriRef;
|
|
4263
4242
|
}
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4243
|
+
const uploadedBlob = await this.handleBlobUpload(content, fallbackMimeType);
|
|
4244
|
+
const blobRef = {
|
|
4245
|
+
$type: "org.hypercerts.defs#smallBlob",
|
|
4246
|
+
blob: uploadedBlob,
|
|
4247
|
+
};
|
|
4248
|
+
return blobRef;
|
|
4249
|
+
}
|
|
4250
|
+
async resolveCollectionImageInput(input, isBanner = false) {
|
|
4251
|
+
if (typeof input === "string") {
|
|
4252
|
+
return { $type: "org.hypercerts.defs#uri", uri: input };
|
|
4253
|
+
}
|
|
4254
|
+
const blob = await this.handleBlobUpload(input, "image/jpeg");
|
|
4255
|
+
if (isBanner) {
|
|
4256
|
+
return { $type: "org.hypercerts.defs#largeImage", image: blob };
|
|
4257
|
+
}
|
|
4258
|
+
return { $type: "org.hypercerts.defs#smallImage", image: blob };
|
|
4259
|
+
}
|
|
4260
|
+
async resolveLocationValue(location) {
|
|
4261
|
+
if (typeof location === "string" || location instanceof Blob) {
|
|
4262
|
+
return this.resolveUriOrBlob(location, "application/geo+json");
|
|
4263
|
+
}
|
|
4264
|
+
return location;
|
|
4265
|
+
}
|
|
4266
|
+
/**
|
|
4267
|
+
* Check if an AttachLocationParams is the object form (not a StrongRef or string).
|
|
4268
|
+
* @internal
|
|
4269
|
+
*/
|
|
4270
|
+
isLocationObject(location) {
|
|
4271
|
+
return (typeof location === "object" &&
|
|
4272
|
+
!("uri" in location) &&
|
|
4273
|
+
!("cid" in location) &&
|
|
4274
|
+
location !== null &&
|
|
4275
|
+
!Array.isArray(location));
|
|
4276
|
+
}
|
|
4277
|
+
/**
|
|
4278
|
+
* Helper to resolve a location reference to a StrongRef.
|
|
4279
|
+
*
|
|
4280
|
+
* @param location - Location parameter (StrongRef, string URI, or location object)
|
|
4281
|
+
* @returns Promise resolving to a StrongRef
|
|
4282
|
+
* @throws {ValidationError} When string input doesn't match AT-URI pattern
|
|
4283
|
+
* @throws {NetworkError} When getRecord fails or returns no CID
|
|
4284
|
+
* @internal
|
|
4285
|
+
*/
|
|
4286
|
+
async resolveLocation(location) {
|
|
4287
|
+
if (typeof location === "string") {
|
|
4288
|
+
return this.resolveStrongRefFromUri(location);
|
|
4289
|
+
}
|
|
4290
|
+
if (this.isLocationObject(location)) {
|
|
4291
|
+
return this.createLocationRecord(location);
|
|
4270
4292
|
}
|
|
4293
|
+
if ("uri" in location && "cid" in location) {
|
|
4294
|
+
return { $type: "com.atproto.repo.strongRef", uri: location.uri, cid: location.cid };
|
|
4295
|
+
}
|
|
4296
|
+
throw new ValidationError("resolveLocation: Unsupported location input.");
|
|
4297
|
+
}
|
|
4298
|
+
async resolveStrongRefFromUri(uri) {
|
|
4299
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
4300
|
+
if (!uriMatch) {
|
|
4301
|
+
throw new ValidationError(`resolveLocation: Invalid location AT-URI: "${uri}"`);
|
|
4302
|
+
}
|
|
4303
|
+
const [, repo, collection, rkey] = uriMatch;
|
|
4304
|
+
const record = await this.agent.com.atproto.repo.getRecord({ repo, collection, rkey });
|
|
4305
|
+
if (!record.success) {
|
|
4306
|
+
throw new NetworkError(`resolveLocation: getRecord failed for repo=${repo}, collection=${collection}, rkey=${rkey}`);
|
|
4307
|
+
}
|
|
4308
|
+
if (!record.data.cid) {
|
|
4309
|
+
throw new NetworkError(`resolveLocation: getRecord returned no CID for repo=${repo}, collection=${collection}, rkey=${rkey}`);
|
|
4310
|
+
}
|
|
4311
|
+
return { $type: "com.atproto.repo.strongRef", uri, cid: record.data.cid };
|
|
4271
4312
|
}
|
|
4272
4313
|
/**
|
|
4273
4314
|
* Adds evidence to any subject via the subject ref.
|
|
@@ -4506,7 +4547,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4506
4547
|
*
|
|
4507
4548
|
* @param params - Collection parameters
|
|
4508
4549
|
* @param params.title - Collection title
|
|
4509
|
-
* @param params.
|
|
4550
|
+
* @param params.items - Array of hypercert references with weights
|
|
4510
4551
|
* @param params.shortDescription - Optional short description
|
|
4511
4552
|
* @param params.banner - Optional cover image blob
|
|
4512
4553
|
* @returns Promise resolving to collection record URI and CID
|
|
@@ -4518,66 +4559,64 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4518
4559
|
* const collection = await repo.hypercerts.createCollection({
|
|
4519
4560
|
* title: "Climate Projects 2024",
|
|
4520
4561
|
* shortDescription: "Our climate impact portfolio",
|
|
4521
|
-
*
|
|
4522
|
-
* { uri: hypercert1Uri, cid: hypercert1Cid,
|
|
4523
|
-
* { uri: hypercert2Uri, cid: hypercert2Cid,
|
|
4524
|
-
* { uri: hypercert3Uri, cid: hypercert3Cid,
|
|
4562
|
+
* items: [
|
|
4563
|
+
* { itemIdentifier: { uri: hypercert1Uri, cid: hypercert1Cid }, itemWeight: "0.5" },
|
|
4564
|
+
* { itemIdentifier: { uri: hypercert2Uri, cid: hypercert2Cid }, itemWeight: "0.3" },
|
|
4565
|
+
* { itemIdentifier: { uri: hypercert3Uri, cid: hypercert3Cid }, itemWeight: "0.2" },
|
|
4525
4566
|
* ],
|
|
4526
4567
|
* banner: coverImageBlob,
|
|
4527
4568
|
* });
|
|
4528
4569
|
* ```
|
|
4529
4570
|
*/
|
|
4530
4571
|
async createCollection(params) {
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
if (uploadResult.success) {
|
|
4541
|
-
bannerRef = {
|
|
4542
|
-
$type: "blob",
|
|
4543
|
-
ref: { $link: uploadResult.data.blob.ref.toString() },
|
|
4544
|
-
mimeType: uploadResult.data.blob.mimeType,
|
|
4545
|
-
size: uploadResult.data.blob.size,
|
|
4546
|
-
};
|
|
4547
|
-
}
|
|
4548
|
-
}
|
|
4549
|
-
const collectionRecord = {
|
|
4550
|
-
$type: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
4551
|
-
title: params.title,
|
|
4552
|
-
claims: params.claims.map((c) => ({ claim: { uri: c.uri, cid: c.cid }, weight: c.weight })),
|
|
4553
|
-
createdAt,
|
|
4554
|
-
};
|
|
4555
|
-
if (params.shortDescription) {
|
|
4556
|
-
collectionRecord.shortDescription = params.shortDescription;
|
|
4557
|
-
}
|
|
4558
|
-
if (bannerRef) {
|
|
4559
|
-
collectionRecord.banner = bannerRef;
|
|
4560
|
-
}
|
|
4561
|
-
const validation = lexicon.validate(collectionRecord, HYPERCERT_COLLECTIONS.COLLECTION, "main", false);
|
|
4562
|
-
if (!validation.success) {
|
|
4563
|
-
throw new ValidationError(`Invalid collection record: ${validation.error?.message}`);
|
|
4564
|
-
}
|
|
4565
|
-
const result = await this.agent.com.atproto.repo.createRecord({
|
|
4566
|
-
repo: this.repoDid,
|
|
4567
|
-
collection: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
4568
|
-
record: collectionRecord,
|
|
4569
|
-
});
|
|
4570
|
-
if (!result.success) {
|
|
4571
|
-
throw new NetworkError("Failed to create collection");
|
|
4572
|
-
}
|
|
4573
|
-
this.emit("collectionCreated", { uri: result.data.uri, cid: result.data.cid });
|
|
4574
|
-
return { uri: result.data.uri, cid: result.data.cid };
|
|
4572
|
+
const createdAt = new Date().toISOString();
|
|
4573
|
+
const collectionRecord = {
|
|
4574
|
+
$type: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
4575
|
+
title: params.title,
|
|
4576
|
+
items: [],
|
|
4577
|
+
createdAt,
|
|
4578
|
+
};
|
|
4579
|
+
if (params.type) {
|
|
4580
|
+
collectionRecord.type = params.type;
|
|
4575
4581
|
}
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4582
|
+
if (params.shortDescription) {
|
|
4583
|
+
collectionRecord.shortDescription = params.shortDescription;
|
|
4584
|
+
}
|
|
4585
|
+
if (params.description) {
|
|
4586
|
+
collectionRecord.description = params.description;
|
|
4587
|
+
}
|
|
4588
|
+
if (params.avatar) {
|
|
4589
|
+
collectionRecord.avatar = await this.resolveCollectionImageInput(params.avatar);
|
|
4580
4590
|
}
|
|
4591
|
+
if (params.banner) {
|
|
4592
|
+
collectionRecord.banner = await this.resolveCollectionImageInput(params.banner, true);
|
|
4593
|
+
}
|
|
4594
|
+
if (params.items !== undefined) {
|
|
4595
|
+
collectionRecord.items = params.items;
|
|
4596
|
+
}
|
|
4597
|
+
if (params.location) {
|
|
4598
|
+
const locationResult = await this.resolveLocation(params.location);
|
|
4599
|
+
collectionRecord.location = locationResult;
|
|
4600
|
+
}
|
|
4601
|
+
const validation = lexicon.validate(collectionRecord, HYPERCERT_COLLECTIONS.COLLECTION, "main", false);
|
|
4602
|
+
if (!validation.success) {
|
|
4603
|
+
throw new ValidationError(`Invalid collection record: ${validation.error?.message}`);
|
|
4604
|
+
}
|
|
4605
|
+
const result = await this.agent.com.atproto.repo.createRecord({
|
|
4606
|
+
repo: this.repoDid,
|
|
4607
|
+
collection: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
4608
|
+
record: collectionRecord,
|
|
4609
|
+
});
|
|
4610
|
+
if (!result.success) {
|
|
4611
|
+
throw new NetworkError("Failed to create collection");
|
|
4612
|
+
}
|
|
4613
|
+
const createCollectionResult = {
|
|
4614
|
+
uri: result.data.uri,
|
|
4615
|
+
cid: result.data.cid,
|
|
4616
|
+
record: collectionRecord,
|
|
4617
|
+
};
|
|
4618
|
+
this.emit("collectionCreated", createCollectionResult);
|
|
4619
|
+
return createCollectionResult;
|
|
4581
4620
|
}
|
|
4582
4621
|
/**
|
|
4583
4622
|
* Gets a collection by its AT-URI.
|
|
@@ -4670,114 +4709,32 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4670
4709
|
/**
|
|
4671
4710
|
* Creates a new project that organizes multiple hypercert activities.
|
|
4672
4711
|
*
|
|
4673
|
-
*
|
|
4712
|
+
* A project is a collection with type='project' and optional location sidecar.
|
|
4713
|
+
* This method delegates to createCollection and adds the type field.
|
|
4674
4714
|
*
|
|
4675
4715
|
* @param params - Project creation parameters
|
|
4676
|
-
* @returns Promise resolving to created project URI and CID
|
|
4716
|
+
* @returns Promise resolving to created project URI and CID with optional location URI
|
|
4677
4717
|
*
|
|
4678
4718
|
* @example
|
|
4679
4719
|
* ```typescript
|
|
4680
4720
|
* const result = await repo.hypercerts.createProject({
|
|
4681
4721
|
* title: "Climate Impact 2024",
|
|
4682
4722
|
* shortDescription: "Year-long climate initiative",
|
|
4683
|
-
*
|
|
4684
|
-
* { uri: activity1Uri, cid: activity1Cid,
|
|
4685
|
-
* { uri: activity2Uri, cid: activity2Cid,
|
|
4723
|
+
* items: [
|
|
4724
|
+
* { itemIdentifier: { uri: activity1Uri, cid: activity1Cid }, itemWeight: "0.6" },
|
|
4725
|
+
* { itemIdentifier: { uri: activity2Uri, cid: activity2Cid }, itemWeight: "0.4" }
|
|
4686
4726
|
* ]
|
|
4687
4727
|
* });
|
|
4688
4728
|
* console.log(`Created project: ${result.uri}`);
|
|
4689
4729
|
* ```
|
|
4690
4730
|
*/
|
|
4691
4731
|
async createProject(params) {
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
const uint8Array = new Uint8Array(arrayBuffer);
|
|
4699
|
-
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
|
|
4700
|
-
encoding: params.avatar.type || "image/jpeg",
|
|
4701
|
-
});
|
|
4702
|
-
if (uploadResult.success) {
|
|
4703
|
-
avatarRef = {
|
|
4704
|
-
$type: "blob",
|
|
4705
|
-
ref: { $link: uploadResult.data.blob.ref.toString() },
|
|
4706
|
-
mimeType: uploadResult.data.blob.mimeType,
|
|
4707
|
-
size: uploadResult.data.blob.size,
|
|
4708
|
-
};
|
|
4709
|
-
}
|
|
4710
|
-
else {
|
|
4711
|
-
throw new NetworkError("Failed to upload avatar image");
|
|
4712
|
-
}
|
|
4713
|
-
}
|
|
4714
|
-
// Upload banner blob if provided
|
|
4715
|
-
let bannerRef;
|
|
4716
|
-
if (params.banner) {
|
|
4717
|
-
const arrayBuffer = await params.banner.arrayBuffer();
|
|
4718
|
-
const uint8Array = new Uint8Array(arrayBuffer);
|
|
4719
|
-
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
|
|
4720
|
-
encoding: params.banner.type || "image/jpeg",
|
|
4721
|
-
});
|
|
4722
|
-
if (uploadResult.success) {
|
|
4723
|
-
bannerRef = {
|
|
4724
|
-
$type: "blob",
|
|
4725
|
-
ref: { $link: uploadResult.data.blob.ref.toString() },
|
|
4726
|
-
mimeType: uploadResult.data.blob.mimeType,
|
|
4727
|
-
size: uploadResult.data.blob.size,
|
|
4728
|
-
};
|
|
4729
|
-
}
|
|
4730
|
-
else {
|
|
4731
|
-
throw new NetworkError("Failed to upload banner image");
|
|
4732
|
-
}
|
|
4733
|
-
}
|
|
4734
|
-
// Build project record as a collection with type='project'
|
|
4735
|
-
// Collections require 'items' array, so we map activities to items
|
|
4736
|
-
const items = params.activities?.map((a) => ({
|
|
4737
|
-
itemIdentifier: { uri: a.uri, cid: a.cid },
|
|
4738
|
-
itemWeight: a.weight,
|
|
4739
|
-
})) || [];
|
|
4740
|
-
const projectRecord = {
|
|
4741
|
-
$type: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
4742
|
-
type: "project",
|
|
4743
|
-
title: params.title,
|
|
4744
|
-
shortDescription: params.shortDescription,
|
|
4745
|
-
items,
|
|
4746
|
-
createdAt,
|
|
4747
|
-
};
|
|
4748
|
-
// Add optional fields
|
|
4749
|
-
if (params.description) {
|
|
4750
|
-
projectRecord.description = params.description;
|
|
4751
|
-
}
|
|
4752
|
-
if (avatarRef) {
|
|
4753
|
-
projectRecord.avatar = avatarRef;
|
|
4754
|
-
}
|
|
4755
|
-
if (bannerRef) {
|
|
4756
|
-
projectRecord.banner = bannerRef;
|
|
4757
|
-
}
|
|
4758
|
-
// Validate against collection lexicon
|
|
4759
|
-
const validation = lexicon.validate(projectRecord, HYPERCERT_COLLECTIONS.COLLECTION, "main", false);
|
|
4760
|
-
if (!validation.success) {
|
|
4761
|
-
throw new ValidationError(`Invalid project record: ${validation.error?.message}`);
|
|
4762
|
-
}
|
|
4763
|
-
// Create record
|
|
4764
|
-
const result = await this.agent.com.atproto.repo.createRecord({
|
|
4765
|
-
repo: this.repoDid,
|
|
4766
|
-
collection: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
4767
|
-
record: projectRecord,
|
|
4768
|
-
});
|
|
4769
|
-
if (!result.success) {
|
|
4770
|
-
throw new NetworkError("Failed to create project");
|
|
4771
|
-
}
|
|
4772
|
-
// Emit event
|
|
4773
|
-
this.emit("projectCreated", { uri: result.data.uri, cid: result.data.cid });
|
|
4774
|
-
return { uri: result.data.uri, cid: result.data.cid };
|
|
4775
|
-
}
|
|
4776
|
-
catch (error) {
|
|
4777
|
-
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
4778
|
-
throw error;
|
|
4779
|
-
throw new NetworkError(`Failed to create project: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
4780
|
-
}
|
|
4732
|
+
const result = await this.createCollection({
|
|
4733
|
+
...params,
|
|
4734
|
+
type: "project",
|
|
4735
|
+
});
|
|
4736
|
+
this.emit("projectCreated", { uri: result.uri, cid: result.cid });
|
|
4737
|
+
return result;
|
|
4781
4738
|
}
|
|
4782
4739
|
/**
|
|
4783
4740
|
* Gets a project by its AT-URI.
|
|
@@ -4891,7 +4848,8 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4891
4848
|
/**
|
|
4892
4849
|
* Updates an existing project.
|
|
4893
4850
|
*
|
|
4894
|
-
*
|
|
4851
|
+
* A project is a collection with type='project'. This method delegates to
|
|
4852
|
+
* updateCollection and handles the avatar field.
|
|
4895
4853
|
*
|
|
4896
4854
|
* @param uri - AT-URI of the project to update
|
|
4897
4855
|
* @param updates - Fields to update
|
|
@@ -4906,8 +4864,73 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4906
4864
|
* ```
|
|
4907
4865
|
*/
|
|
4908
4866
|
async updateProject(uri, updates) {
|
|
4867
|
+
// Verify it's a project before updating
|
|
4868
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
4869
|
+
if (!uriMatch) {
|
|
4870
|
+
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
4871
|
+
}
|
|
4872
|
+
const [, , collection, rkey] = uriMatch;
|
|
4873
|
+
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
4874
|
+
repo: this.repoDid,
|
|
4875
|
+
collection,
|
|
4876
|
+
rkey,
|
|
4877
|
+
});
|
|
4878
|
+
if (!existing.success) {
|
|
4879
|
+
throw new NetworkError(`Project not found: ${uri}`);
|
|
4880
|
+
}
|
|
4881
|
+
const record = existing.data.value;
|
|
4882
|
+
if (record.type !== "project") {
|
|
4883
|
+
throw new ValidationError(`Record is not a project (type='${record.type}')`);
|
|
4884
|
+
}
|
|
4885
|
+
// Delegate to updateCollection
|
|
4886
|
+
const result = await this.updateCollection(uri, updates);
|
|
4887
|
+
this.emit("projectUpdated", { uri: result.uri, cid: result.cid });
|
|
4888
|
+
return result;
|
|
4889
|
+
}
|
|
4890
|
+
/**
|
|
4891
|
+
* Deletes a project.
|
|
4892
|
+
*
|
|
4893
|
+
* A project is a collection with type='project'. This method delegates to
|
|
4894
|
+
* deleteCollection after verifying the record is a project.
|
|
4895
|
+
*
|
|
4896
|
+
* @param uri - AT-URI of the project to delete
|
|
4897
|
+
*
|
|
4898
|
+
* @example
|
|
4899
|
+
* ```typescript
|
|
4900
|
+
* await repo.hypercerts.deleteProject(projectUri);
|
|
4901
|
+
* console.log("Project deleted");
|
|
4902
|
+
* ```
|
|
4903
|
+
*/
|
|
4904
|
+
async deleteProject(uri) {
|
|
4905
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
4906
|
+
if (!uriMatch) {
|
|
4907
|
+
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
4908
|
+
}
|
|
4909
|
+
const [, , collection, rkey] = uriMatch;
|
|
4910
|
+
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
4911
|
+
repo: this.repoDid,
|
|
4912
|
+
collection,
|
|
4913
|
+
rkey,
|
|
4914
|
+
});
|
|
4915
|
+
if (!existing.success) {
|
|
4916
|
+
throw new NetworkError(`Project not found: ${uri}`);
|
|
4917
|
+
}
|
|
4918
|
+
const record = existing.data.value;
|
|
4919
|
+
if (record.type !== "project") {
|
|
4920
|
+
throw new ValidationError(`Record is not a project (type='${record.type}')`);
|
|
4921
|
+
}
|
|
4922
|
+
await this.deleteCollection(uri);
|
|
4923
|
+
this.emit("projectDeleted", { uri });
|
|
4924
|
+
}
|
|
4925
|
+
/**
|
|
4926
|
+
* Updates a collection.
|
|
4927
|
+
*
|
|
4928
|
+
* @param uri - AT-URI of the collection to update
|
|
4929
|
+
* @param updates - Fields to update
|
|
4930
|
+
* @returns Promise resolving to updated collection URI and CID
|
|
4931
|
+
*/
|
|
4932
|
+
async updateCollection(uri, updates) {
|
|
4909
4933
|
try {
|
|
4910
|
-
// Parse URI and fetch existing record
|
|
4911
4934
|
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
4912
4935
|
if (!uriMatch) {
|
|
4913
4936
|
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
@@ -4919,100 +4942,74 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
4919
4942
|
rkey,
|
|
4920
4943
|
});
|
|
4921
4944
|
if (!existing.success) {
|
|
4922
|
-
throw new NetworkError(`
|
|
4945
|
+
throw new NetworkError(`Collection not found: ${uri}`);
|
|
4923
4946
|
}
|
|
4924
4947
|
const existingRecord = existing.data.value;
|
|
4925
|
-
// Verify it's actually a project
|
|
4926
|
-
if (existingRecord.type !== "project") {
|
|
4927
|
-
throw new ValidationError(`Record is not a project (type='${existingRecord.type}')`);
|
|
4928
|
-
}
|
|
4929
|
-
// Merge updates with existing record
|
|
4930
4948
|
const recordForUpdate = {
|
|
4931
4949
|
...existingRecord,
|
|
4932
|
-
// MUST preserve type, createdAt, and items structure
|
|
4933
|
-
type: "project",
|
|
4934
4950
|
createdAt: existingRecord.createdAt,
|
|
4951
|
+
type: existingRecord.type,
|
|
4935
4952
|
};
|
|
4936
|
-
// Apply simple field updates
|
|
4937
4953
|
if (updates.title !== undefined)
|
|
4938
4954
|
recordForUpdate.title = updates.title;
|
|
4939
4955
|
if (updates.shortDescription !== undefined)
|
|
4940
4956
|
recordForUpdate.shortDescription = updates.shortDescription;
|
|
4941
4957
|
if (updates.description !== undefined)
|
|
4942
4958
|
recordForUpdate.description = updates.description;
|
|
4943
|
-
//
|
|
4959
|
+
// Explicitly reject type changes
|
|
4960
|
+
if (updates.type !== undefined && updates.type !== existingRecord.type) {
|
|
4961
|
+
throw new ValidationError(`Cannot change collection type from '${existingRecord.type}' to '${updates.type}'`);
|
|
4962
|
+
}
|
|
4944
4963
|
delete recordForUpdate.avatar;
|
|
4945
4964
|
if (updates.avatar !== undefined) {
|
|
4946
4965
|
if (updates.avatar === null) {
|
|
4947
4966
|
// Remove avatar
|
|
4948
4967
|
}
|
|
4949
4968
|
else {
|
|
4950
|
-
|
|
4951
|
-
const uint8Array = new Uint8Array(arrayBuffer);
|
|
4952
|
-
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
|
|
4953
|
-
encoding: updates.avatar.type || "image/jpeg",
|
|
4954
|
-
});
|
|
4955
|
-
if (uploadResult.success) {
|
|
4956
|
-
recordForUpdate.avatar = {
|
|
4957
|
-
$type: "blob",
|
|
4958
|
-
ref: uploadResult.data.blob.ref,
|
|
4959
|
-
mimeType: uploadResult.data.blob.mimeType,
|
|
4960
|
-
size: uploadResult.data.blob.size,
|
|
4961
|
-
};
|
|
4962
|
-
}
|
|
4963
|
-
else {
|
|
4964
|
-
throw new NetworkError("Failed to upload avatar image");
|
|
4965
|
-
}
|
|
4969
|
+
recordForUpdate.avatar = await this.resolveCollectionImageInput(updates.avatar);
|
|
4966
4970
|
}
|
|
4967
4971
|
}
|
|
4968
4972
|
else if (existingRecord.avatar) {
|
|
4969
4973
|
recordForUpdate.avatar = existingRecord.avatar;
|
|
4970
4974
|
}
|
|
4971
|
-
// Handle banner update
|
|
4972
4975
|
delete recordForUpdate.banner;
|
|
4973
4976
|
if (updates.banner !== undefined) {
|
|
4974
4977
|
if (updates.banner === null) {
|
|
4975
4978
|
// Remove banner
|
|
4976
4979
|
}
|
|
4977
4980
|
else {
|
|
4978
|
-
|
|
4979
|
-
const uint8Array = new Uint8Array(arrayBuffer);
|
|
4980
|
-
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
|
|
4981
|
-
encoding: updates.banner.type || "image/jpeg",
|
|
4982
|
-
});
|
|
4983
|
-
if (uploadResult.success) {
|
|
4984
|
-
recordForUpdate.banner = {
|
|
4985
|
-
$type: "blob",
|
|
4986
|
-
ref: uploadResult.data.blob.ref,
|
|
4987
|
-
mimeType: uploadResult.data.blob.mimeType,
|
|
4988
|
-
size: uploadResult.data.blob.size,
|
|
4989
|
-
};
|
|
4990
|
-
}
|
|
4991
|
-
else {
|
|
4992
|
-
throw new NetworkError("Failed to upload banner image");
|
|
4993
|
-
}
|
|
4981
|
+
recordForUpdate.banner = await this.resolveCollectionImageInput(updates.banner, true);
|
|
4994
4982
|
}
|
|
4995
4983
|
}
|
|
4996
4984
|
else if (existingRecord.banner) {
|
|
4997
4985
|
recordForUpdate.banner = existingRecord.banner;
|
|
4998
4986
|
}
|
|
4999
|
-
|
|
5000
|
-
if (updates.
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
4987
|
+
delete recordForUpdate.location;
|
|
4988
|
+
if (updates.location !== undefined) {
|
|
4989
|
+
if (updates.location === null) {
|
|
4990
|
+
// Remove location
|
|
4991
|
+
}
|
|
4992
|
+
else {
|
|
4993
|
+
const resolvedLocation = await this.resolveLocation(updates.location);
|
|
4994
|
+
if (!resolvedLocation) {
|
|
4995
|
+
throw new ValidationError("resolveLocation: failed to resolve location");
|
|
4996
|
+
}
|
|
4997
|
+
recordForUpdate.location = resolvedLocation;
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
5000
|
+
else if (existingRecord.location) {
|
|
5001
|
+
recordForUpdate.location = existingRecord.location;
|
|
5005
5002
|
}
|
|
5006
|
-
|
|
5007
|
-
|
|
5003
|
+
if (updates.items) {
|
|
5004
|
+
recordForUpdate.items = updates.items;
|
|
5005
|
+
}
|
|
5006
|
+
else if (existingRecord.items) {
|
|
5008
5007
|
recordForUpdate.items = existingRecord.items;
|
|
5009
5008
|
}
|
|
5010
|
-
// Validate merged record
|
|
5011
5009
|
const validation = lexicon.validate(recordForUpdate, HYPERCERT_COLLECTIONS.COLLECTION, "main", false);
|
|
5012
5010
|
if (!validation.success) {
|
|
5013
|
-
throw new ValidationError(`Invalid
|
|
5011
|
+
throw new ValidationError(`Invalid collection record: ${validation.error?.message}`);
|
|
5014
5012
|
}
|
|
5015
|
-
// Update record
|
|
5016
5013
|
const result = await this.agent.com.atproto.repo.putRecord({
|
|
5017
5014
|
repo: this.repoDid,
|
|
5018
5015
|
collection,
|
|
@@ -5020,68 +5017,196 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
|
|
|
5020
5017
|
record: recordForUpdate,
|
|
5021
5018
|
});
|
|
5022
5019
|
if (!result.success) {
|
|
5023
|
-
throw new NetworkError("Failed to update
|
|
5020
|
+
throw new NetworkError("Failed to update collection");
|
|
5024
5021
|
}
|
|
5025
|
-
|
|
5026
|
-
this.emit("projectUpdated", { uri: result.data.uri, cid: result.data.cid });
|
|
5022
|
+
this.emit("collectionUpdated", { uri: result.data.uri, cid: result.data.cid });
|
|
5027
5023
|
return { uri: result.data.uri, cid: result.data.cid };
|
|
5028
5024
|
}
|
|
5029
5025
|
catch (error) {
|
|
5030
5026
|
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
5031
5027
|
throw error;
|
|
5032
|
-
throw new NetworkError(`Failed to update
|
|
5028
|
+
throw new NetworkError(`Failed to update collection: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
5033
5029
|
}
|
|
5034
5030
|
}
|
|
5035
5031
|
/**
|
|
5036
|
-
* Deletes a
|
|
5032
|
+
* Deletes a collection.
|
|
5037
5033
|
*
|
|
5038
|
-
*
|
|
5034
|
+
* @param uri - AT-URI of the collection to delete
|
|
5035
|
+
*/
|
|
5036
|
+
async deleteCollection(uri) {
|
|
5037
|
+
try {
|
|
5038
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
5039
|
+
if (!uriMatch) {
|
|
5040
|
+
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5041
|
+
}
|
|
5042
|
+
const [, , collection, rkey] = uriMatch;
|
|
5043
|
+
const result = await this.agent.com.atproto.repo.deleteRecord({
|
|
5044
|
+
repo: this.repoDid,
|
|
5045
|
+
collection,
|
|
5046
|
+
rkey,
|
|
5047
|
+
});
|
|
5048
|
+
if (!result.success) {
|
|
5049
|
+
throw new NetworkError("Failed to delete collection");
|
|
5050
|
+
}
|
|
5051
|
+
this.emit("collectionDeleted", { uri });
|
|
5052
|
+
}
|
|
5053
|
+
catch (error) {
|
|
5054
|
+
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
5055
|
+
throw error;
|
|
5056
|
+
throw new NetworkError(`Failed to delete collection: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
/**
|
|
5060
|
+
* Attaches a location to a collection.
|
|
5039
5061
|
*
|
|
5040
|
-
* @param uri - AT-URI of the
|
|
5062
|
+
* @param uri - AT-URI of the collection
|
|
5063
|
+
* @param location - Location data
|
|
5064
|
+
* @returns Promise resolving to location record result
|
|
5065
|
+
*/
|
|
5066
|
+
async attachLocationToCollection(uri, location) {
|
|
5067
|
+
try {
|
|
5068
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
5069
|
+
if (!uriMatch) {
|
|
5070
|
+
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5071
|
+
}
|
|
5072
|
+
const [, , collection, rkey] = uriMatch;
|
|
5073
|
+
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
5074
|
+
repo: this.repoDid,
|
|
5075
|
+
collection,
|
|
5076
|
+
rkey,
|
|
5077
|
+
});
|
|
5078
|
+
if (!existing.success) {
|
|
5079
|
+
throw new NetworkError(`Collection not found: ${uri}`);
|
|
5080
|
+
}
|
|
5081
|
+
const resolvedLocation = await this.resolveLocation(location);
|
|
5082
|
+
if (!resolvedLocation) {
|
|
5083
|
+
throw new ValidationError("attachLocationToCollection: failed to resolve location");
|
|
5084
|
+
}
|
|
5085
|
+
const recordForUpdate = {
|
|
5086
|
+
...existing.data.value,
|
|
5087
|
+
location: resolvedLocation,
|
|
5088
|
+
};
|
|
5089
|
+
const updateResult = await this.agent.com.atproto.repo.putRecord({
|
|
5090
|
+
repo: this.repoDid,
|
|
5091
|
+
collection,
|
|
5092
|
+
rkey,
|
|
5093
|
+
record: recordForUpdate,
|
|
5094
|
+
});
|
|
5095
|
+
if (!updateResult.success) {
|
|
5096
|
+
throw new NetworkError("Failed to update collection with location");
|
|
5097
|
+
}
|
|
5098
|
+
this.emit("locationAttachedToCollection", {
|
|
5099
|
+
uri: resolvedLocation.uri,
|
|
5100
|
+
cid: resolvedLocation.cid,
|
|
5101
|
+
collectionUri: uri,
|
|
5102
|
+
});
|
|
5103
|
+
return { uri: resolvedLocation.uri, cid: resolvedLocation.cid };
|
|
5104
|
+
}
|
|
5105
|
+
catch (error) {
|
|
5106
|
+
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
5107
|
+
throw error;
|
|
5108
|
+
throw new NetworkError(`Failed to attach location: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5111
|
+
/**
|
|
5112
|
+
* Removes a location from a collection.
|
|
5041
5113
|
*
|
|
5042
|
-
* @
|
|
5043
|
-
* ```typescript
|
|
5044
|
-
* await repo.hypercerts.deleteProject(projectUri);
|
|
5045
|
-
* console.log("Project deleted");
|
|
5046
|
-
* ```
|
|
5114
|
+
* @param uri - AT-URI of the collection
|
|
5047
5115
|
*/
|
|
5048
|
-
async
|
|
5116
|
+
async removeLocationFromCollection(uri) {
|
|
5049
5117
|
try {
|
|
5050
|
-
// Parse URI
|
|
5051
5118
|
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
5052
5119
|
if (!uriMatch) {
|
|
5053
5120
|
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5054
5121
|
}
|
|
5055
5122
|
const [, , collection, rkey] = uriMatch;
|
|
5056
|
-
// Verify it's actually a project before deleting
|
|
5057
5123
|
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
5058
5124
|
repo: this.repoDid,
|
|
5059
5125
|
collection,
|
|
5060
5126
|
rkey,
|
|
5061
5127
|
});
|
|
5062
|
-
if (existing.success) {
|
|
5063
|
-
|
|
5064
|
-
if (record.type !== "project") {
|
|
5065
|
-
throw new ValidationError(`Record is not a project (type='${record.type}')`);
|
|
5066
|
-
}
|
|
5128
|
+
if (!existing.success) {
|
|
5129
|
+
throw new NetworkError(`Collection not found: ${uri}`);
|
|
5067
5130
|
}
|
|
5068
|
-
|
|
5069
|
-
|
|
5131
|
+
const recordForUpdate = { ...existing.data.value };
|
|
5132
|
+
delete recordForUpdate.location;
|
|
5133
|
+
const result = await this.agent.com.atproto.repo.putRecord({
|
|
5070
5134
|
repo: this.repoDid,
|
|
5071
5135
|
collection,
|
|
5072
5136
|
rkey,
|
|
5137
|
+
record: recordForUpdate,
|
|
5073
5138
|
});
|
|
5074
5139
|
if (!result.success) {
|
|
5075
|
-
throw new NetworkError("Failed to
|
|
5140
|
+
throw new NetworkError("Failed to remove location from collection");
|
|
5076
5141
|
}
|
|
5077
|
-
|
|
5078
|
-
this.emit("projectDeleted", { uri });
|
|
5142
|
+
this.emit("locationRemovedFromCollection", { collectionUri: uri });
|
|
5079
5143
|
}
|
|
5080
5144
|
catch (error) {
|
|
5081
5145
|
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
5082
5146
|
throw error;
|
|
5083
|
-
throw new NetworkError(`Failed to
|
|
5147
|
+
throw new NetworkError(`Failed to remove location: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
/**
|
|
5151
|
+
* Attaches a location to a project.
|
|
5152
|
+
*
|
|
5153
|
+
* @param uri - AT-URI of the project
|
|
5154
|
+
* @param location - Location data
|
|
5155
|
+
* @returns Promise resolving to location record result
|
|
5156
|
+
*/
|
|
5157
|
+
async attachLocationToProject(uri, location) {
|
|
5158
|
+
const result = await this.attachLocationToCollection(uri, location);
|
|
5159
|
+
this.emit("locationAttachedToProject", {
|
|
5160
|
+
uri: result.uri,
|
|
5161
|
+
cid: result.cid,
|
|
5162
|
+
projectUri: uri,
|
|
5163
|
+
});
|
|
5164
|
+
return result;
|
|
5165
|
+
}
|
|
5166
|
+
/**
|
|
5167
|
+
* Removes a location from a project.
|
|
5168
|
+
*
|
|
5169
|
+
* @param uri - AT-URI of the project
|
|
5170
|
+
*/
|
|
5171
|
+
async removeLocationFromProject(uri) {
|
|
5172
|
+
await this.removeLocationFromCollection(uri);
|
|
5173
|
+
this.emit("locationRemovedFromProject", { projectUri: uri });
|
|
5174
|
+
}
|
|
5175
|
+
/**
|
|
5176
|
+
* Creates an app.certified.location record.
|
|
5177
|
+
*
|
|
5178
|
+
* @param location - Location parameters
|
|
5179
|
+
* @returns Promise resolving to location record URI and CID
|
|
5180
|
+
*/
|
|
5181
|
+
async createLocationRecord(location) {
|
|
5182
|
+
if (!location.srs) {
|
|
5183
|
+
throw new ValidationError("srs (Spatial Reference System) is required. Example: 'EPSG:4326' for WGS84 coordinates, or 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' for CRS84.");
|
|
5184
|
+
}
|
|
5185
|
+
const { $type, createdAt, location: locationValue, lpVersion, ...rest } = location;
|
|
5186
|
+
if (locationValue === undefined) {
|
|
5187
|
+
throw new ValidationError("location is required to create a location record.");
|
|
5188
|
+
}
|
|
5189
|
+
const resolvedLocationValue = await this.resolveLocationValue(locationValue);
|
|
5190
|
+
const locationRecord = {
|
|
5191
|
+
...rest,
|
|
5192
|
+
lpVersion: lpVersion ?? "1.0",
|
|
5193
|
+
$type: $type ?? HYPERCERT_COLLECTIONS.LOCATION,
|
|
5194
|
+
createdAt: createdAt ?? new Date().toISOString(),
|
|
5195
|
+
location: resolvedLocationValue,
|
|
5196
|
+
};
|
|
5197
|
+
const validation = lexicon.validate(locationRecord, HYPERCERT_COLLECTIONS.LOCATION, "main", false);
|
|
5198
|
+
if (!validation.success) {
|
|
5199
|
+
throw new ValidationError(`Invalid location record: ${validation.error?.message}`);
|
|
5200
|
+
}
|
|
5201
|
+
const result = await this.agent.com.atproto.repo.createRecord({
|
|
5202
|
+
repo: this.repoDid,
|
|
5203
|
+
collection: HYPERCERT_COLLECTIONS.LOCATION,
|
|
5204
|
+
record: locationRecord,
|
|
5205
|
+
});
|
|
5206
|
+
if (!result.success) {
|
|
5207
|
+
throw new NetworkError("Failed to create location record");
|
|
5084
5208
|
}
|
|
5209
|
+
return { uri: result.data.uri, cid: result.data.cid };
|
|
5085
5210
|
}
|
|
5086
5211
|
}
|
|
5087
5212
|
|