Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
54 changes: 54 additions & 0 deletions .changeset/support-multiple-locations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
"@hypercerts-org/sdk-core": minor
---

Support multiple locations for hypercert activity claims

**Breaking Changes:**

- `CreateHypercertParams.location` is now `CreateHypercertParams.locations` (plural, array)
- `CreateHypercertResult.locationUri` is now `CreateHypercertResult.locationUris` (plural, array)
- `CreateHypercertResult.locationCid` is now `CreateHypercertResult.locationCids` (plural, array)

**New Functionality:**

- Hypercerts can now have multiple locations to support activities spanning multiple places
- Each location can be a StrongRef, string URI, or location object
- `attachLocation()` now appends to existing locations array instead of replacing

**Migration:**

```typescript
// Before (v0.10.0-beta.5 and earlier)
await repo.hypercerts.create({
...params,
location: {
lpVersion: "1.0.0",
srs: "EPSG:4326",
locationType: "coordinate-decimal",
location: "https://example.com/location",
},
});

// After (v0.10.0-beta.6+)
await repo.hypercerts.create({
...params,
locations: [
{
lpVersion: "1.0.0",
srs: "EPSG:4326",
locationType: "coordinate-decimal",
location: "https://example.com/location",
},
],
});

// Now supports multiple locations
await repo.hypercerts.create({
...params,
locations: [
{ location: "https://example.com/location1", ... },
{ location: "https://example.com/location2", ... },
],
});
```
8 changes: 8 additions & 0 deletions .changeset/wise-papayas-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@hypercerts-org/sdk-core": minor
---

feat: implement pre-generation of rKeys for activity claims using deterministic content hashing (SHA-256).
Comment on lines +1 to +5
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a 0.x breaking-change note to the changeset.
Since @hypercerts-org/sdk-core is still 0.x, this behavioral change (deterministic rKeys replacing server-assigned keys) can be breaking for clients depending on random rKeys or duplicate-creation behavior. Please call that out explicitly. Based on learnings.

✍️ Suggested wording update
 feat: implement pre-generation of rKeys for activity claims using deterministic content hashing (SHA-256).
+Note: `@hypercerts-org/sdk-core` is 0.x; this changes rKey generation from server-assigned keys to deterministic `hc:<hash>`, which may affect clients relying on random rKeys or duplicate-creation behavior.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
---
"@hypercerts-org/sdk-core": minor
---
feat: implement pre-generation of rKeys for activity claims using deterministic content hashing (SHA-256).
---
"@hypercerts-org/sdk-core": minor
---
feat: implement pre-generation of rKeys for activity claims using deterministic content hashing (SHA-256).
Note: `@hypercerts-org/sdk-core` is 0.x; this changes rKey generation from server-assigned keys to deterministic `hc:<hash>`, which may affect clients relying on random rKeys or duplicate-creation behavior.
🤖 Prompt for AI Agents
In @.changeset/wise-papayas-sing.md around lines 1 - 5, Update the changeset to
include a 0.x breaking-change note for "@hypercerts-org/sdk-core": add a section
under the frontmatter explaining that the new deterministic rKeys (SHA-256
content hashing) replace server-assigned random rKeys and may break clients
relying on random keys or duplicate-creation behavior; explicitly mark it as a
breaking change for 0.x consumers and include a short suggested message
describing the behavior change and migration advice.


fix: normalize hashInput in `createHypercertRecord()` to use resolved StrongRefs (`locationRef`, `contributorsData`)
instead of raw params which may contain non-serializable Blobs or inconsistent formats, ensuring stable rKey generation.
4 changes: 2 additions & 2 deletions packages/sdk-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ const claim = await repo.hypercerts.create({
shortDescription: "1000 trees planted in rainforest",
description: "Planted 1000 trees in the Amazon rainforest region",
workScope: "Environmental Conservation",
workTimeFrameFrom: "2025-01-01T00:00:00Z",
workTimeFrameTo: "2025-12-31T23:59:59Z",
startDate: "2025-01-01",
endDate: "2025-12-31",
rights: {
name: "Attribution",
type: "license",
Expand Down
75 changes: 75 additions & 0 deletions packages/sdk-core/src/lib/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Crypto utilities for the SDK.
*/

import { NetworkError, ValidationError } from "../core/errors.js";

/**
* Deterministically stringifies an object by sorting keys recursively.
* Handles deeply nested objects and null values correctly.
*/
export function stableStringify(obj: unknown): string | undefined {
if (obj === undefined || typeof obj === "function" || typeof obj === "symbol") {
return undefined;
}
if (obj === null || typeof obj !== "object") {
return JSON.stringify(obj);
}

if (Array.isArray(obj)) {
return JSON.stringify(obj.map((item) => {
const val = stableStringify(item);
return val === undefined ? null : JSON.parse(val);
}));
}

const sortedKeys = Object.keys(obj as object).sort();
const sortedObj: Record<string, unknown> = {};

for (const key of sortedKeys) {
const value = (obj as Record<string, unknown>)[key];
const str = stableStringify(value);
// Skip undefined or non-serializable values
if (str === undefined) continue;
sortedObj[key] = JSON.parse(str);
}

return JSON.stringify(sortedObj);
}

/**
* Computes the SHA-256 hash of a JSON-serializable object.
* Returns the hash as a hexadecimal string.
*
* @param content - The content to hash (will be JSON serialized)
* @returns The SHA-256 hash of the content
* @throws {ValidationError} If content is not serializable (e.g. undefined, function, symbol)
*/
export async function sha256Hash(content: unknown): Promise<string> {
// Use stable stringification to ensure deterministic output
const jsonString = stableStringify(content);

if (jsonString === undefined) {
throw new ValidationError(`Content illegal: not serializable (type: ${typeof content})`);
}

const msgBuffer = new TextEncoder().encode(jsonString);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (typeof crypto !== "undefined" && crypto.subtle) {
// Browser / Modern Node.js
const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
} else {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Fallback for older environments or specific setups if global crypto isn't available
try {
// Dynamic import to avoid breaking browser builds if bundler doesn't handle it

const { createHash } = await import("node:crypto");
const hash = createHash("sha256").update(jsonString).digest("hex");
return hash;
} catch (e) {
throw new NetworkError("SHA-256 hashing not supported in this environment", e);
}
}
}
Loading