1- const PINATA_GATEWAY = 'https://gateway.pinata.cloud/ipfs'
1+ /**
2+ * Metadata storage client.
3+ * Uploads/fetches task metadata via the Cloudflare Worker + R2 API.
4+ * Falls back to localStorage-only if no API URL is configured.
5+ */
6+
7+ // Set this to your deployed Worker URL, or leave empty for local-only mode
8+ const API_URL = import . meta. env . VITE_METADATA_API || ''
29
310export interface TaskMetadata {
411 title : string
@@ -12,55 +19,72 @@ export interface TaskMetadata {
1219}
1320
1421/**
15- * Upload task metadata JSON to IPFS via Pinata.
16- * Returns the IPFS CID (content identifier).
22+ * SHA-256 hash of the metadata JSON as a hex string.
23+ */
24+ export async function hashMetadataHex ( metadata : TaskMetadata ) : Promise < string > {
25+ const json = JSON . stringify ( metadata )
26+ const encoded = new TextEncoder ( ) . encode ( json )
27+ const hashBuffer = await crypto . subtle . digest ( 'SHA-256' , encoded )
28+ return Array . from ( new Uint8Array ( hashBuffer ) )
29+ . map ( b => b . toString ( 16 ) . padStart ( 2 , '0' ) )
30+ . join ( '' )
31+ }
32+
33+ /**
34+ * SHA-256 hash as a 32-byte array (for on-chain storage).
35+ */
36+ export async function hashMetadata ( metadata : TaskMetadata ) : Promise < number [ ] > {
37+ const json = JSON . stringify ( metadata )
38+ const encoded = new TextEncoder ( ) . encode ( json )
39+ const hashBuffer = await crypto . subtle . digest ( 'SHA-256' , encoded )
40+ return Array . from ( new Uint8Array ( hashBuffer ) )
41+ }
42+
43+ /**
44+ * Upload task metadata to R2 via the Worker API.
45+ * Returns the content hash (used as the key).
1746 */
1847export async function uploadMetadata ( metadata : TaskMetadata ) : Promise < string > {
19- const jwt = import . meta. env . VITE_PINATA_JWT
20- if ( ! jwt ) {
21- console . warn ( 'VITE_PINATA_JWT not set — falling back to local-only storage' )
22- return ''
48+ const json = JSON . stringify ( metadata )
49+ const hash = await hashMetadataHex ( metadata )
50+
51+ if ( ! API_URL ) {
52+ console . warn ( 'VITE_METADATA_API not set — using local-only storage' )
53+ return hash // return hash anyway for localStorage keying
2354 }
2455
25- const res = await fetch ( 'https://api.pinata.cloud/pinning/pinJSONToIPFS' , {
26- method : 'POST' ,
27- headers : {
28- 'Content-Type' : 'application/json' ,
29- Authorization : `Bearer ${ jwt } ` ,
30- } ,
31- body : JSON . stringify ( {
32- pinataContent : metadata ,
33- pinataMetadata : {
34- name : `taskforest-${ metadata . title . slice ( 0 , 30 ) } ` ,
35- } ,
36- } ) ,
56+ const res = await fetch ( `${ API_URL } /metadata/${ hash } ` , {
57+ method : 'PUT' ,
58+ headers : { 'Content-Type' : 'application/json' } ,
59+ body : json ,
3760 } )
3861
3962 if ( ! res . ok ) {
4063 const err = await res . text ( )
41- throw new Error ( `Pinata upload failed: ${ err } ` )
64+ throw new Error ( `Upload failed: ${ err } ` )
4265 }
4366
44- const data = await res . json ( )
45- return data . IpfsHash as string // e.g. "QmXyz..."
67+ return hash
4668}
4769
4870/**
49- * Fetch task metadata from IPFS via public gateway .
71+ * Fetch task metadata from the Worker API .
5072 * Uses localStorage as cache to avoid repeated fetches.
5173 */
52- export async function fetchMetadata ( cid : string ) : Promise < TaskMetadata | null > {
53- if ( ! cid ) return null
74+ export async function fetchMetadata ( hash : string ) : Promise < TaskMetadata | null > {
75+ if ( ! hash ) return null
5476
5577 // Check cache first
56- const cacheKey = `tf_ipfs_ ${ cid } `
78+ const cacheKey = `tf_meta_ ${ hash } `
5779 const cached = localStorage . getItem ( cacheKey )
5880 if ( cached ) {
5981 try { return JSON . parse ( cached ) } catch { /* re-fetch */ }
6082 }
6183
84+ if ( ! API_URL ) return null
85+
6286 try {
63- const res = await fetch ( `${ PINATA_GATEWAY } / ${ cid } ` , {
87+ const res = await fetch ( `${ API_URL } /metadata/ ${ hash } ` , {
6488 signal : AbortSignal . timeout ( 8000 ) ,
6589 } )
6690 if ( ! res . ok ) return null
@@ -71,14 +95,3 @@ export async function fetchMetadata(cid: string): Promise<TaskMetadata | null> {
7195 return null
7296 }
7397}
74-
75- /**
76- * SHA-256 hash of the metadata JSON, returned as a 32-byte array.
77- * This gets stored on-chain for content verification.
78- */
79- export async function hashMetadata ( metadata : TaskMetadata ) : Promise < number [ ] > {
80- const json = JSON . stringify ( metadata )
81- const encoded = new TextEncoder ( ) . encode ( json )
82- const hashBuffer = await crypto . subtle . digest ( 'SHA-256' , encoded )
83- return Array . from ( new Uint8Array ( hashBuffer ) )
84- }
0 commit comments