From 4b80edca4162c4ce929edb28ffffa3f99f21cb74 Mon Sep 17 00:00:00 2001 From: bitbeckers Date: Thu, 4 Dec 2025 23:55:09 +0100 Subject: [PATCH] fix(sdk-core): add required $type field to all record creation operations - Add $type field to rights, claims, locations, contributions, measurements, evaluations, and collections - Fix location record implementation to match app.certified.location lexicon schema - Make srs field required for location records with validation error message - Update interfaces and documentation to reflect required fields Fixes validation error: 'Invalid rights record: Record/$type must be a string' BREAKING CHANGE: location.srs is now required when attaching locations (use 'EPSG:4326' for WGS84) --- .../fix-missing-type-field-in-records.md | 14 +++++ packages/lexicon/package.json | 2 +- packages/sdk-core/package.json | 2 +- .../src/repository/HypercertOperationsImpl.ts | 56 +++++++++++++++---- .../sdk-core/src/repository/interfaces.ts | 11 +++- .../HypercertOperationsImpl.test.ts | 2 +- packages/sdk-react/package.json | 2 +- 7 files changed, 70 insertions(+), 19 deletions(-) create mode 100644 .changeset/fix-missing-type-field-in-records.md diff --git a/.changeset/fix-missing-type-field-in-records.md b/.changeset/fix-missing-type-field-in-records.md new file mode 100644 index 0000000..79cf142 --- /dev/null +++ b/.changeset/fix-missing-type-field-in-records.md @@ -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). diff --git a/packages/lexicon/package.json b/packages/lexicon/package.json index b94484c..fca65e7 100644 --- a/packages/lexicon/package.json +++ b/packages/lexicon/package.json @@ -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", diff --git a/packages/sdk-core/package.json b/packages/sdk-core/package.json index 8004aaf..ad3ace5 100644 --- a/packages/sdk-core/package.json +++ b/packages/sdk-core/package.json @@ -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": { diff --git a/packages/sdk-core/src/repository/HypercertOperationsImpl.ts b/packages/sdk-core/src/repository/HypercertOperationsImpl.ts index d95df9c..68d8b96 100644 --- a/packages/sdk-core/src/repository/HypercertOperationsImpl.ts +++ b/packages/sdk-core/src/repository/HypercertOperationsImpl.ts @@ -229,7 +229,8 @@ export class HypercertOperationsImpl extends EventEmitter imple // Step 2: Create rights record this.emitProgress(params.onProgress, { name: "createRights", status: "start" }); - const rightsRecord: Omit = { + const rightsRecord: HypercertRights = { + $type: HYPERCERT_COLLECTIONS.RIGHTS, rightsName: params.rights.name, rightsType: params.rights.type, rightsDescription: params.rights.description, @@ -263,6 +264,7 @@ export class HypercertOperationsImpl extends EventEmitter imple // Step 3: Create hypercert record this.emitProgress(params.onProgress, { name: "createHypercert", status: "start" }); const hypercertRecord: Record = { + $type: HYPERCERT_COLLECTIONS.CLAIM, title: params.title, description: params.description, workScope: params.workScope, @@ -641,6 +643,7 @@ export class HypercertOperationsImpl extends EventEmitter imple * await repo.hypercerts.attachLocation(hypercertUri, { * value: "San Francisco, CA", * name: "SF Bay Area", + * srs: "EPSG:4326", * }); * ``` * @@ -662,34 +665,58 @@ export class HypercertOperationsImpl extends EventEmitter imple location: { value: string; name?: string; description?: string; srs?: string; geojson?: Blob }, ): Promise { 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 = { - 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); @@ -784,11 +811,13 @@ export class HypercertOperationsImpl extends EventEmitter imple }): Promise { try { const createdAt = new Date().toISOString(); - const contributionRecord: Omit = { + 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) { @@ -863,7 +892,8 @@ export class HypercertOperationsImpl extends EventEmitter imple const hypercert = await this.get(params.hypercertUri); const createdAt = new Date().toISOString(); - const measurementRecord: Omit = { + const measurementRecord: HypercertMeasurement = { + $type: HYPERCERT_COLLECTIONS.MEASUREMENT, hypercert: { uri: hypercert.uri, cid: hypercert.cid }, measurers: params.measurers, metric: params.metric, @@ -922,7 +952,8 @@ export class HypercertOperationsImpl extends EventEmitter imple const subject = await this.get(params.subjectUri); const createdAt = new Date().toISOString(); - const evaluationRecord: Omit = { + const evaluationRecord: HypercertEvaluation = { + $type: HYPERCERT_COLLECTIONS.EVALUATION, subject: { uri: subject.uri, cid: subject.cid }, evaluators: params.evaluators, summary: params.summary, @@ -1007,6 +1038,7 @@ export class HypercertOperationsImpl extends EventEmitter imple } const collectionRecord: Record = { + $type: HYPERCERT_COLLECTIONS.COLLECTION, title: params.title, claims: params.claims.map((c) => ({ claim: { uri: c.uri, cid: c.cid }, weight: c.weight })), createdAt, diff --git a/packages/sdk-core/src/repository/interfaces.ts b/packages/sdk-core/src/repository/interfaces.ts index 35fe070..4d00692 100644 --- a/packages/sdk-core/src/repository/interfaces.ts +++ b/packages/sdk-core/src/repository/interfaces.ts @@ -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. @@ -663,6 +663,11 @@ export interface HypercertOperations extends EventEmitter { * * @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( @@ -671,7 +676,7 @@ export interface HypercertOperations extends EventEmitter { value: string; name?: string; description?: string; - srs?: string; + srs: string; geojson?: Blob; }, ): Promise; diff --git a/packages/sdk-core/tests/repository/HypercertOperationsImpl.test.ts b/packages/sdk-core/tests/repository/HypercertOperationsImpl.test.ts index f5dd809..a74e2af 100644 --- a/packages/sdk-core/tests/repository/HypercertOperationsImpl.test.ts +++ b/packages/sdk-core/tests/repository/HypercertOperationsImpl.test.ts @@ -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"); diff --git a/packages/sdk-react/package.json b/packages/sdk-react/package.json index b8c5002..ce0c66c 100644 --- a/packages/sdk-react/package.json +++ b/packages/sdk-react/package.json @@ -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",