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