const encoder = new TextEncoder(); function stable(value) { if (Array.isArray(value)) return value.map(stable); if (value && typeof value === "object") { return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])])); } return value; } export function canonicalJson(value) { return JSON.stringify(stable(value)); } async function hmacHex(secretHex, value) { const secret = Uint8Array.from(secretHex.match(/../gu) || [], (item) => Number.parseInt(item, 16)); const key = await crypto.subtle.importKey("raw", secret, {name: "HMAC", hash: "SHA-256"}, false, ["sign"]); const result = await crypto.subtle.sign("HMAC", key, encoder.encode(canonicalJson(value))); return [...new Uint8Array(result)].map((byte) => byte.toString(16).padStart(2, "0")).join(""); } async function verify(secret, message) { if (!message || typeof message !== "object" || typeof message.hmac !== "string") throw new Error("E_PROTOCOL"); const unsigned = {...message}; const supplied = unsigned.hmac; delete unsigned.hmac; if (!/^[0-9a-f]{64}$/u.test(supplied)) throw new Error("E_HMAC"); const rawSecret = Uint8Array.from(secret.match(/../gu) || [], (item) => Number.parseInt(item, 16)); const signature = Uint8Array.from(supplied.match(/../gu) || [], (item) => Number.parseInt(item, 16)); const key = await crypto.subtle.importKey("raw", rawSecret, {name: "HMAC", hash: "SHA-256"}, false, ["verify"]); const valid = await crypto.subtle.verify("HMAC", key, signature, encoder.encode(canonicalJson(unsigned))); if (!valid) throw new Error("E_HMAC"); } export class TrustedRuntimeSession { constructor(api, hello) { this.api = api; this.hello = Object.freeze({...hello}); this.secret = null; this.runId = null; this.requestId = null; this.sequence = 0; this.prepared = null; this.dispatched = false; this.actionResult = null; } helloFrame() { return {schema_version: 1, type: "EXTENSION_HELLO", sequence: 1, ...this.hello}; } async accept(message) { if (message?.type === "HOST_CHALLENGE") { if (this.secret !== null || !/^[0-9a-f]{64}$/u.test(String(message.secret || ""))) throw new Error("E_CHALLENGE"); this.secret = message.secret; await verify(this.secret, message); this.runId = message.run_id; this.requestId = message.request_id; this.sequence = 2; return this.sign("EXTENSION_CHALLENGE_ACCEPTED", 3, {challenge_id: message.challenge_id}); } if (!this.secret) throw new Error("E_CHALLENGE_REQUIRED"); await verify(this.secret, message); if (message.run_id !== this.runId || message.request_id !== this.requestId || message.sequence !== this.sequence + 2) { throw new Error("E_SEQUENCE"); } this.sequence = message.sequence; if (message.type === "HOST_ACTION_PREPARE") { if (this.prepared) throw new Error("E_REPLAY"); this.prepared = await this.api.prepare(message.action); return this.sign("EXTENSION_READY_TO_DISPATCH", message.sequence + 1, { action_id: message.action.action_id, prepared: this.prepared }); } if (message.type === "HOST_DISPATCH_PERMIT") { if (!this.prepared || this.dispatched || message.permit.action_id !== this.prepared.action_id) throw new Error("E_PERMIT"); if (Date.now() > Date.parse(message.permit.deadline_at)) throw new Error("E_DEADLINE"); this.dispatched = true; this.actionResult = await this.api.dispatch(this.prepared); return this.sign("EXTENSION_ACTION_RESULT", message.sequence + 1, { permit_id: message.permit.permit_id, result: this.actionResult }); } if (message.type === "HOST_OBSERVATION_REQUEST") { if (!this.dispatched || !this.actionResult) throw new Error("E_ACTION_RESULT_REQUIRED"); const observation = await this.api.observe(this.actionResult); return this.sign("EXTENSION_OBSERVATION", message.sequence + 1, {observation}); } if (message.type === "HOST_COMMIT_RESULT") { return null; } throw new Error("E_MESSAGE_TYPE"); } async sign(type, sequence, fields) { const unsigned = {schema_version: 1, type, run_id: this.runId, request_id: this.requestId, sequence, ...fields}; return {...unsigned, hmac: await hmacHex(this.secret, unsigned)}; } }