browser-bridge.mjs (5639B)
1 // Shared site's boundary validators/codecs; no JS evaluator executes programs. 2 import {decodeBundle,sha256,bundleHash} from './site/bundle.js'; 3 import {literals,makeLiteral,outputType} from './site/codecs.js'; 4 import {exportTree,treeFormat} from './site/tree-export.js'; 5 const utf8=new TextEncoder(); 6 export function frame(fields){ 7 const values=fields.map(x=>typeof x==='string'?utf8.encode(x):x); 8 const size=values.reduce((sum,x)=>sum+4+x.byteLength,0); 9 if(size>16777216)throw Error('Input byte limit exceeded'); 10 const bytes=new Uint8Array(size),view=new DataView(bytes.buffer);let at=0; 11 for(const value of values){view.setUint32(at,value.byteLength,true);at+=4;bytes.set(value,at);at+=value.byteLength;} 12 return bytes; 13 } 14 export function dagBytes(nodes){ 15 if(!nodes.length||nodes.length>1000000)throw Error('DAG node limit exceeded'); 16 const bytes=new Uint8Array(8+nodes.length*9),view=new DataView(bytes.buffer); 17 view.setUint32(0,nodes.length,true);view.setUint32(4,0,true); 18 for(const [i,node] of nodes.entries()){ 19 const at=8+i*9; 20 bytes[at]=node.kind==='leaf'?0:node.kind==='stem'?1:2; 21 view.setUint32(at+1,node.kind==='stem'?node.child:node.kind==='fork'?node.left:0,true); 22 view.setUint32(at+5,node.kind==='fork'?node.right:0,true); 23 } 24 return bytes; 25 } 26 export async function authenticatedBundle(buffer,hash){ 27 bundleHash(hash); 28 if(!(buffer instanceof ArrayBuffer)||buffer.byteLength>16777216)throw Error('Bundle byte limit exceeded'); 29 const bytes=new Uint8Array(buffer.slice(0)); 30 if(await sha256(bytes)!==hash)throw Error('Bundle SHA-256 mismatch'); 31 return decodeBundle(bytes); 32 } 33 export function loadFields(bundle){return ['load',dagBytes(bundle.nodes),...bundle.exports.flatMap(e=>[e.name,String(e.root)])];} 34 class DataGraph { 35 constructor(){this.nodes=[{kind:'leaf'}];this.leaf=0;} 36 add(node){if(this.nodes.length>=1000000)throw Error('Input DAG limit exceeded');this.nodes.push(node);return this.nodes.length-1;} 37 stem(child){return this.add({kind:'stem',child});} 38 fork(left,right){return this.add({kind:'fork',left,right});} 39 fromTernary(text){ 40 const pending=[];let root; 41 for(const c of text){ 42 if(root!==undefined)throw Error('Trailing ternary data'); 43 if(c==='1'||c==='2'){pending.push({arity:Number(c),children:[]});continue;} 44 if(c!=='0')throw Error('Invalid ternary digit'); 45 let value=this.leaf; 46 for(;;){ 47 if(!pending.length){root=value;break;} 48 const frame=pending.at(-1);frame.children.push(value); 49 if(frame.children.length<frame.arity)break; 50 pending.pop();value=frame.arity===1?this.stem(frame.children[0]):this.fork(...frame.children); 51 } 52 } 53 if(root===undefined||pending.length)throw Error('Truncated tree'); 54 return root; 55 } 56 } 57 export function application(name,args=[]){ 58 if(typeof name!=='string'||!name||name.length>4096)throw Error('Invalid export name'); 59 const graph=new DataGraph(),roots=literals(args).map(value=>makeLiteral(graph,value)); 60 return [name,dagBytes(graph.nodes),...roots.map(String)]; 61 } 62 export function evaluation(name,args,codec){const [n,...rest]=application(name,args);return ['evaluate',n,outputType(codec),...rest];} 63 export function snapshotFields(name,args,limit,full=false){ 64 if(!Number.isInteger(limit)||limit<1||limit>(full?100000:256))throw Error('Invalid snapshot budget'); 65 const [n,...rest]=application(name,args);return [full?'snapshot':'preview',n,String(limit),...rest]; 66 } 67 // Convert a COMPLETE runtime snapshot to the site's immutable DAG export input. 68 // IDs are local to this snapshot; no Haskell handle or address is retained. 69 export function snapshotBundle(snapshot,name){ 70 if(snapshot.truncated)throw Error('Snapshot limit exceeded; refusing partial export'); 71 const aliases=new Map(snapshot.aliases.map(a=>[a.from,a.to])),byId=new Map(snapshot.nodes.map(n=>[n.id,n])); 72 const resolve=id=>{const seen=new Set();while(aliases.has(id)){if(seen.has(id))throw Error('Snapshot alias cycle');seen.add(id);id=aliases.get(id);}return id;}; 73 const nodes=[],mapped=new Map(),active=new Set(),root=resolve(snapshot.root),stack=[[root,false]]; 74 while(stack.length){ 75 const [raw,leave]=stack.pop(),id=resolve(raw); 76 if(mapped.has(id))continue; 77 const node=byId.get(id);if(!node)throw Error('Incomplete snapshot'); 78 if(!leave){ 79 if(active.has(id))throw Error('Cyclic tree cannot be exported as a DAG'); 80 active.add(id);stack.push([id,true]);for(const child of [...node.children].reverse())stack.push([child,false]); 81 }else{ 82 active.delete(id);const cs=node.children.map(c=>mapped.get(resolve(c))); 83 if(cs.some(c=>c===undefined))throw Error('Invalid snapshot order'); 84 mapped.set(id,nodes.length);nodes.push(node.kind==='leaf'?{kind:'leaf'}:node.kind==='stem'?{kind:'stem',child:cs[0]}:{kind:'fork',left:cs[0],right:cs[1]}); 85 } 86 } 87 return {nodes,roots:[mapped.get(root)],exports:[{name,root:mapped.get(root),kind:'term',abi:'arboricx.abi.tree.v1'}],metadata:{}}; 88 } 89 export function exportedTree(bundle,name,format){return exportTree(bundle,name,treeFormat(format));} 90 91 export function environmentFields(bundle,names){ 92 if(!Array.isArray(names)||names.length>4096||names.some(n=>typeof n!=='string'||!n||n.length>128)||new Set(names).size!==names.length)throw Error('Invalid environment bindings'); 93 const exports=new Map(bundle.exports.map(e=>[e.name,e.root])); 94 return [dagBytes(bundle.nodes),...names.flatMap(name=>{if(!exports.has(name))throw Error('Missing environment binding '+name);return [name,String(exports.get(name))];})]; 95 } 96 export function emptyEnvironment(){return [dagBytes([{kind:'leaf'}])];}