Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fix-add-evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hypercerts-org/sdk-core": minor
---

Update the body of addEvidence parameters so that $type and uri and subject is constructed in the sdk. Evidence is also
saved as a record on its own now instead of it being added to the hypercert.
4 changes: 4 additions & 0 deletions packages/sdk-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type {
RepositoryAccessGrant,
OrganizationInfo,
ProgressStep,
HypercertEvidenceInput,
} from "./repository/types.js";
export type {
RecordOperations,
Expand All @@ -36,6 +37,9 @@ export type {
OrganizationOperations,
CreateHypercertParams,
CreateHypercertResult,
AttachLocationParams,
CreateHypercertEvidenceParams,
CreateOrganizationParams,
} from "./repository/interfaces.js";

// ============================================================================
Expand Down
154 changes: 90 additions & 64 deletions packages/sdk-core/src/repository/HypercertOperationsImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,31 @@
*/

import type { Agent } from "@atproto/api";
import { validate } from "@hypercerts-org/lexicon";
import { EventEmitter } from "eventemitter3";
import { NetworkError, ValidationError } from "../core/errors.js";
import type { LoggerInterface } from "../core/interfaces.js";
import { validate } from "@hypercerts-org/lexicon";
import {
HYPERCERT_COLLECTIONS,
type JsonBlobRef,
type HypercertEvidence,
type HypercertClaim,
type HypercertRights,
type HypercertCollection,
type HypercertContribution,
type HypercertMeasurement,
type HypercertEvaluation,
type HypercertCollection,
type HypercertEvidence,
type HypercertLocation,
type HypercertMeasurement,
type HypercertRights,
type JsonBlobRef,
} from "../services/hypercerts/types.js";
import type {
HypercertOperations,
HypercertEvents,
AttachLocationParams,
CreateHypercertEvidenceParams,
CreateHypercertParams,
CreateHypercertResult,
HypercertEvents,
HypercertOperations,
} from "./interfaces.js";
import type { CreateResult, UpdateResult, PaginatedList, ListParams, ProgressStep } from "./types.js";
import type { CreateResult, ListParams, PaginatedList, ProgressStep, UpdateResult } from "./types.js";

/**
* Implementation of high-level hypercert operations.
Expand Down Expand Up @@ -291,7 +293,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
*/
private async attachLocationWithProgress(
hypercertUri: string,
location: { value: string; name?: string; description?: string; srs?: string; geojson?: Blob },
location: AttachLocationParams,
onProgress?: (step: ProgressStep) => void,
): Promise<string> {
this.emitProgress(onProgress, { name: "attachLocation", status: "start" });
Expand Down Expand Up @@ -773,12 +775,8 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
* });
* ```
*/
async attachLocation(
hypercertUri: string,
location: { value: string; name?: string; description?: string; srs?: string; geojson?: Blob },
): Promise<CreateResult> {
async attachLocation(hypercertUri: string, location: AttachLocationParams): Promise<CreateResult> {
try {
// Validate required srs field
if (!location.srs) {
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.",
Expand All @@ -789,43 +787,34 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
await this.get(hypercertUri);
const createdAt = new Date().toISOString();

// Determine location type and prepare location data
let locationData: { $type: string; uri: string } | JsonBlobRef;
let locationType: string;
let locationData: HypercertLocation["location"];
const content = location.location;

if (location.geojson) {
// Upload GeoJSON as a blob
const arrayBuffer = await location.geojson.arrayBuffer();
if (typeof content === "string") {
locationData = {
$type: "org.hypercerts.defs#uri",
uri: content,
};
} else {
const arrayBuffer = await content.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
encoding: location.geojson.type || "application/geo+json",
encoding: content.type || "application/geo+json",
});
if (uploadResult.success) {
locationData = {
$type: "blob",
ref: { $link: uploadResult.data.blob.ref.toString() },
mimeType: uploadResult.data.blob.mimeType,
size: uploadResult.data.blob.size,
};
locationType = "geojson-point";
} else {
throw new NetworkError("Failed to upload GeoJSON blob");
if (!uploadResult.success) {
throw new NetworkError("Failed to upload location blob");
}
} else {
// Use value as a URI reference
locationData = {
$type: "org.hypercerts.defs#uri",
uri: location.value,
$type: "org.hypercerts.defs#smallBlob",
blob: uploadResult.data.blob,
};
locationType = "coordinate-decimal";
}

// Build location record according to app.certified.location lexicon
const locationRecord: HypercertLocation = {
$type: HYPERCERT_COLLECTIONS.LOCATION,
lpVersion: "1.0",
srs: location.srs,
locationType,
locationType: location.locationType,
location: locationData,
createdAt,
name: location.name,
Expand All @@ -840,13 +829,24 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
const result = await this.agent.com.atproto.repo.createRecord({
repo: this.repoDid,
collection: HYPERCERT_COLLECTIONS.LOCATION,
record: locationRecord as Record<string, unknown>,
record: locationRecord,
});

if (!result.success) {
throw new NetworkError("Failed to attach location");
}

await this.update({
uri: hypercertUri,
updates: {
location: {
$type: "com.atproto.repo.strongRef",
uri: result.data.uri,
cid: result.data.cid,
},
},
});

this.emit("locationAttached", { uri: result.data.uri, cid: result.data.cid, hypercertUri });
return { uri: result.data.uri, cid: result.data.cid };
} catch (error) {
Expand All @@ -856,38 +856,64 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
}

/**
* Adds evidence to an existing hypercert.
* Adds evidence to any subject via the subject ref.
*
* @param hypercertUri - AT-URI of the hypercert
* @param evidence - Array of evidence items to add
* @param evidence - HypercertEvidenceInput
* @returns Promise resolving to update result
* @throws {@link ValidationError} if validation fails
* @throws {@link NetworkError} if the operation fails
*
* @remarks
* Evidence is appended to existing evidence, not replaced.
*
* @example
* ```typescript
* await repo.hypercerts.addEvidence(hypercertUri, [
* { uri: "https://example.com/report.pdf", description: "Impact report" },
* { uri: "https://example.com/data.csv", description: "Raw data" },
* ]);
* ```
*/
async addEvidence(hypercertUri: string, evidence: HypercertEvidence[]): Promise<UpdateResult> {
async addEvidence(evidence: CreateHypercertEvidenceParams): Promise<UpdateResult> {
try {
const existing = await this.get(hypercertUri);
const existingEvidence = (existing.record.evidence as HypercertEvidence[]) || [];
const updatedEvidence = [...existingEvidence, ...evidence];
const { subjectUri, content, ...rest } = evidence;
const subject = await this.get(subjectUri);
const createdAt = new Date().toISOString();

const result = await this.update({
uri: hypercertUri,
updates: { evidence: updatedEvidence },
});
let evidenceContent: HypercertEvidence["content"];
if (typeof content === "string") {
evidenceContent = {
$type: "org.hypercerts.defs#uri",
uri: content,
};
} else {
// Handle Blob upload
const arrayBuffer = await content.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
encoding: content.type || "application/octet-stream",
});

this.emit("evidenceAdded", { uri: result.uri, cid: result.cid });
return result;
if (!uploadResult.success) {
throw new NetworkError("Failed to upload evidence blob");
}

evidenceContent = {
$type: "org.hypercerts.defs#smallBlob",
blob: uploadResult.data.blob,
};
}

const evidenceRecord: HypercertEvidence = {
...rest,
$type: HYPERCERT_COLLECTIONS.EVIDENCE,
createdAt,
content: evidenceContent,
subject: { uri: subject.uri, cid: subject.cid },
};
const validation = validate(evidenceRecord, HYPERCERT_COLLECTIONS.EVIDENCE, "main", false);
if (!validation.success) {
throw new ValidationError(`Invalid evidence record: ${validation.error?.message}`);
}
const result = await this.agent.com.atproto.repo.createRecord({
repo: this.repoDid,
collection: HYPERCERT_COLLECTIONS.EVIDENCE,
record: evidenceRecord,
});
if (!result.success) {
throw new NetworkError(`Failed to add evidence`);
}
this.emit("evidenceAdded", { uri: result.data.uri, cid: result.data.cid });
return { uri: result.data.uri, cid: result.data.cid };
} catch (error) {
if (error instanceof ValidationError || error instanceof NetworkError) throw error;
throw new NetworkError(`Failed to add evidence: ${error instanceof Error ? error.message : "Unknown"}`, error);
Expand Down
Loading