@@ -36,9 +36,9 @@ import {
3636 type UpdateProjectParams ,
3737 type CreateMeasurementParams ,
3838 type UpdateMeasurementParams ,
39+ type CreateAttachmentParams ,
3940} from "../services/hypercerts/types.js" ;
4041import type {
41- CreateHypercertEvidenceParams ,
4242 LocationParams ,
4343 CreateHypercertParams ,
4444 CreateHypercertResult ,
@@ -470,38 +470,38 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
470470 }
471471
472472 /**
473- * Creates evidence records with progress tracking .
473+ * Creates attachment records and returns their URIs .
474474 *
475- * @param hypercertUri - URI of the hypercert
476- * @param evidenceItems - Array of evidence data (without subjectUri)
475+ * @param hypercertUri - URI of the parent hypercert
476+ * @param attachmentItems - Array of attachment items to create
477477 * @param onProgress - Optional progress callback
478- * @returns Promise resolving to array of evidence URIs
478+ * @returns Promise resolving to array of attachment URIs
479479 * @internal
480480 */
481- private async createEvidenceWithProgress (
481+ private async createAttachmentsWithProgress (
482482 hypercertUri : string ,
483- evidenceItems : Array < Omit < CreateHypercertEvidenceParams , "subjectUri " > > ,
483+ attachmentItems : Array < Omit < CreateAttachmentParams , "subjects " > > ,
484484 onProgress ?: ( step : ProgressStep ) => void ,
485485 ) : Promise < string [ ] > {
486- this . emitProgress ( onProgress , { name : "addEvidence " , status : "start" } ) ;
486+ this . emitProgress ( onProgress , { name : "addAttachment " , status : "start" } ) ;
487487 try {
488- const evidenceUris = await Promise . all (
489- evidenceItems . map ( ( evidence ) =>
490- this . addEvidence ( {
491- subjectUri : hypercertUri ,
492- ... evidence ,
493- } as CreateHypercertEvidenceParams ) . then ( ( result ) => result . uri ) ,
488+ const attachmentUris = await Promise . all (
489+ attachmentItems . map ( ( attachment ) =>
490+ this . addAttachment ( {
491+ ... attachment ,
492+ subjects : hypercertUri ,
493+ } as CreateAttachmentParams ) . then ( ( result ) => result . uri ) ,
494494 ) ,
495495 ) ;
496496 this . emitProgress ( onProgress , {
497- name : "addEvidence " ,
497+ name : "addAttachment " ,
498498 status : "success" ,
499- data : { count : evidenceUris . length } ,
499+ data : { count : attachmentUris . length } ,
500500 } ) ;
501- return evidenceUris ;
501+ return attachmentUris ;
502502 } catch ( error ) {
503- this . emitProgress ( onProgress , { name : "addEvidence " , status : "error" , error : error as Error } ) ;
504- this . logger ?. warn ( `Failed to create evidence : ${ error instanceof Error ? error . message : "Unknown" } ` ) ;
503+ this . emitProgress ( onProgress , { name : "addAttachment " , status : "error" , error : error as Error } ) ;
504+ this . logger ?. warn ( `Failed to create attachments : ${ error instanceof Error ? error . message : "Unknown" } ` ) ;
505505 throw error ;
506506 }
507507 }
@@ -534,7 +534,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
534534 * - `createHypercert`: Main hypercert record creation
535535 * - `attachLocation`: Location record creation
536536 * - `createContributions`: Contribution records creation
537- * - `addEvidence `: Evidence records creation
537+ * - `addAttachment `: Attachment records creation
538538 *
539539 * @example Minimal hypercert
540540 * ```typescript
@@ -569,7 +569,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
569569 * { contributors: ["did:plc:org1"], role: "coordinator" },
570570 * { contributors: ["did:plc:org2"], role: "implementer" },
571571 * ],
572- * evidence : [{ uri: "https://...", description: "Satellite data" }],
572+ * attachments : [{ uri: "https://...", description: "Satellite data" }],
573573 * onProgress: console.log,
574574 * });
575575 * ```
@@ -621,10 +621,14 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
621621 result . hypercertUri = hypercertUri ;
622622 result . hypercertCid = hypercertCid ;
623623
624- // Step 6: Add evidence records if provided
625- if ( params . evidence && params . evidence . length > 0 ) {
624+ // Step 6: Add attachment records if provided
625+ if ( params . attachments && params . attachments . length > 0 ) {
626626 try {
627- result . evidenceUris = await this . createEvidenceWithProgress ( hypercertUri , params . evidence , params . onProgress ) ;
627+ result . attachmentUris = await this . createAttachmentsWithProgress (
628+ hypercertUri ,
629+ params . attachments ,
630+ params . onProgress ,
631+ ) ;
628632 } catch {
629633 // Error already logged and progress emitted
630634 }
@@ -1020,7 +1024,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
10201024 }
10211025
10221026 /**
1023- * Check if an AttachLocationParams is the object form (not a StrongRef or string).
1027+ * Check if an AttachLocationParams is the object form (not a StrongRef or string or Blob ).
10241028 * @internal
10251029 */
10261030 private isLocationObject ( location : LocationParams ) : location is CreateLocationParams {
@@ -1029,7 +1033,8 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
10291033 ! ( "uri" in location ) &&
10301034 ! ( "cid" in location ) &&
10311035 location !== null &&
1032- ! Array . isArray ( location )
1036+ ! Array . isArray ( location ) &&
1037+ ! ( location instanceof Blob )
10331038 ) ;
10341039 }
10351040
@@ -1101,62 +1106,136 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
11011106 }
11021107
11031108 /**
1104- * TODO: Match Attachment Lexicon
1105- * Adds evidence to any subject via the subject ref .
1109+ * Resolves attachment subjects to an array of StrongRefs.
1110+ * Accepts single or multiple subjects and normalizes to StrongRef array .
11061111 *
1107- * @param evidence - HypercertEvidenceInput
1108- * @returns Promise resolving to update result
1112+ * @param subjectsInput - Single subject or array of subjects (URI strings or StrongRefs)
1113+ * @returns Promise resolving to array of StrongRefs
1114+ * @throws {@link ValidationError } if subject format is invalid
1115+ * @throws {@link NetworkError } if fetching subject record fails
1116+ * @internal
1117+ */
1118+ private async resolveAttachmentSubjects (
1119+ subjectsInput : string | StrongRef | Array < string | StrongRef > ,
1120+ ) : Promise < StrongRef [ ] > {
1121+ const subjectsArray = Array . isArray ( subjectsInput ) ? subjectsInput : [ subjectsInput ] ;
1122+
1123+ return await Promise . all ( subjectsArray . map ( ( subject ) => this . resolveToStrongRef ( subject ) ) ) ;
1124+ }
1125+
1126+ /**
1127+ * Resolves attachment content items to an array of URI or Blob references.
1128+ * Accepts single or multiple content items and normalizes to array.
1129+ * Uploads Blob content and formats URI content.
1130+ *
1131+ * @param contentInput - Single content item or array (URI strings or Blobs)
1132+ * @returns Promise resolving to array of URI refs or Blob refs
1133+ * @throws {@link NetworkError } if blob upload fails
1134+ * @internal
1135+ */
1136+ private async resolveAttachmentContent ( contentInput : string | Blob | Array < string | Blob > ) {
1137+ const contentArray = Array . isArray ( contentInput ) ? contentInput : [ contentInput ] ;
1138+
1139+ return await Promise . all ( contentArray . map ( ( item ) => this . resolveUriOrBlob ( item , "application/octet-stream" ) ) ) ;
1140+ }
1141+
1142+ /**
1143+ * Builds an attachment record from resolved components.
1144+ *
1145+ * @param subjects - Resolved subject StrongRefs
1146+ * @param content - Resolved content items (URI or Blob refs)
1147+ * @param locationRef - Optional resolved location StrongRef
1148+ * @param rest - Remaining attachment parameters (title, description, etc.)
1149+ * @returns Fully constructed attachment record
1150+ * @internal
1151+ */
1152+ private buildAttachmentRecord (
1153+ subjects : StrongRef [ ] ,
1154+ content : Awaited < ReturnType < typeof this . resolveAttachmentContent > > ,
1155+ locationRef : StrongRef | undefined ,
1156+ rest : Omit < CreateAttachmentParams , "subjects" | "content" | "location" > ,
1157+ ) : HypercertAttachment {
1158+ const createdAt = new Date ( ) . toISOString ( ) ;
1159+
1160+ return {
1161+ ...rest ,
1162+ $type : HYPERCERT_COLLECTIONS . ATTACHMENT ,
1163+ createdAt,
1164+ subjects,
1165+ content,
1166+ ...( locationRef && { location : locationRef } ) ,
1167+ } as HypercertAttachment ;
1168+ }
1169+
1170+ /**
1171+ * Adds an attachment to any subject record.
1172+ *
1173+ * Attachments provide commentary, context, evidence, or documentary material
1174+ * related to hypercert records.
1175+ *
1176+ * @param attachment - Attachment parameters
1177+ * @returns Promise resolving to attachment record URI and CID
11091178 * @throws {@link ValidationError } if validation fails
11101179 * @throws {@link NetworkError } if the operation fails
11111180 *
1112- * @example
1181+ * @example Single subject with URI content
1182+ * ```typescript
1183+ * await repo.hypercerts.addAttachment({
1184+ * subjects: "at://did:plc:u7h3dstby64di67bxaotzxcz/org.hypercerts.claim.activity/3mbvv5d7ixh2g",
1185+ * content: "https://example.com/report.pdf",
1186+ * title: "Impact Report",
1187+ * contentType: "report"
1188+ * });
1189+ * ```
1190+ *
1191+ * @example Multiple subjects with mixed content
11131192 * ```typescript
1114- * await repo.hypercerts.addEvidence({
1115- * subjectUri: "at://did:plc:u7h3dstby64di67bxaotzxcz/org.hypercerts.claim.activity/3mbvv5d7ixh2g"
1116- * content: Blob,
1117- * title: "Meeting Notes",
1118- * shortDescription: "Meetings notes from the 3rd of December 2025",
1119- * description: "The meeting with the board of directors and audience on 2025 in regards to the ecological landscape",
1120- * relationType: "supports",
1121- * })
1193+ * await repo.hypercerts.addAttachment({
1194+ * subjects: [
1195+ * "at://did:plc:abc/org.hypercerts.claim.activity/xyz",
1196+ * { uri: "at://...", cid: "..." }
1197+ * ],
1198+ * content: [
1199+ * "https://example.com/report.pdf",
1200+ * new Blob(["data"], { type: "application/pdf" })
1201+ * ],
1202+ * title: "Multi-source Evidence",
1203+ * location: { uri: "at://...", cid: "..." }
1204+ * });
11221205 * ```
11231206 */
1124- async addEvidence ( evidence : CreateHypercertEvidenceParams ) : Promise < UpdateResult > {
1207+ async addAttachment ( attachment : CreateAttachmentParams ) : Promise < UpdateResult > {
11251208 try {
1126- const { subjectUri, content, ...rest } = evidence ;
1127- const subject = await this . get ( subjectUri ) ;
1128- const createdAt = new Date ( ) . toISOString ( ) ;
1209+ const { subjects : subjectsInput , content : contentInput , location : locationInput , ...rest } = attachment ;
11291210
1130- const evidenceContent = await this . resolveUriOrBlob ( content , "application/octet-stream" ) ;
1131- // Note: In beta.13, evidence was renamed to attachment with schema changes
1132- // - subject -> subjects (array)
1133- // - content is now an array
1134- // This is a temporary fix to maintain backward compatibility
1135- // and since evidence is no longer available in the lexicons
1136- const evidenceRecord : HypercertAttachment = {
1137- ...rest ,
1138- $type : HYPERCERT_COLLECTIONS . ATTACHMENT ,
1139- createdAt,
1140- content : [ evidenceContent ] , // content is now an array
1141- subjects : [ { uri : subject . uri , cid : subject . cid } ] , // subject -> subjects array
1142- } ;
1143- const validation = validate ( evidenceRecord , HYPERCERT_COLLECTIONS . ATTACHMENT , "main" , false ) ;
1211+ if ( ! contentInput ) {
1212+ throw new ValidationError ( "content is required for attachments" ) ;
1213+ }
1214+ const [ subjects , content , locationRef ] = await Promise . all ( [
1215+ this . resolveAttachmentSubjects ( subjectsInput ) ,
1216+ this . resolveAttachmentContent ( contentInput ) ,
1217+ locationInput ? this . resolveLocation ( locationInput ) : Promise . resolve ( undefined ) ,
1218+ ] ) ;
1219+ const attachmentRecord = this . buildAttachmentRecord ( subjects , content , locationRef , rest ) ;
1220+ const validation = validate ( attachmentRecord , HYPERCERT_COLLECTIONS . ATTACHMENT , "main" , false ) ;
11441221 if ( ! validation . success ) {
1145- throw new ValidationError ( `Invalid evidence record: ${ validation . error ?. message } ` ) ;
1222+ throw new ValidationError ( `Invalid attachment record: ${ validation . error ?. message } ` ) ;
11461223 }
11471224 const result = await this . agent . com . atproto . repo . createRecord ( {
11481225 repo : this . repoDid ,
1149- collection : HYPERCERT_COLLECTIONS . EVIDENCE ,
1150- record : evidenceRecord ,
1226+ collection : HYPERCERT_COLLECTIONS . ATTACHMENT ,
1227+ record : attachmentRecord ,
11511228 } ) ;
1229+
11521230 if ( ! result . success ) {
1153- throw new NetworkError ( `Failed to add evidence ` ) ;
1231+ throw new NetworkError ( `Failed to add attachment ` ) ;
11541232 }
1155- this . emit ( "evidenceAdded" , { uri : result . data . uri , cid : result . data . cid } ) ;
1233+ this . emit ( "attachmentAdded" , { uri : result . data . uri , cid : result . data . cid } ) ;
1234+
11561235 return { uri : result . data . uri , cid : result . data . cid } ;
11571236 } catch ( error ) {
11581237 if ( error instanceof ValidationError || error instanceof NetworkError ) throw error ;
1159- throw new NetworkError ( `Failed to add evidence : ${ error instanceof Error ? error . message : "Unknown" } ` , error ) ;
1238+ throw new NetworkError ( `Failed to add attachment : ${ error instanceof Error ? error . message : "Unknown" } ` , error ) ;
11601239 }
11611240 }
11621241
0 commit comments