Spaces:
Sleeping
Sleeping
feat: deploy sustainability, staff command center, security hardening, accessibility and UX polish to HF
ca26788 | /** | |
| * Match Proof Layer — Cairo-ready session proof generator | |
| */ | |
| export class MatchProofService { | |
| constructor() {} | |
| canonicalize(input) { | |
| if (typeof input === 'string') return input; | |
| if (typeof input !== 'object' || input === null) return String(input); | |
| const sortObjectKeys = (obj) => { | |
| if (Array.isArray(obj)) return obj.map(sortObjectKeys); | |
| if (typeof obj === 'object' && obj !== null) { | |
| return Object.keys(obj) | |
| .sort() | |
| .reduce((acc, key) => { | |
| acc[key] = sortObjectKeys(obj[key]); | |
| return acc; | |
| }, {}); | |
| } | |
| return obj; | |
| }; | |
| return JSON.stringify(sortObjectKeys(input)); | |
| } | |
| buildCanonicalPayload(sessionData) { | |
| return this.canonicalize(sessionData); | |
| } | |
| async generateProofId(input) { | |
| const canonical = this.canonicalize(input); | |
| return this._sha256(canonical); | |
| } | |
| async generateEngagementFingerprint(categoryTotals = {}) { | |
| const canonical = this.canonicalize(categoryTotals); | |
| return this._sha256(canonical); | |
| } | |
| toFelt252Sync(hexHash) { | |
| if (!hexHash) return BigInt(0); | |
| const cleanHex = hexHash.replace(/^0x/, ''); | |
| const first31Bytes = cleanHex.substring(0, 62); | |
| const val = BigInt('0x' + (first31Bytes || '0')); | |
| const maxFelt = (BigInt(2) ** BigInt(248)) - BigInt(1); | |
| return val % maxFelt; | |
| } | |
| /** Async version for backward-compat — delegates to sync implementation. */ | |
| async toFelt252(input) { | |
| let hexHash; | |
| if (typeof input === 'string') { | |
| hexHash = input; | |
| } else { | |
| hexHash = await this.generateProofId(input); | |
| } | |
| return this.toFelt252Sync(hexHash); | |
| } | |
| async toCairoCalldata(input) { | |
| const proofIdHex = await this.generateProofId(input); | |
| const proofIdFelt = await this.toFelt252(proofIdHex); | |
| const engagement = input.engagement || input.categoryTotals || {}; | |
| const fingerprintHex = await this.generateEngagementFingerprint(engagement); | |
| const fingerprintFelt = await this.toFelt252(fingerprintHex); | |
| const ts = input.timestamp || Math.floor(Date.now() / 1000); | |
| return { | |
| proof_id: proofIdFelt.toString(), | |
| proof_id_hex: proofIdHex, | |
| accuracy_bps: input.accuracyBps || 0, | |
| engagement_fingerprint: fingerprintFelt.toString(), | |
| engagement_fingerprint_hex: fingerprintHex, | |
| recorded_at: ts, | |
| timestamp: ts | |
| }; | |
| } | |
| buildCairoCalldata(proofIdHex, accuracyBps, fingerprintHex, timestamp) { | |
| const proofIdFelt = this.toFelt252Sync(proofIdHex); | |
| const fingerprintFelt = this.toFelt252Sync(fingerprintHex); | |
| return { | |
| proof_id: proofIdFelt.toString(), | |
| proof_id_hex: proofIdHex, | |
| accuracy_bps: accuracyBps, | |
| engagement_fingerprint: fingerprintFelt.toString(), | |
| engagement_fingerprint_hex: fingerprintHex, | |
| recorded_at: timestamp, | |
| timestamp: timestamp | |
| }; | |
| } | |
| async exportProof(calldataOrInput) { | |
| let calldata; | |
| if (calldataOrInput && calldataOrInput.proof_id) { | |
| calldata = calldataOrInput; | |
| } else { | |
| calldata = await this.toCairoCalldata(calldataOrInput); | |
| } | |
| const jsonStr = JSON.stringify(calldata, null, 2); | |
| if (typeof document !== 'undefined') { | |
| try { | |
| const blob = new Blob([jsonStr], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `vantage_proof_${calldata.recorded_at || Date.now()}.json`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } catch { | |
| // Fallback for non-DOM environments | |
| } | |
| } | |
| return jsonStr; | |
| } | |
| async verifyProof(sessionData, expectedProofIdHex) { | |
| const actualProofIdHex = await this.generateProofId(sessionData); | |
| return actualProofIdHex === expectedProofIdHex; | |
| } | |
| async _sha256(str) { | |
| if (typeof crypto !== 'undefined' && crypto.subtle) { | |
| try { | |
| const encoder = new TextEncoder(); | |
| const data = encoder.encode(str); | |
| const hashBuffer = await crypto.subtle.digest('SHA-256', data); | |
| const hashArray = Array.from(new Uint8Array(hashBuffer)); | |
| return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); | |
| } catch { | |
| // fallback | |
| } | |
| } | |
| // Secure, standard pure JS SHA-256 fallback | |
| const rightRotate = (value, amount) => (value >>> amount) | (value << (32 - amount)); | |
| const mathPow = Math.pow; | |
| const maxWord = mathPow(2, 32); | |
| const result = []; | |
| const words = []; | |
| const asciiLength = str.length; | |
| const hash = []; | |
| const k = []; | |
| let primeCounter = 0; | |
| const isPrime = (n) => { | |
| for (let factor = 2; factor * factor <= n; factor++) { | |
| if (n % factor === 0) return false; | |
| } | |
| return true; | |
| }; | |
| let candidate = 2; | |
| while (primeCounter < 64) { | |
| if (isPrime(candidate)) { | |
| if (primeCounter < 8) { | |
| hash[primeCounter] = (mathPow(candidate, 1/2) * maxWord) | 0; | |
| } | |
| k[primeCounter] = (mathPow(candidate, 1/3) * maxWord) | 0; | |
| primeCounter++; | |
| } | |
| candidate++; | |
| } | |
| const utf8 = []; | |
| for (let i = 0; i < asciiLength; i++) { | |
| let charcode = str.charCodeAt(i); | |
| if (charcode < 0x80) utf8.push(charcode); | |
| else if (charcode < 0x800) { | |
| utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f)); | |
| } else if (charcode < 0xd800 || charcode >= 0xe000) { | |
| utf8.push(0xe0 | (charcode >> 12), 0x80 | ((charcode >> 6) & 0x3f), 0x80 | (charcode & 0x3f)); | |
| } else { | |
| i++; | |
| charcode = 0x10000 + (((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff)); | |
| utf8.push( | |
| 0xf0 | (charcode >> 18), | |
| 0x80 | ((charcode >> 12) & 0x3f), | |
| 0x80 | ((charcode >> 6) & 0x3f), | |
| 0x80 | (charcode & 0x3f) | |
| ); | |
| } | |
| } | |
| const utf8Length = utf8.length; | |
| utf8.push(0x80); | |
| while (utf8.length % 64 !== 56) utf8.push(0); | |
| const totalBits = utf8Length * 8; | |
| for (let i = 7; i >= 0; i--) { | |
| utf8.push((totalBits >>> (i * 8)) & 0xff); | |
| } | |
| for (let i = 0; i < utf8.length; i += 4) { | |
| words.push((utf8[i] << 24) | (utf8[i+1] << 16) | (utf8[i+2] << 8) | utf8[i+3]); | |
| } | |
| for (let i = 0; i < words.length; i += 16) { | |
| const w = new Array(64); | |
| for (let j = 0; j < 16; j++) w[j] = words[i + j]; | |
| for (let j = 16; j < 64; j++) { | |
| const s0 = rightRotate(w[j - 15], 7) ^ rightRotate(w[j - 15], 18) ^ (w[j - 15] >>> 3); | |
| const s1 = rightRotate(w[j - 2], 17) ^ rightRotate(w[j - 2], 19) ^ (w[j - 2] >>> 10); | |
| w[j] = (w[j - 16] + s0 + w[j - 7] + s1) | 0; | |
| } | |
| let a = hash[0], b = hash[1], c = hash[2], d = hash[3], e = hash[4], f = hash[5], g = hash[6], h = hash[7]; | |
| for (let j = 0; j < 64; j++) { | |
| const S1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25); | |
| const ch = (e & f) ^ (~e & g); | |
| const temp1 = (h + S1 + ch + k[j] + w[j]) | 0; | |
| const S0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22); | |
| const maj = (a & b) ^ (a & c) ^ (b & c); | |
| const temp2 = (S0 + maj) | 0; | |
| h = g; | |
| g = f; | |
| f = e; | |
| e = (d + temp1) | 0; | |
| d = c; | |
| c = b; | |
| b = a; | |
| a = (temp1 + temp2) | 0; | |
| } | |
| hash[0] = (hash[0] + a) | 0; | |
| hash[1] = (hash[1] + b) | 0; | |
| hash[2] = (hash[2] + c) | 0; | |
| hash[3] = (hash[3] + d) | 0; | |
| hash[4] = (hash[4] + e) | 0; | |
| hash[5] = (hash[5] + f) | 0; | |
| hash[6] = (hash[6] + g) | 0; | |
| hash[7] = (hash[7] + h) | 0; | |
| } | |
| for (let i = 0; i < 8; i++) { | |
| const hexStr = (hash[i] >>> 0).toString(16).padStart(8, '0'); | |
| result.push(hexStr); | |
| } | |
| return result.join(''); | |
| } | |
| } | |