@@ -22,6 +22,7 @@ import {
2222 type HypercertClaim ,
2323 type HypercertCollection ,
2424 type HypercertContributionDetails ,
25+ type HypercertContributorInformation ,
2526 type HypercertEvaluation ,
2627 type HypercertEvidence ,
2728 type HypercertLocation ,
@@ -43,6 +44,8 @@ import type {
4344 HypercertOperations ,
4445 ContributionDetailsParams ,
4546 ResolvedContributionDetails ,
47+ ContributorIdentityParams ,
48+ ResolvedContributorIdentity ,
4649} from "./interfaces.js" ;
4750import type { CreateResult , ListParams , PaginatedList , ProgressStep , UpdateResult } from "./types.js" ;
4851import { $Typed } from "@atproto/api" ;
@@ -271,7 +274,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
271274 locationRefs : Array < { uri : string ; cid : string } > | undefined ,
272275 contributorsData :
273276 | Array < {
274- contributorIdentity : string | { uri : string ; cid : string } ;
277+ contributorIdentity : ResolvedContributorIdentity ;
275278 contributionWeight ?: string ;
276279 contributionDetails ?: ResolvedContributionDetails ;
277280 } >
@@ -1161,7 +1164,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
11611164 }
11621165
11631166 /**
1164- * Processes contribution parameters, creating detailed contribution records if necessary.
1167+ * Processes contribution parameters, creating contributor/ contribution records if necessary.
11651168 *
11661169 * @param contributions - Array of contribution parameters
11671170 * @param onProgress - Optional progress callback
@@ -1171,15 +1174,15 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
11711174 private async processContributors (
11721175 contributions :
11731176 | Array < {
1174- contributors : Array < string | { uri : string ; cid : string } > ;
1177+ contributors : Array < ContributorIdentityParams > ;
11751178 contributionDetails : ContributionDetailsParams ;
11761179 weight ?: string ;
11771180 } >
11781181 | undefined ,
11791182 onProgress ?: ( step : ProgressStep ) => void ,
11801183 ) : Promise <
11811184 | Array < {
1182- contributorIdentity : string | { uri : string ; cid : string } ;
1185+ contributorIdentity : ResolvedContributorIdentity ;
11831186 contributionWeight ?: string ;
11841187 contributionDetails ?: ResolvedContributionDetails ;
11851188 } >
@@ -1188,55 +1191,16 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
11881191 if ( ! contributions || contributions . length === 0 ) return undefined ;
11891192
11901193 const contributorsPromises = contributions . map ( async ( contrib ) => {
1191- let detailsRef : ResolvedContributionDetails ;
1192- const details = contrib . contributionDetails ;
1193-
1194- // Determine the type of contributionDetails
1195- if ( typeof details === "string" ) {
1196- // Inline role string
1197- detailsRef = details ;
1198- } else if ( "uri" in details && "cid" in details && ! ( "role" in details ) ) {
1199- // StrongRef to existing record (has uri+cid but no role)
1200- detailsRef = { uri : details . uri as string , cid : details . cid as string } ;
1201- } else if ( "role" in details ) {
1202- // CreateContributionDetailsParams - auto-create record
1203- try {
1204- this . emitProgress ( onProgress , { name : "createContribution" , status : "start" } ) ;
1205- const { role, contributionDescription, startDate, endDate, ...extraProps } = details as {
1206- role : string ;
1207- contributionDescription ?: string ;
1208- startDate ?: string ;
1209- endDate ?: string ;
1210- [ key : string ] : unknown ;
1211- } ;
1212- const result = await this . addContribution ( {
1213- role,
1214- description : contributionDescription ,
1215- startDate,
1216- endDate,
1217- ...extraProps ,
1218- } ) ;
1219- detailsRef = { uri : result . uri , cid : result . cid } ;
1220- this . emitProgress ( onProgress , {
1221- name : "createContribution" ,
1222- status : "success" ,
1223- data : result ,
1224- } ) ;
1225- } catch ( error ) {
1226- this . emitProgress ( onProgress , {
1227- name : "createContribution" ,
1228- status : "error" ,
1229- error : error as Error ,
1230- } ) ;
1231- throw error ;
1232- }
1233- } else {
1234- // Fallback - shouldn't happen with proper types
1235- throw new Error ( "Invalid contributionDetails format" ) ;
1236- }
1194+ // Resolve contributionDetails
1195+ const detailsRef = await this . resolveContributionDetails ( contrib . contributionDetails , onProgress ) ;
12371196
1238- // Expand to one entry per contributor (DID string or StrongRef)
1239- return contrib . contributors . map ( ( identity ) => ( {
1197+ // Resolve each contributor identity
1198+ const resolvedContributors = await Promise . all (
1199+ contrib . contributors . map ( ( identity ) => this . resolveContributorIdentity ( identity , onProgress ) ) ,
1200+ ) ;
1201+
1202+ // Expand to one entry per contributor
1203+ return resolvedContributors . map ( ( identity ) => ( {
12401204 contributorIdentity : identity ,
12411205 contributionWeight : contrib . weight ,
12421206 contributionDetails : detailsRef ,
@@ -1247,6 +1211,116 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
12471211 return nestedContributors . flat ( ) ;
12481212 }
12491213
1214+ /**
1215+ * Resolves ContributionDetailsParams to a ResolvedContributionDetails.
1216+ * Creates a record if CreateContributionDetailsParams is provided.
1217+ * @internal
1218+ */
1219+ private async resolveContributionDetails (
1220+ details : ContributionDetailsParams ,
1221+ onProgress ?: ( step : ProgressStep ) => void ,
1222+ ) : Promise < ResolvedContributionDetails > {
1223+ if ( typeof details === "string" ) {
1224+ // Inline role string
1225+ return details ;
1226+ } else if ( "uri" in details && "cid" in details && ! ( "role" in details ) ) {
1227+ // StrongRef to existing record
1228+ return { uri : details . uri as string , cid : details . cid as string } ;
1229+ } else if ( "role" in details ) {
1230+ // CreateContributionDetailsParams - auto-create record
1231+ try {
1232+ this . emitProgress ( onProgress , { name : "createContribution" , status : "start" } ) ;
1233+ const { role, contributionDescription, startDate, endDate, ...extraProps } = details as {
1234+ role : string ;
1235+ contributionDescription ?: string ;
1236+ startDate ?: string ;
1237+ endDate ?: string ;
1238+ [ key : string ] : unknown ;
1239+ } ;
1240+ const result = await this . addContribution ( {
1241+ role,
1242+ description : contributionDescription ,
1243+ startDate,
1244+ endDate,
1245+ ...extraProps ,
1246+ } ) ;
1247+ this . emitProgress ( onProgress , {
1248+ name : "createContribution" ,
1249+ status : "success" ,
1250+ data : result ,
1251+ } ) ;
1252+ return { uri : result . uri , cid : result . cid } ;
1253+ } catch ( error ) {
1254+ this . emitProgress ( onProgress , {
1255+ name : "createContribution" ,
1256+ status : "error" ,
1257+ error : error as Error ,
1258+ } ) ;
1259+ throw error ;
1260+ }
1261+ }
1262+ throw new Error ( "Invalid contributionDetails format" ) ;
1263+ }
1264+
1265+ /**
1266+ * Resolves ContributorIdentityParams to a ResolvedContributorIdentity.
1267+ * Creates a contributorInformation record if CreateContributorInformationParams is provided.
1268+ * @internal
1269+ */
1270+ private async resolveContributorIdentity (
1271+ identity : ContributorIdentityParams ,
1272+ onProgress ?: ( step : ProgressStep ) => void ,
1273+ ) : Promise < ResolvedContributorIdentity > {
1274+ if ( typeof identity === "string" ) {
1275+ // DID or inline string
1276+ return identity ;
1277+ } else if ( "uri" in identity && "cid" in identity && ! ( "identifier" in identity ) ) {
1278+ // StrongRef to existing record
1279+ return { uri : identity . uri as string , cid : identity . cid as string } ;
1280+ } else if ( "identifier" in identity ) {
1281+ // CreateContributorInformationParams - auto-create record
1282+ try {
1283+ this . emitProgress ( onProgress , { name : "createContributorInformation" , status : "start" } ) ;
1284+ const { identifier, displayName, image, ...extraProps } = identity as {
1285+ identifier : string ;
1286+ displayName ?: string ;
1287+ image ?: string | Blob ;
1288+ [ key : string ] : unknown ;
1289+ } ;
1290+
1291+ // Handle image upload if it's a Blob
1292+ let imageRef : JsonBlobRef | string | undefined ;
1293+ if ( image instanceof Blob ) {
1294+ const uploadResult = await this . uploadImageBlob ( image , onProgress ) ;
1295+ imageRef = uploadResult ;
1296+ } else {
1297+ imageRef = image ;
1298+ }
1299+
1300+ const result = await this . addContributorInformation ( {
1301+ identifier,
1302+ displayName,
1303+ image : imageRef ,
1304+ ...extraProps ,
1305+ } ) ;
1306+ this . emitProgress ( onProgress , {
1307+ name : "createContributorInformation" ,
1308+ status : "success" ,
1309+ data : result ,
1310+ } ) ;
1311+ return { uri : result . uri , cid : result . cid } ;
1312+ } catch ( error ) {
1313+ this . emitProgress ( onProgress , {
1314+ name : "createContributorInformation" ,
1315+ status : "error" ,
1316+ error : error as Error ,
1317+ } ) ;
1318+ throw error ;
1319+ }
1320+ }
1321+ throw new Error ( "Invalid contributorIdentity format" ) ;
1322+ }
1323+
12501324 /**
12511325 * Creates a contribution details record.
12521326 *
@@ -1333,6 +1407,88 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
13331407 }
13341408 }
13351409
1410+ /**
1411+ * Creates a contributor information record.
1412+ *
1413+ * This creates a contributor profile record that can be referenced
1414+ * from an activity's `contributors` array via a strong reference.
1415+ *
1416+ * @param params - Contributor parameters
1417+ * @param params.identifier - DID or URI of the contributor
1418+ * @param params.displayName - Display name of the contributor
1419+ * @param params.image - Optional image URI or blob ref
1420+ * @returns Promise resolving to contributor information record URI and CID
1421+ * @throws {@link ValidationError } if validation fails
1422+ * @throws {@link NetworkError } if the operation fails
1423+ *
1424+ * @example
1425+ * ```typescript
1426+ * await repo.hypercerts.addContributorInformation({
1427+ * identifier: "did:plc:contributor123",
1428+ * displayName: "Alice",
1429+ * });
1430+ * ```
1431+ */
1432+ async addContributorInformation ( params : {
1433+ identifier : string ;
1434+ displayName ?: string ;
1435+ image ?: JsonBlobRef | string ;
1436+ [ key : string ] : unknown ;
1437+ } ) : Promise < CreateResult > {
1438+ try {
1439+ const createdAt = new Date ( ) . toISOString ( ) ;
1440+ const { identifier, displayName, image, ...extraProps } = params ;
1441+
1442+ // Resolve image to proper lexicon type if provided
1443+ let resolvedImage : HypercertContributorInformation [ "image" ] ;
1444+ if ( image ) {
1445+ if ( typeof image === "string" ) {
1446+ // URI string - wrap in typed object
1447+ resolvedImage = { $type : "org.hypercerts.defs#uri" , uri : image } as HypercertContributorInformation [ "image" ] ;
1448+ } else {
1449+ // JsonBlobRef from upload - wrap in smallImage
1450+ resolvedImage = {
1451+ $type : "org.hypercerts.defs#smallImage" ,
1452+ image : image ,
1453+ } as HypercertContributorInformation [ "image" ] ;
1454+ }
1455+ }
1456+
1457+ const contributorRecord = {
1458+ $type : HYPERCERT_COLLECTIONS . CONTRIBUTOR_INFORMATION ,
1459+ identifier,
1460+ displayName,
1461+ image : resolvedImage ,
1462+ createdAt,
1463+ ...extraProps ,
1464+ } as HypercertContributorInformation ;
1465+
1466+ const validation = validate ( contributorRecord , HYPERCERT_COLLECTIONS . CONTRIBUTOR_INFORMATION , "main" , false ) ;
1467+ if ( ! validation . success ) {
1468+ throw new ValidationError ( `Invalid contributor information record: ${ validation . error ?. message } ` ) ;
1469+ }
1470+
1471+ const result = await this . agent . com . atproto . repo . createRecord ( {
1472+ repo : this . repoDid ,
1473+ collection : HYPERCERT_COLLECTIONS . CONTRIBUTOR_INFORMATION ,
1474+ record : contributorRecord as Record < string , unknown > ,
1475+ } ) ;
1476+
1477+ if ( ! result . success ) {
1478+ throw new NetworkError ( "Failed to create contributor information" ) ;
1479+ }
1480+
1481+ this . emit ( "contributorCreated" , { uri : result . data . uri , cid : result . data . cid } ) ;
1482+ return { uri : result . data . uri , cid : result . data . cid } ;
1483+ } catch ( error ) {
1484+ if ( error instanceof ValidationError || error instanceof NetworkError ) throw error ;
1485+ throw new NetworkError (
1486+ `Failed to add contributor information: ${ error instanceof Error ? error . message : "Unknown" } ` ,
1487+ error ,
1488+ ) ;
1489+ }
1490+ }
1491+
13361492 /**
13371493 * Creates a measurement record for a hypercert.
13381494 *
0 commit comments