@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/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 createdAt = new Date().toISOString();
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: result.data.uri,
4236
- cid: result.data.cid,
4209
+ uri: resolvedLocation.uri,
4210
+ cid: resolvedLocation.cid,
4237
4211
  },
4238
4212
  },
4239
4213
  });
4240
- this.emit("locationAttached", { uri: result.data.uri, cid: result.data.cid, hypercertUri });
4241
- return { uri: result.data.uri, cid: result.data.cid };
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 union type
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
- return {
4237
+ const uriRef = {
4260
4238
  $type: "org.hypercerts.defs#uri",
4261
4239
  uri: content,
4262
4240
  };
4241
+ return uriRef;
4263
4242
  }
4264
- else {
4265
- const uploadedBlob = await this.handleBlobUpload(content, fallbackMimeType);
4266
- return {
4267
- $type: "org.hypercerts.defs#smallBlob",
4268
- blob: uploadedBlob,
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.claims - Array of hypercert references with weights
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
- * claims: [
4522
- * { uri: hypercert1Uri, cid: hypercert1Cid, weight: "0.5" },
4523
- * { uri: hypercert2Uri, cid: hypercert2Cid, weight: "0.3" },
4524
- * { uri: hypercert3Uri, cid: hypercert3Cid, weight: "0.2" },
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
- try {
4532
- const createdAt = new Date().toISOString();
4533
- let bannerRef;
4534
- if (params.banner) {
4535
- const arrayBuffer = await params.banner.arrayBuffer();
4536
- const uint8Array = new Uint8Array(arrayBuffer);
4537
- const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
4538
- encoding: params.banner.type || "image/jpeg",
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
- catch (error) {
4577
- if (error instanceof ValidationError || error instanceof NetworkError)
4578
- throw error;
4579
- throw new NetworkError(`Failed to create collection: ${error instanceof Error ? error.message : "Unknown"}`, error);
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
- * Projects are now implemented as collections with `type='project'`.
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
- * activities: [
4684
- * { uri: activity1Uri, cid: activity1Cid, weight: "0.6" },
4685
- * { uri: activity2Uri, cid: activity2Cid, weight: "0.4" }
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
- try {
4693
- const createdAt = new Date().toISOString();
4694
- // Upload avatar blob if provided
4695
- let avatarRef;
4696
- if (params.avatar) {
4697
- const arrayBuffer = await params.avatar.arrayBuffer();
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
- * Projects are collections with `type='project'`.
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(`Project not found: ${uri}`);
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
- // Handle avatar update with three-way logic
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
- const arrayBuffer = await updates.avatar.arrayBuffer();
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
- const arrayBuffer = await updates.banner.arrayBuffer();
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
- // Transform activities to items array
5000
- if (updates.activities) {
5001
- recordForUpdate.items = updates.activities.map((a) => ({
5002
- itemIdentifier: { uri: a.uri, cid: a.cid },
5003
- itemWeight: a.weight,
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
- else {
5007
- // Preserve existing items
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 project record: ${validation.error?.message}`);
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 project");
5020
+ throw new NetworkError("Failed to update collection");
5024
5021
  }
5025
- // Emit event
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 project: ${error instanceof Error ? error.message : "Unknown"}`, error);
5028
+ throw new NetworkError(`Failed to update collection: ${error instanceof Error ? error.message : "Unknown"}`, error);
5033
5029
  }
5034
5030
  }
5035
5031
  /**
5036
- * Deletes a project.
5032
+ * Deletes a collection.
5037
5033
  *
5038
- * Projects are collections with `type='project'`.
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 project to delete
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
- * @example
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 deleteProject(uri) {
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
- const record = existing.data.value;
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
- // Delete record
5069
- const result = await this.agent.com.atproto.repo.deleteRecord({
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 delete project");
5140
+ throw new NetworkError("Failed to remove location from collection");
5076
5141
  }
5077
- // Emit event
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 delete project: ${error instanceof Error ? error.message : "Unknown"}`, error);
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