Skip to content
Merged
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
14 changes: 14 additions & 0 deletions .changeset/fix-missing-type-field-in-records.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@hypercerts-org/sdk-core": patch
---

fix(sdk-core): add required $type field to all record creation operations

The AT Protocol requires all records to include a `$type` field, but the SDK was omitting it during record creation, causing validation errors like "Record/$type must be a string". This fix:

- Adds `$type` field to all record types (rights, claims, locations, contributions, measurements, evaluations, collections)
- Fixes location record implementation to match `app.certified.location` lexicon schema
- Makes `srs` (Spatial Reference System) field required for location records with proper validation
- Updates interfaces and documentation to reflect required fields

Breaking change: `location.srs` is now required when creating locations (use "EPSG:4326" for standard WGS84 coordinates).
2 changes: 1 addition & 1 deletion packages/lexicon/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hypercerts-org/lexicon",
"version": "0.6.0",
"version": "0.7.0",
"description": "ATProto lexicon definitions and TypeScript types for the Hypercerts protocol",
"type": "module",
"main": "dist/index.cjs",
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hypercerts-org/sdk-core",
"version": "0.6.0",
"version": "0.7.0",
"description": "Framework-agnostic ATProto SDK core for authentication, repository operations, and lexicon management",
"main": "dist/index.cjs",
"repository": {
Expand Down
56 changes: 44 additions & 12 deletions packages/sdk-core/src/repository/HypercertOperationsImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple

// Step 2: Create rights record
this.emitProgress(params.onProgress, { name: "createRights", status: "start" });
const rightsRecord: Omit<HypercertRights, "$type"> = {
const rightsRecord: HypercertRights = {
$type: HYPERCERT_COLLECTIONS.RIGHTS,
rightsName: params.rights.name,
rightsType: params.rights.type,
rightsDescription: params.rights.description,
Expand Down Expand Up @@ -263,6 +264,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
// Step 3: Create hypercert record
this.emitProgress(params.onProgress, { name: "createHypercert", status: "start" });
const hypercertRecord: Record<string, unknown> = {
$type: HYPERCERT_COLLECTIONS.CLAIM,
title: params.title,
description: params.description,
workScope: params.workScope,
Expand Down Expand Up @@ -641,6 +643,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
* await repo.hypercerts.attachLocation(hypercertUri, {
* value: "San Francisco, CA",
* name: "SF Bay Area",
* srs: "EPSG:4326",
* });
* ```
*
Expand All @@ -662,34 +665,58 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
location: { value: string; name?: string; description?: string; srs?: string; geojson?: Blob },
): Promise<CreateResult> {
try {
// Get hypercert to get CID
const hypercert = await this.get(hypercertUri);
// 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.",
);
}

// Validate that hypercert exists (unused but confirms hypercert is valid)
await this.get(hypercertUri);
const createdAt = new Date().toISOString();

let locationValue: string | BlobRef = location.value;
// Determine location type and prepare location data
let locationData: { $type: string; uri: string } | BlobRef;
let locationType: string;

if (location.geojson) {
// Upload GeoJSON as a blob
const arrayBuffer = await location.geojson.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
encoding: location.geojson.type || "application/geo+json",
});
if (uploadResult.success) {
locationValue = {
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");
}
} else {
// Use value as a URI reference
locationData = {
$type: "app.certified.defs#uri",
uri: location.value,
};
locationType = "coordinate-decimal";
}

const locationRecord: Omit<HypercertLocation, "$type"> = {
hypercert: { uri: hypercert.uri, cid: hypercert.cid },
value: locationValue,
// Build location record according to app.certified.location lexicon
const locationRecord: HypercertLocation = {
$type: HYPERCERT_COLLECTIONS.LOCATION,
lpVersion: "1.0",
srs: location.srs,
locationType,
location: locationData,
createdAt,
name: location.name,
description: location.description,
srs: location.srs,
};

const validation = this.lexiconRegistry.validate(HYPERCERT_COLLECTIONS.LOCATION, locationRecord);
Expand Down Expand Up @@ -784,11 +811,13 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
}): Promise<CreateResult> {
try {
const createdAt = new Date().toISOString();
const contributionRecord: Omit<HypercertContribution, "$type"> = {
const contributionRecord: HypercertContribution = {
$type: HYPERCERT_COLLECTIONS.CONTRIBUTION,
contributors: params.contributors,
role: params.role,
createdAt,
description: params.description,
hypercert: { uri: "", cid: "" }, // Will be set below if hypercertUri provided
};

if (params.hypercertUri) {
Expand Down Expand Up @@ -863,7 +892,8 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
const hypercert = await this.get(params.hypercertUri);
const createdAt = new Date().toISOString();

const measurementRecord: Omit<HypercertMeasurement, "$type"> = {
const measurementRecord: HypercertMeasurement = {
$type: HYPERCERT_COLLECTIONS.MEASUREMENT,
hypercert: { uri: hypercert.uri, cid: hypercert.cid },
measurers: params.measurers,
metric: params.metric,
Expand Down Expand Up @@ -922,7 +952,8 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
const subject = await this.get(params.subjectUri);
const createdAt = new Date().toISOString();

const evaluationRecord: Omit<HypercertEvaluation, "$type"> = {
const evaluationRecord: HypercertEvaluation = {
$type: HYPERCERT_COLLECTIONS.EVALUATION,
subject: { uri: subject.uri, cid: subject.cid },
evaluators: params.evaluators,
summary: params.summary,
Expand Down Expand Up @@ -1007,6 +1038,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
}

const collectionRecord: Record<string, unknown> = {
$type: HYPERCERT_COLLECTIONS.COLLECTION,
title: params.title,
claims: params.claims.map((c) => ({ claim: { uri: c.uri, cid: c.cid }, weight: c.weight })),
createdAt,
Expand Down
11 changes: 8 additions & 3 deletions packages/sdk-core/src/repository/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,11 @@ export interface CreateHypercertParams {
description?: string;

/**
* Spatial Reference System identifier.
* Spatial Reference System identifier (required if location is provided).
*
* @example "EPSG:4326" for WGS84
*/
srs?: string;
srs: string;

/**
* GeoJSON file as a Blob for precise boundaries.
Expand Down Expand Up @@ -663,6 +663,11 @@ export interface HypercertOperations extends EventEmitter<HypercertEvents> {
*
* @param uri - AT-URI of the hypercert
* @param location - Location data
* @param location.value - Location value (address, coordinates, or description)
* @param location.srs - Spatial Reference System (required). Use 'EPSG:4326' for WGS84 lat/lon coordinates.
* @param location.name - Optional human-readable location name
* @param location.description - Optional description of the location
* @param location.geojson - Optional GeoJSON blob for precise boundaries
* @returns Promise resolving to location record result
*/
attachLocation(
Expand All @@ -671,7 +676,7 @@ export interface HypercertOperations extends EventEmitter<HypercertEvents> {
value: string;
name?: string;
description?: string;
srs?: string;
srs: string;
geojson?: Blob;
},
): Promise<CreateResult>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ describe("HypercertOperationsImpl", () => {

const result = await hypercertOps.create({
...validParams,
location: { value: "New York, NY" },
location: { value: "New York, NY", srs: "EPSG:4326" },
});

expect(result.locationUri).toBe("at://did:plc:test/org.hypercerts.claim.location/ghi");
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hypercerts-org/sdk-react",
"version": "0.6.0",
"version": "0.7.0",
"description": "React hooks and components for the Hypercerts ATProto SDK",
"type": "module",
"main": "dist/index.cjs",
Expand Down