Skip to content

Commit e1ea5a1

Browse files
authored
Merge pull request #108 from hypercerts-org/activity-claim-rkey
2 parents 3b9e014 + fb29a89 commit e1ea5a1

9 files changed

Lines changed: 548 additions & 109 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
"@hypercerts-org/sdk-core": minor
3+
---
4+
5+
Support multiple locations for hypercert activity claims
6+
7+
**Breaking Changes:**
8+
9+
- `CreateHypercertParams.location` is now `CreateHypercertParams.locations` (plural, array)
10+
- `CreateHypercertResult.locationUri` is now `CreateHypercertResult.locationUris` (plural, array)
11+
- `CreateHypercertResult.locationCid` is now `CreateHypercertResult.locationCids` (plural, array)
12+
13+
**New Functionality:**
14+
15+
- Hypercerts can now have multiple locations to support activities spanning multiple places
16+
- Each location can be a StrongRef, string URI, or location object
17+
- `attachLocation()` now appends to existing locations array instead of replacing
18+
19+
**Migration:**
20+
21+
```typescript
22+
// Before (v0.10.0-beta.5 and earlier)
23+
await repo.hypercerts.create({
24+
...params,
25+
location: {
26+
lpVersion: "1.0.0",
27+
srs: "EPSG:4326",
28+
locationType: "coordinate-decimal",
29+
location: "https://example.com/location",
30+
},
31+
});
32+
33+
// After (v0.10.0-beta.6+)
34+
await repo.hypercerts.create({
35+
...params,
36+
locations: [
37+
{
38+
lpVersion: "1.0.0",
39+
srs: "EPSG:4326",
40+
locationType: "coordinate-decimal",
41+
location: "https://example.com/location",
42+
},
43+
],
44+
});
45+
46+
// Now supports multiple locations
47+
await repo.hypercerts.create({
48+
...params,
49+
locations: [
50+
{ location: "https://example.com/location1", ... },
51+
{ location: "https://example.com/location2", ... },
52+
],
53+
});
54+
```

.changeset/wise-papayas-sing.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@hypercerts-org/sdk-core": minor
3+
---
4+
5+
feat: implement pre-generation of rKeys for activity claims using deterministic content hashing (SHA-256).
6+
7+
fix: normalize hashInput in `createHypercertRecord()` to use resolved StrongRefs (`locationRef`, `contributorsData`)
8+
instead of raw params which may contain non-serializable Blobs or inconsistent formats, ensuring stable rKey generation.

packages/sdk-core/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ const claim = await repo.hypercerts.create({
4141
shortDescription: "1000 trees planted in rainforest",
4242
description: "Planted 1000 trees in the Amazon rainforest region",
4343
workScope: "Environmental Conservation",
44-
workTimeFrameFrom: "2025-01-01T00:00:00Z",
45-
workTimeFrameTo: "2025-12-31T23:59:59Z",
44+
startDate: "2025-01-01",
45+
endDate: "2025-12-31",
4646
rights: {
4747
name: "Attribution",
4848
type: "license",
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Crypto utilities for the SDK.
3+
*/
4+
5+
import { NetworkError, ValidationError } from "../core/errors.js";
6+
7+
/**
8+
* Deterministically stringifies an object by sorting keys recursively.
9+
* Handles deeply nested objects and null values correctly.
10+
*/
11+
export function stableStringify(obj: unknown): string | undefined {
12+
if (obj === undefined || typeof obj === "function" || typeof obj === "symbol") {
13+
return undefined;
14+
}
15+
if (obj === null || typeof obj !== "object") {
16+
return JSON.stringify(obj);
17+
}
18+
19+
if (Array.isArray(obj)) {
20+
return JSON.stringify(obj.map((item) => {
21+
const val = stableStringify(item);
22+
return val === undefined ? null : JSON.parse(val);
23+
}));
24+
}
25+
26+
const sortedKeys = Object.keys(obj as object).sort();
27+
const sortedObj: Record<string, unknown> = {};
28+
29+
for (const key of sortedKeys) {
30+
const value = (obj as Record<string, unknown>)[key];
31+
const str = stableStringify(value);
32+
// Skip undefined or non-serializable values
33+
if (str === undefined) continue;
34+
sortedObj[key] = JSON.parse(str);
35+
}
36+
37+
return JSON.stringify(sortedObj);
38+
}
39+
40+
/**
41+
* Computes the SHA-256 hash of a JSON-serializable object.
42+
* Returns the hash as a hexadecimal string.
43+
*
44+
* @param content - The content to hash (will be JSON serialized)
45+
* @returns The SHA-256 hash of the content
46+
* @throws {ValidationError} If content is not serializable (e.g. undefined, function, symbol)
47+
*/
48+
export async function sha256Hash(content: unknown): Promise<string> {
49+
// Use stable stringification to ensure deterministic output
50+
const jsonString = stableStringify(content);
51+
52+
if (jsonString === undefined) {
53+
throw new ValidationError(`Content illegal: not serializable (type: ${typeof content})`);
54+
}
55+
56+
const msgBuffer = new TextEncoder().encode(jsonString);
57+
58+
if (typeof crypto !== "undefined" && crypto.subtle) {
59+
// Browser / Modern Node.js
60+
const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
61+
const hashArray = Array.from(new Uint8Array(hashBuffer));
62+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
63+
} else {
64+
// Fallback for older environments or specific setups if global crypto isn't available
65+
try {
66+
// Dynamic import to avoid breaking browser builds if bundler doesn't handle it
67+
68+
const { createHash } = await import("node:crypto");
69+
const hash = createHash("sha256").update(jsonString).digest("hex");
70+
return hash;
71+
} catch (e) {
72+
throw new NetworkError("SHA-256 hashing not supported in this environment", e);
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)