6b55f8f65d6c
remove chirp pairing: audio exchange, ggwave loader and assets, the popup orchestrator, the Capacitor pairing server, the chirp flow UI and its phase types, and the audioCapture permission (v4.0.28)
- time
- 2026-07-24 22:28 UTC · 3 months ago
- machine
- koh2
- agent
- claude-opus-5
- session
- pairing code transport
- parent
- ad1ba5b1a3e7
↓ peach-browser-6b55f8f6.tar.gzatlas at this save
files changed23 files
manifest.firefox.json+1 −2manifest.json+1 −4package.json+1 −1public/ggwave/ggwave.js+0 −29src/background/chirp-sync-static.test.ts+11 −13src/popup/App.tsx+5 −9src/popup/chirp-pair-capacitor.test.ts+0 −108src/popup/chirp-pair-capacitor.ts+0 −213src/popup/chirp-pair.adopt-source.test.ts+0 −265src/popup/chirp-pair.test.ts+0 −218src/popup/chirp-pair.ts+0 −547src/popup/components/ChirpPairingFlow.test.tsx+0 −102src/popup/components/ChirpPairingFlow.tsx+6 −388src/popup/views/ChirpSync.tsx+21 −288src/popup/views/Settings.tsx+1 −1src/shared/chirp-pairing.test.ts+0 −191src/shared/chirp-pairing.ts+0 −291src/shared/chirp-payload.test.ts+0 −106src/shared/chirp-payload.ts+0 −139src/shared/chirp-transport.probe.test.ts+0 −244src/shared/ggwave.test.ts+0 −131src/shared/ggwave.ts+0 −187src/shared/sync-types.ts+0 −26manifest.firefox.json+1 −2
@@ -1,8 +1,7 @@11 {2- "_comment_audio_capture": "Firefox does not honor the Chrome 'audioCapture' optional_permission — getUserMedia in WebExtensions is gated solely by the runtime permission prompt. Microphone access for chirp pairing works from the popup under the existing '<all_urls>' host permission; no manifest entry is required or accepted here.",32 "manifest_version": 3,43 "name": "Peach Password Manager",5- "version": "4.0.27",4+ "version": "4.0.28",65 "description": "Zero-knowledge password manager",76 "browser_specific_settings": {87 "gecko": {manifest.json+1 −4
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "4.0.27",4+ "version": "4.0.28",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",@@ -64,9 +64,6 @@6464 "webAuthenticationProxy",6565 "offscreen",6666 "mdns"67- ],68- "optional_permissions": [69- "audioCapture"7067 ],7168 "host_permissions": [7269 "<all_urls>"package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "4.0.27",3+ "version": "4.0.28",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",public/ggwave/ggwave.js+0 −29
@@ -1,29 +0,0 @@1-/* PEACH-PATCH LV-7 (2026-06-09): createNamedFunction rewritten to avoid2- the dynamic Function constructor — CSP 'wasm-unsafe-eval' forbids it.3- Re-apply this patch if ggwave.js is ever re-vendored. See src/drafts/AUDIT-FIXES. */4-5-// PATCHED for peach-browser: appended `globalThis.ggwave_factory = ggwave_factory` at EOF so the loader in src/shared/ggwave.ts can pick up the factory when this file is evaluated as an ES module via dynamic import().6-var ggwave_factory = (() => {7- var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined;8- if (typeof __filename !== 'undefined') _scriptDir = _scriptDir || __filename;9- return (10-function(ggwave_factory) {11- ggwave_factory = ggwave_factory || {};12-13-var Module=typeof ggwave_factory!=="undefined"?ggwave_factory:{};var objAssign=Object.assign;var readyPromiseResolve,readyPromiseReject;Module["ready"]=new Promise(function(resolve,reject){readyPromiseResolve=resolve;readyPromiseReject=reject});var moduleOverrides=objAssign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var ENVIRONMENT_IS_WEB=typeof window==="object";var ENVIRONMENT_IS_WORKER=typeof importScripts==="function";var ENVIRONMENT_IS_NODE=typeof process==="object"&&typeof process.versions==="object"&&typeof process.versions.node==="string";var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var read_,readAsync,readBinary,setWindowTitle;function logExceptionOnExit(e){if(e instanceof ExitStatus)return;let toLog=e;err("exiting due to exception: "+toLog)}var fs;var nodePath;var requireNodeFS;if(ENVIRONMENT_IS_NODE){if(ENVIRONMENT_IS_WORKER){scriptDirectory=require("path").dirname(scriptDirectory)+"/"}else{scriptDirectory=__dirname+"/"}requireNodeFS=(()=>{if(!nodePath){fs=require("fs");nodePath=require("path")}});read_=function shell_read(filename,binary){var ret=tryParseAsDataURI(filename);if(ret){return binary?ret:ret.toString()}requireNodeFS();filename=nodePath["normalize"](filename);return fs.readFileSync(filename,binary?null:"utf8")};readBinary=(filename=>{var ret=read_(filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}return ret});readAsync=((filename,onload,onerror)=>{var ret=tryParseAsDataURI(filename);if(ret){onload(ret)}requireNodeFS();filename=nodePath["normalize"](filename);fs.readFile(filename,function(err,data){if(err)onerror(err);else onload(data.buffer)})});if(process["argv"].length>1){thisProgram=process["argv"][1].replace(/\\/g,"/")}arguments_=process["argv"].slice(2);process["on"]("uncaughtException",function(ex){if(!(ex instanceof ExitStatus)){throw ex}});process["on"]("unhandledRejection",function(reason){throw reason});quit_=((status,toThrow)=>{if(keepRuntimeAlive()){process["exitCode"]=status;throw toThrow}logExceptionOnExit(toThrow);process["exit"](status)});Module["inspect"]=function(){return"[Emscripten Module object]"}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!=="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptDir){scriptDirectory=_scriptDir}if(scriptDirectory.indexOf("blob:")!==0){scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}else{scriptDirectory=""}{read_=(url=>{try{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText}catch(err){var data=tryParseAsDataURI(url);if(data){return intArrayToString(data)}throw err}});if(ENVIRONMENT_IS_WORKER){readBinary=(url=>{try{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}catch(err){var data=tryParseAsDataURI(url);if(data){return data}throw err}})}readAsync=((url,onload,onerror)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=(()=>{if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}var data=tryParseAsDataURI(url);if(data){onload(data.buffer);return}onerror()});xhr.onerror=onerror;xhr.send(null)})}setWindowTitle=(title=>document.title=title)}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.warn.bind(console);objAssign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["quit"])quit_=Module["quit"];var tempRet0=0;var setTempRet0=value=>{tempRet0=value};var wasmBinary;if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];var noExitRuntime=Module["noExitRuntime"]||true;if(typeof WebAssembly!=="object"){abort("no native wasm support detected")}var wasmMemory;var ABORT=false;var EXITSTATUS;function assert(condition,text){if(!condition){abort(text)}}var UTF8Decoder=typeof TextDecoder!=="undefined"?new TextDecoder("utf8"):undefined;function UTF8ArrayToString(heap,idx,maxBytesToRead){var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heap[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heap.subarray&&UTF8Decoder){return UTF8Decoder.decode(heap.subarray(idx,endPtr))}else{var str="";while(idx<endPtr){var u0=heap[idx++];if(!(u0&128)){str+=String.fromCharCode(u0);continue}var u1=heap[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}var u2=heap[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u0=(u0&7)<<18|u1<<12|u2<<6|heap[idx++]&63}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}}return str}function UTF8ToString(ptr,maxBytesToRead){return ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):""}function stringToUTF8Array(str,heap,outIdx,maxBytesToWrite){if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.charCodeAt(i);if(u>=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx}function stringToUTF8(str,outPtr,maxBytesToWrite){return stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite)}function lengthBytesUTF8(str){var len=0;for(var i=0;i<str.length;++i){var u=str.charCodeAt(i);if(u>=55296&&u<=57343)u=65536+((u&1023)<<10)|str.charCodeAt(++i)&1023;if(u<=127)++len;else if(u<=2047)len+=2;else if(u<=65535)len+=3;else len+=4}return len}var UTF16Decoder=typeof TextDecoder!=="undefined"?new TextDecoder("utf-16le"):undefined;function UTF16ToString(ptr,maxBytesToRead){var endPtr=ptr;var idx=endPtr>>1;var maxIdx=idx+maxBytesToRead/2;while(!(idx>=maxIdx)&&HEAPU16[idx])++idx;endPtr=idx<<1;if(endPtr-ptr>32&&UTF16Decoder){return UTF16Decoder.decode(HEAPU8.subarray(ptr,endPtr))}else{var str="";for(var i=0;!(i>=maxBytesToRead/2);++i){var codeUnit=HEAP16[ptr+i*2>>1];if(codeUnit==0)break;str+=String.fromCharCode(codeUnit)}return str}}function stringToUTF16(str,outPtr,maxBytesToWrite){if(maxBytesToWrite===undefined){maxBytesToWrite=2147483647}if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite<str.length*2?maxBytesToWrite/2:str.length;for(var i=0;i<numCharsToWrite;++i){var codeUnit=str.charCodeAt(i);HEAP16[outPtr>>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr}function lengthBytesUTF16(str){return str.length*2}function UTF32ToString(ptr,maxBytesToRead){var i=0;var str="";while(!(i>=maxBytesToRead/4)){var utf32=HEAP32[ptr+i*4>>2];if(utf32==0)break;++i;if(utf32>=65536){var ch=utf32-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}else{str+=String.fromCharCode(utf32)}}return str}function stringToUTF32(str,outPtr,maxBytesToWrite){if(maxBytesToWrite===undefined){maxBytesToWrite=2147483647}if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i<str.length;++i){var codeUnit=str.charCodeAt(i);if(codeUnit>=55296&&codeUnit<=57343){var trailSurrogate=str.charCodeAt(++i);codeUnit=65536+((codeUnit&1023)<<10)|trailSurrogate&1023}HEAP32[outPtr>>2]=codeUnit;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr}function lengthBytesUTF32(str){var len=0;for(var i=0;i<str.length;++i){var codeUnit=str.charCodeAt(i);if(codeUnit>=55296&&codeUnit<=57343)++i;len+=4}return len}function alignUp(x,multiple){if(x%multiple>0){x+=multiple-x%multiple}return x}var buffer,HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateGlobalBufferAndViews(buf){buffer=buf;Module["HEAP8"]=HEAP8=new Int8Array(buf);Module["HEAP16"]=HEAP16=new Int16Array(buf);Module["HEAP32"]=HEAP32=new Int32Array(buf);Module["HEAPU8"]=HEAPU8=new Uint8Array(buf);Module["HEAPU16"]=HEAPU16=new Uint16Array(buf);Module["HEAPU32"]=HEAPU32=new Uint32Array(buf);Module["HEAPF32"]=HEAPF32=new Float32Array(buf);Module["HEAPF64"]=HEAPF64=new Float64Array(buf)}var INITIAL_MEMORY=Module["INITIAL_MEMORY"]||16777216;var wasmTable;var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;var runtimeKeepaliveCounter=0;function keepRuntimeAlive(){return noExitRuntime||runtimeKeepaliveCounter>0}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}}function removeRunDependency(id){runDependencies--;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}Module["preloadedImages"]={};Module["preloadedAudios"]={};function abort(what){{if(Module["onAbort"]){Module["onAbort"](what)}}what="Aborted("+what+")";err(what);ABORT=true;EXITSTATUS=1;what+=". Build with -s ASSERTIONS=1 for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";function isDataURI(filename){return filename.startsWith(dataURIPrefix)}function isFileURI(filename){return filename.startsWith("file://")}var wasmBinaryFile;wasmBinaryFile="data:application/octet-stream;base64,AGFzbQEAAAAB5QEhYAN/f38AYAF/AX9gAX8AYAN/f38Bf2AFf39/f38AYAR/f39/AGAGf39/f39/AGAAAGACf38AYAV/f39/fwF/YAJ/fwF/YAN/fn8BfmAEf39/fwF/YAF8AX1gAAF/YAF9AX1gAXwBfGACfH8BfGAGf3x/f39/AX9gCn9/f39/f39/f38AYAN/f3wAYAd/f39/f39/AGACfn8Bf2ADfHx/AXxgAnx8AXxgBX99f39/AX9gBn9/f39/fwF/YAJ9fwF/YAJ8fwF/YAd/f39/f39/AX9gBH9/fn4AYAN/f30AYAJ/fwF9Ap0BGgFhAWEAAAFhAWIAAAFhAWMABgFhAWQAEwFhAWUABAFhAWYAFAFhAWcACgFhAWgABwFhAWkAAAFhAWoAAgFhAWsAAgFhAWwADAFhAW0ACAFhAW4AAAFhAW8ABQFhAXAAAgFhAXEABgFhAXIACQFhAXMAFQFhAXQAAgFhAXUAAQFhAXYAAQFhAXcAAwFhAXgACAFhAXkABAFhAXoACANragMDAAEABAMCAgENDQ4BAAEWDwIQFxgZBA8RBwYFABADAAwBBQcFAwoAAAEaBREBAgAbHAkHAAEDAAkdCgAAHgQFBQUDCAEHChIBCwELAwEfIAECAQIFAAICCAgABwcCAAMECQYGBgQECQ4EBQFwATQ0BQcBAYACgIACBgkBfwFBsO3AAgsHIQgBQQIAAUIAPgFDAQABRAAhAUUAJwFGAGsBRwBOAUgAfAk9AQBBAQszgwFyVU9qaVVPY25hbV5sggF7enl4d3Z1dHNxaGdmZWRiXyxJSUAsQCxdfYABXCx+gQFbLH9ZWgrtuQRqgQQBA38gAkGABE8EQCAAIAEgAhAWGiAADwsgACACaiEDAkAgACABc0EDcUUEQAJAIABBA3FFBEAgACECDAELIAJFBEAgACECDAELIAAhAgNAIAIgAS0AADoAACABQQFqIQEgAkEBaiICQQNxRQ0BIAIgA0kNAAsLAkAgA0F8cSIEQcAASQ0AIAIgBEFAaiIFSw0AA0AgAiABKAIANgIAIAIgASgCBDYCBCACIAEoAgg2AgggAiABKAIMNgIMIAIgASgCEDYCECACIAEoAhQ2AhQgAiABKAIYNgIYIAIgASgCHDYCHCACIAEoAiA2AiAgAiABKAIkNgIkIAIgASgCKDYCKCACIAEoAiw2AiwgAiABKAIwNgIwIAIgASgCNDYCNCACIAEoAjg2AjggAiABKAI8NgI8IAFBQGshASACQUBrIgIgBU0NAAsLIAIgBE8NAQNAIAIgASgCADYCACABQQRqIQEgAkEEaiICIARJDQALDAELIANBBEkEQCAAIQIMAQsgACADQQRrIgRLBEAgACECDAELIAAhAgNAIAIgAS0AADoAACACIAEtAAE6AAEgAiABLQACOgACIAIgAS0AAzoAAyABQQRqIQEgAkEEaiICIARNDQALCyACIANJBEADQCACIAEtAAA6AAAgAUEBaiEBIAJBAWoiAiADRw0ACwsgAAvyAgICfwF+AkAgAkUNACAAIAE6AAAgACACaiIDQQFrIAE6AAAgAkEDSQ0AIAAgAToAAiAAIAE6AAEgA0EDayABOgAAIANBAmsgAToAACACQQdJDQAgACABOgADIANBBGsgAToAACACQQlJDQAgAEEAIABrQQNxIgRqIgMgAUH/AXFBgYKECGwiATYCACADIAIgBGtBfHEiBGoiAkEEayABNgIAIARBCUkNACADIAE2AgggAyABNgIEIAJBCGsgATYCACACQQxrIAE2AgAgBEEZSQ0AIAMgATYCGCADIAE2AhQgAyABNgIQIAMgATYCDCACQRBrIAE2AgAgAkEUayABNgIAIAJBGGsgATYCACACQRxrIAE2AgAgBCADQQRxQRhyIgRrIgJBIEkNACABrUKBgICAEH4hBSADIARqIQEDQCABIAU3AxggASAFNwMQIAEgBTcDCCABIAU3AwAgAUEgaiEBIAJBIGsiAkEfSw0ACwsgAAsjAQF/IwBBEGsiAyQAIAMgAjYCDCAAIAEgAhBCIANBEGokAAszAQF/IABBASAAGyEAAkADQCAAECciAQ0BQaTtACgCACIBBEAgAREHAAwBCwsQBwALIAELFwAgAC0AAEEgcUUEQCABIAIgABA5GgsLbwEBfyMAQYACayIFJAACQCACIANMDQAgBEGAwARxDQAgBSABQf8BcSACIANrIgJBgAIgAkGAAkkiARsQGxogAUUEQANAIAAgBUGAAhAeIAJBgAJrIgJB/wFLDQALCyAAIAUgAhAeCyAFQYACaiQAC3QBAX8gAkUEQCAAKAIEIAEoAgRGDwsgACABRgRAQQEPCyABKAIEIgItAAAhAQJAIAAo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LwEYakEAIAAtABYQGxogACgCKCgCACAALwEkakEAIAAtACIQGxoCQCAALQANBEAgACgCNCgCACAALwEwaiAAKAIIIAAtAAFBAWoiA0H/AXEQGhogACADOgAsDAELIAAoAjQoAgAgAC8BMGpBAToAACAAQQI6ADggAEEBOgAsIAAtAAEEQANAIAAoAkAoAgAgAC8BPGpBAToAACAAKAJAKAIAIAAvATxqIAlBGHRBGHUiA0H/AWogAyADQQBIG0HwKmotAAA6AAEgACAALQAsIAAtADhqQQFrIgM6AEQgACgCTCgCACAALwFIakEAIANB/wFxEBsaIAAtACwhAwJAIAAtADgiBUUNAEEAIQRBASEIIANFBEBBACEDDAELA0BBACEGIAhB/wFxBH8DQEEAIQMCQCAAKAI0KAIAIAAvATBqIAZqLQAAIgVFDQAgAC8BPCAAKAJAKAIAIARqai0AACIIRQ0AIAhB8ChqLQAAIAVB8ChqLQAAakHwKmotAAAhAwsgACgCTCgCACAALwFIaiAEIAZqQf8BcWoiBSAFLQAAIANzOgAAIAZBAWoiBiAALQAsIgNJDQALIAAtADghBSADBUEACyEIIARBAWoiBCAFSQ0ACwsgACADIAAtAEQiBCADIARLGyIDOgAsIAAoAjQoAgAgAC8BMGogACgCTCgCACAALwFIaiADQf8BcRAaGiAAIAM6ACwgAC0AASAJQQFqIglBGHRBGHVKDQALCyAAKAIIIAAoAjQoAgAgAC8BMGogAC0ALBAaGiAAQQE6AA0LIAAoAhwoAgAgAC8BGGogASAALQAAIgMQGhogACADOgAUIAAoAigoAgAgAC8BJGogASAALQAAEBoaIAAgAC0AASIBIAAtABRqOgAgIAAtAAAiAwRAA0ACQCAAKAIoKAIAIAAvASRqIAdqLQAAIgRFDQBBASEBIAAtACxBAkkNAANAIAAoAigoAgAgAC8BJGogASAHakH/AXFqIgMgACgCNCgCACAALwEwaiABai0AACIGBH8gBEHwKGotAAAgBkHwKGotAABqQfAqai0AAAVBAAsgAy0AAHM6AAAgAUEBaiIBIAAtACxJDQALIAAtAAAhAwsgB0EBaiIHIANJDQALIAMhByAALQABIQELIAIgACgCKCgCACAALwEkaiAHQf8BcWogAUH/AXEQGhoLzAYBA38j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AzoAASABQQE6AAAgAUEAOwEMIAJFBEBBASEKIANB/wFxIgIgAkEcbGpBBGoQJyECCyABQQA7ARggASAGOgAWIAEgBkH/AXEiCzsBJCABIAY6ACIgASADQQF0IgM6AC4gAUEDOgA5IAEgAzoAOiABIAtBAXQiEzsBMCABIBMgA0H+AXEiBGoiEzsBPCABIAo6AAwgASACNgIEIAEgAjYCCCABQQA7ARQgAUGAAjsBICABQYAEOwEsIAEgAUEQaiICNgIcIAEgAjYCKCABIAI2AjQgASAEIBNqIgo7AUggASADOgBGIAEgBjoAUiABIAM6AF4gASADOgBqIAFBCDoAdSABIAQgCmoiBjsBVCABIAYgC2oiBjsBYCABIAQgBmoiBjsBbCABIAQgBmoiBjsBeCABQYAIOwFEIAEgAjYCQCABQQA6ADggASACNgJMIAFBgAo7AVAgASACNgJYIAFBgAw7AVwgASACNgJkIAFBgA47AWggASACNgJwIAEgAzoAdiABIAQgBmoiBjsBhAEgASADOgCCASABIAM6AI4BIAEgAzoAmgEgAUEMOgClASABIAM6AKYBIAEgBCAGaiIGOwGQASABIAQgBmoiBjsBnAEgASAEIAZqIgY7AagBIAFBADoAdCABIAI2AnwgAUGAEjsBgAEgASACNgKIASABQYAUOwGMASABIAI2ApQBIAFBgBY7AZgBIAEgAjYCoAEgASAEIAZqIgY7AbQBIAFBDToAsQEgASACNgKsASABQQA6AKQBIAEgAzoAsgEgASAEIAZqIgY7AcABIAFBDjoAvQEgASACNgK4ASABQQA6ALABIAEgAzoAvgEgASAEIAZqIgY7AcwBIAFBDzoAyQEgASACNgLEASABQQA6ALwBIAEgAzoAygEgASAEIAZqOwHYASABQRA6ANUBIAEgAjYC0AEgAUEAOgDIASABIAM6ANYBIAEgAjYC3AEgAUEAOgDUASABIAcoAuwEIgIgBygCVCACIAEtAAAQGiABLQAAahBSAkAgAS0ADEUNACABKAIEIgJFDQAgAhAhCyAHLQC8BCECCyAHKAJkIQRBACEGIAFBADYC3AEgAUEAOgDUASABQQA2AtABIAFBADoAyAEgAUEANgLEASABQQA6ALwBIAFBADYCuAEgAUEAOgCwASABQQA2AqwBIAFBADoApAEgAUEANgKgASABQQA6AJgBIAFBADYClAEgAUEAOgCMASABQQA2AogBIAFBADoAgAEgAUEANgJ8IAFBADoAdCABQQA2AnAgAUEAOgBoIAFBADYCZCABQQA6AFwgAUEANgJYIAFBADoAUCABQQA2AkwgAUEAOgBEIAFBQGtBADYCACABQQA6ADggAUEANgI0IAFBADoALCABQQA2AiggAUEAOgAgIAFBADYCHCABQQA6ABQgASAJOgABIAEgAjoAACABQQA7AQwgBEUEQEEBIQ0gCUH+AXEiA0EcbCADaiACQf8BcUEDbGpBAWoQJyEECyAIIBRsIAxsIRQgAUEAOwEYIAEgAiAJaiILOgAWIAEgCzoAIiABIAlBAXQiAzoALiABQQM6ADkgASADOgA6IAEgC0H/AXEiCTsBJCABIAlBAXQiAjsBMCABIAIgA0H+AXEiCmoiCDsBPCABIA06AAwgASAENgIEIAEgBDYCCCABQQA7ARQgAUGAAjsBICABQYAEOwEsIAEgAUEQaiICNgIcIAEgAjYCKCABIAI2AjQgASAIIApqIgQ7AUggASADOgBGIAEgCzoAUiABIAM6AF4gASADOgBqIAFBCDoAdSABIAQgCmoiBDsBVCABIAQgCWoiBDsBYCABIAQgCmoiBDsBbCABIAQgCmoiBDsBeCABQYAIOwFEIAEgAjYCQCABQQA6ADggASACNgJMIAFBgAo7AVAgASACNgJYIAFBgAw7AVwgASACNgJkIAFBgA47AWggASACNgJwIAEgAzoAdiABIAQgCmoiBDsBhAEgASADOgCCASABIAM6AI4BIAEgAzoAmgEgAUEMOgClASABIAM6AKYBIAEgBCAKaiIEOwGQASABIAQgCmoiBDsBnAEgASAEIApqIgQ7AagBIAFBADoAdCABIAI2AnwgAUGAEjsBgAEgASACNgKIASABQYAUOwGMASABIAI2ApQBIAFBgBY7AZgBIAEgAjYCoAEgASAEIApqIgQ7AbQBIAFBDToAsQEgASACNgKsASABQQA6AKQBIAEgAzoAsgEgASAEIApqIgQ7AcABIAFBDjoAvQEgASACNgK4ASABQQA6ALABIAEgAzoAvgEgASAEIApqIgQ7AcwBIAFBDzoAyQEgASACNgLEASABQQA6ALwBIAEgAzoAygEgASAEIApqOwHYASABQRA6ANUBIAEgAjYC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ECACDAELIABBADYCHCAAQgA3AxAgACAAKAIAQSByNgIAQQAgB0ECRg0AGiACIAEoAgRrCyEAIARBIGokACAACwkAIAAoAjwQFQsPACABIAAoAgBqIAI4AgALDQAgASAAKAIAaioCAAudAQEDfwJ/IAAoAgQiAiEAAkAgAkEDcQRAA0AgAC0AAEUNAiAAQQFqIgBBA3ENAAsLA0AgACIBQQRqIQAgASgCACIDQX9zIANBgYKECGtxQYCBgoR4cUUNAAsgASACayADQf8BcUUNARoDQCABLQABIQMgAUEBaiIAIQEgAw0ACwsgACACawtBAWoiABAnIgEEfyABIAIgABAaBUEACwttAQN/IwBBEGsiAiQAAkACQCAAQQNLDQAgAEECdEGw2QBqIgMoAgAiAUUNACABKALwByIABEAgABAhCyABECEgA0EANgIADAELQcDZACgCACIBRQ0AIAIgADYCACABQcodIAIQHAsgAkEQaiQAC8ETAwl/An0BfiMAQTBrIgQkAAJAAkBBsNkAKAIARQRAQbDZACEHDAELQbTZACgCAEUEQEG02QAhB0EBIQgMAQtBuNkAKAIARQRAQbjZACEHQQIhCAwBC0G82QAoAgBFBEBBvNkAIQdBAyEIDAELQX8hCEHA2QAoAgAiAEUNASAEQQQ2AgAgAEGRIiAEEBwMAQtB+AcQHSEBIAQgACgCADYCCCAEIAAqAgQ4AgwgBCAAKgIIOAIQIAQgACoCDDgCFCAEIAAoAhA2AhggBCAAKgIUOAIcIAQgACgCGDYCICAEIAAoAhw2AiQgBCAAKAIgNgIoIAFBfzYCSCABQQA6AEQgAUGAgID8ezYCQCABQn83AzggAUKAgID8ezcDMCABQoCAgPx7NwMoIAFCgICAgICAgMC/fzcDICABQv////8PNwMYIAFCgICA/Hs3AxAgAUKAgID8ezcDCCABQoCAgPyLgIDAv383AwAgAUIANwCnASABQgA3AqABIAFCADcCmAEgAUIANwKQASABQgA3AogBIAFCADcCgAEgAUIANwJ4IAFCADcCcCABQQA2AtgBIAFCADcC0AEgAUIANwLIASABQgA3AsABIAFCADcCuAEgAUIANwKwASABQQA2AkwgAUEAOgBQIAFCADcCVCABQgA3AlwgAUIANwJkIAFBADsBbCABQfQDakEAQcEAEBsa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OgC0ASADQQA2ArABIANBADoAqAEgA0EANgKkASADQQA6AJwBIANBADYCmAEgA0EAOgCQASADQQA2AowBIANBADoAhAEgA0EANgKAASADQQA6AHggA0EANgJ0IANBADoAbCADQQA2AmggA0EAOgBgIANBADYCXCADQQA6AFQgAyAAQQFrIgo6AEEgA0EBOgBAIANBADsBTEEAIQ4gBkUEQEEBIQ4gCkH/AXEiBiAGQRxsakEEahAnIQYLIAMgDjoATCADIAY2AkQgA0EAOwFYIAMgBjYCSCADIAA6AFYgAyAJNgJcIANBADsBVCADIAA6AGIgAyAJNgJoIANBgAI7AWAgA0EDOgB5IAMgCTYCdCADQYAEOwFsIAMgCkEBdCIGOgBuIAMgBjoAeiADIABB/wFxIg47AWQgAyAOQQF0Ig07AXAgAyANIAZB/gFxIgpqIg07AXwgA0GACDsBhAEgAyAJNgKAASADQQA6AHggAyAGOgCGASADIAk2AowBIANBgAo7AZABIAMgADoAkgEgAyAJNgKYASADQYAMOwGcASADIAY6AJ4BIAMgCTYCpAEgA0GADjsBqAEgAyAGOgCqASADIAk2ArABIANBCDoAtQEgAyAKIA1qIgA7AYgBIAMgACAKaiIAOwGUASADIAAgDmoiADsBoAEgAyAAIApqIgA7AawBIAMgACAKaiIAOwG4ASADQQA6ALQBIAMgBjoAtgEgAyAJNgK8ASADIAY6AMIBIANBgBI7AcABIAMgCTYCyAEgAyAGOgDOASADQYAUOwHMASADIAk2AtQBIAMgBjoA2gEgA0GAFjsB2AEgA0EMOgDlASADIAk2AuABIAMgBjoA5gEgAyAAIApqIgA7AcQBIAMgACAKaiIAOwHQASADIAAgCmoiADsB3AEgAyAAIApqIgA7AegBIANBDToA8QEgAyAJNgLsASADQQA6AOQBIAMgACAKaiIAOwH0ASADIAY6APIBIANBDjoA/QEgAyAJNgL4ASADQQA6APABIAMgACAKaiIAOwGAAiADIAY6AP4BIANBDzoAiQIgAyAJNgKEAiADQQA6APwBIAMgACAKaiIAOwGMAiADIAY6AIoCIAMgACAKajsBmAIgA0EQOgCVAiADIAk2ApACIANBADoAiAIgAyAGOgCWAiADIAk2ApwCIANBADoAlAJBFyEGAkAgA0FA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Y3RvcgB1bnNpZ25lZCBjaGFyAHNhbXBsZUZvcm1hdElucABzYW1wbGVSYXRlSW5wAF9fY3hhX2d1YXJkX2FjcXVpcmUgZGV0ZWN0ZWQgcmVjdXJzaXZlIGluaXRpYWxpemF0aW9uAG5hbgBib29sAHR4VG9nZ2xlUHJvdG9jb2wAcnhUb2dnbGVQcm90b2NvbABlbXNjcmlwdGVuOjp2YWwAW1VdIE5vcm1hbABbTVRdIE5vcm1hbABbRFRdIE5vcm1hbABwYXlsb2FkTGVuZ3RoAGRpc2FibGVMb2cAZW5hYmxlTG9nAHVuc2lnbmVkIGxvbmcAc3RkOjp3c3RyaW5nAGJhc2ljX3N0cmluZwBzdGQ6OnN0cmluZwBzdGQ6OnUxNnN0cmluZwBzdGQ6OnUzMnN0cmluZwBpbmYAc2FtcGxlUmF0ZQBzYW1wbGVzUGVyRnJhbWUAZG91YmxlAGZyZWUAZW5jb2RlAGRlY29kZQBvcGVyYXRpbmdNb2RlAHNvdW5kTWFya2VyVGhyZXNob2xkAHZvaWQAUHJvdG9jb2xJZABHR1dBVkVfT1BFUkFUSU5HX01PREVfVFgAR0dXQVZFX09QRVJBVElOR19NT0RFX1JYX0FORF9UWABHR1dBVkVfT1BFUkFUSU5HX01PREVfUlgAR0dXQVZFX1BST1RPQ09MX0RUX0ZBU1RFU1QAR0dXQVZFX1BST1RPQ09MX0FVRElCTEVfRkFTVEVTVABHR1dBVkVfUFJPVE9DT0xfVUxUUkFTT1VORF9GQVNURVNUAEdHV0FWRV9QUk9UT0NPTF9EVF9GQVNUAEdHV0FWRV9QUk9UT0NPTF9BVURJQkxFX0ZBU1QAR0dXQVZFX1BST1RPQ09MX1VMVFJBU09VTkRfRkFTVABHR1dBVkVfT1BFUkFUSU5HX01PREVfVVNFX0RTUwBHR1dBVkVfT1BFUkFUSU5HX01PREVfVFhfT05MWV9UT05FUwBOQU4AR0dXQVZFX1BST1RPQ09MX0RUX05PUk1BTABHR1dBVkVfUFJPVE9DT0xfQVVESUJMRV9OT1JNQUwAR0dXQVZFX1BST1RPQ09MX1VMVFJBU09VTkRfTk9STUFMAElORgBHR1dBVkVfU0FNUExFX0ZPUk1BVF9VTkRFRklORUQAZW1zY3JpcHRlbjo6bWVtb3J5X3ZpZXc8c2hvcnQ+AGVtc2NyaXB0ZW46Om1lbW9yeV92aWV3PHVuc2lnbmVkIHNob3J0PgBlbXNjcmlwdGVuOjptZW1vcnlfdmlldzxpbnQ+AGVtc2NyaXB0ZW46Om1lbW9yeV92aWV3PHVuc2lnbmVkIGludD4AZW1zY3JpcHRlbjo6bWVtb3J5X3ZpZXc8ZmxvYXQ+AGVtc2NyaXB0ZW46Om1lbW9yeV92aWV3PHVpbnQ4X3Q+AGVtc2NyaXB0ZW46Om1lbW9yeV92aWV3PGludDhfdD4AZW1zY3JpcHRlbjo6bWVtb3J5X3ZpZXc8dWludDE2X3Q+AGVtc2NyaXB0ZW46Om1lbW9yeV92aWV3PGludDE2X3Q+AGVtc2NyaXB0ZW46Om1lbW9yeV92aWV3PHVpbnQzMl90PgBlbXNjcmlwdGVuOjptZW1vcnlfdmlldzxpbnQzMl90PgBlbXNjcmlwdGVuOjptZW1vcnlfdmlldzxjaGFyPgBlbXNjcmlwdGVuOjptZW1vcnlfdmlldzx1bnNpZ25lZCBjaGFyPgBzdGQ6OmJhc2ljX3N0cmluZzx1bnNpZ25lZCBjaGFyPgBlbXNjcmlwdGVuOjptZW1vcnlfdmlldzxzaWduZWQgY2hhcj4AZW1zY3JpcHRlbjo6bWVtb3J5X3ZpZXc8bG9uZz4AZW1zY3JpcHRlbjo6bWVtb3J5X3ZpZXc8dW5zaWduZWQgbG9uZz4AZW1zY3JpcHRlbjo6bWVtb3J5X3ZpZXc8ZG91YmxlPgBHR1dBVkVfUFJPVE9DT0xfQ1VTVE9NXzkAR0dXQVZFX1BST1RPQ09MX0NVU1RPTV84AEdHV0FWRV9TQU1QTEVfRk9STUFUX1U4AEdHV0FWRV9TQU1QTEVfRk9STUFUX0k4AEdHV0FWRV9QUk9UT0NPTF9DVVNUT01fNwBHR1dBVkVfUFJPVE9DT0xfQ1VTVE9NXzYAR0dXQVZFX1NBTVBMRV9GT1JNQVRfVTE2AEdHV0FWRV9TQU1QTEVfRk9STUFUX0kxNgBHR1dBVkVfUFJPVE9DT0xfQ1VTVE9NXzUAR0dXQVZFX1BST1RPQ09MX0NVU1RPTV80AEdHV0FWRV9QUk9UT0NPTF9DVVNUT01fMwBHR1dBVkVfUFJPVE9DT0xfQ1VTVE9NXzIAR0dXQVZFX1NBTVBMRV9GT1JNQVRfRjMyAEdHV0FWRV9QUk9UT0NPTF9DVVNUT01fMQBHR1dBVkVfUFJPVE9DT0xfQ1VTVE9NXzAALgAobnVsbCkARXJyb3I6IGNhcHR1cmUgc2FtcGxlIHJhdGUgKCVnIEh6KSBtdXN0IGJlID49ICVnIEh6CgBFcnJvcjogY2FwdHVyZSBzYW1wbGUgcmF0ZSAoJWcgSHopIG11c3QgYmUgPD0gJWcgSHoKAEVycm9yOiBmYWlsZWQgdG8gY29tcHV0ZSB0aGUgc2l6ZSBvZiB0aGUgcmVxdWlyZWQgbWVtb3J5CgBUcnVuY2F0aW5nIGRhdGEgZnJvbSAlZCB0byAlZCBieXRlcwoAQ2Fubm90IGRlY29kZSB3aGlsZSB0cmFuc21pdHRpbmcKAFR4IGlzIGRpc2FibGVkIC0gY2Fubm90IHRyYW5zbWl0IGRhdGEgd2l0aCB0aGlzIEdHV2F2ZSBpbnN0YW5jZQoAUnggaXMgZGlzYWJsZWQgLSBjYW5ub3QgcmVjZWl2ZSBkYXRhIHdpdGggdGhpcyBHR1dhdmUgaW5zdGFuY2UKAFByb3RvY29sICVkIGlzIG5vdCBlbmFibGVkIC0gbWFrZSBzdXJlIHRvIGVuYWJsZSBpdCBiZWZvcmUgY3JlYXRpbmcgdGhlIGluc3RhbmNlCgBNb25vLXRvbmUgcHJvdG9jb2xzIHdpdGggdmFyaWFibGUgbGVuZ3RoIGFyZSBub3Qgc3VwcG9ydGVkCgBGYWlsZWQgdG8gaW5pdGlhbGl6ZSBUeCB0cmFuc21pc3Npb24gZm9yIEdHV2F2ZSBpbnN0YW5jZSAlZAoASW52YWxpZCBHR1dhdmUgaW5zdGFuY2UgJWQKAEZhaWxlZCB0byBlbmNvZGUgZGF0YSAtIEdHV2F2ZSBpbnN0YW5jZSAlZAoARmFpbGVkIHRvIGRlY29kZSBkYXRhIC0gR0dXYXZlIGluc3RhbmNlICVkCgBGYWlsZWQgdG8gZnJlZSBHR1dhdmUgaW5zdGFuY2UgLSBpbnZhbGlkIEdHV2F2ZSBpbnN0YW5jZSBpZCAlZAoAbiA9ICVkLCBuQWN0dWFsID0gJWQKAFJlY2VpdmVkIGVuZCBtYXJrZXIuIEZyYW1lcyBsZWZ0ID0gJWQsIHJlY29yZGVkID0gJWQKAEVycm9yOiBmYWlsZWQgdG8gYWxsb2NhdGUgdGhlIHJlcXVpcmVkIG1lbW9yeTogJWQKAEludmFsaWQgcGF5bG9hZCBsZW5ndGg6ICVkLCBtYXg6ICVkCgBJbnZhbGlkIHNhbXBsZXMgcGVyIGZyYW1lOiAlZCwgbWF4OiAlZAoASW52YWxpZCBvciB1bnN1cHBvcnRlZCBwbGF5YmFjayBzYW1wbGUgZm9ybWF0OiAlZAoASW52YWxpZCBvciB1bnN1cHBvcnRlZCBjYXB0dXJlIHNhbXBsZSBmb3JtYXQ6ICVkCgBJbnZhbGlkIHNhbXBsZSBmb3JtYXQ6ICVkCgBOZWdhdGl2ZSBkYXRhIHNpemU6ICVkCgBFcnJvcjogZmFpbGVkIHRvIGFsbG9jYXRlIG1lbW9yeSAtIGhlYXBTaXplMDogJWQsIGhlYXBTaXplOiAlZAoASW52YWxpZCB2b2x1bWU6ICVkCgBJbnZhbGlkIHByb3RvY29sIElEOiAlZAoAUmVjZWl2aW5nIHNvdW5kIGRhdGEgLi4uCgBBbmFseXppbmcgY2FwdHVyZWQgZGF0YSAuLgoARmFpbGVkIHRvIGNyZWF0ZSBHR1dhdmUgaW5zdGFuY2UgLSByZWFjaGVkIG1heGltdW0gbnVtYmVyIG9mIGluc3RhbmNlcyAoJWQpCgBGYWlsdXJlIGR1cmluZyBjYXB0dXJlIC0gcHJvdmlkZWQgYnl0ZXMgKCVkKSBhcmUgbm90IG11bHRpcGxlIG9mIHNhbXBsZSBzaXplICglZCkKAERlY29kZWQgbGVuZ3RoID0gJWQsIHByb3RvY29sID0gJyVzJyAoJWQpCgBFcnJvcjogdG90YWwgbGVuZ3RoICVkIChwYXlsb2FkICVkICsgRUNDICVkIGJ5dGVzKSBpcyB0b28gbGFyZ2UgKCA+ICVkKQoARmFpbGVkIHRvIGNhcHR1cmUgc291bmQgZGF0YS4gUGxlYXNlIHRyeSBhZ2FpbiAobGVuZ3RoID0gJWQpCgBSZWNlaXZlZCBzb3VuZCBkYXRhIHN1Y2Nlc3NmdWxseTogJyVzJwoAMTlnZ3dhdmVfU2FtcGxlRm9ybWF0AAAAAPwoAACLEgAAMTdnZ3dhdmVfUHJvdG9jb2xJZAD8KAAArBIAADE3Z2d3YXZlX1BhcmFtZXRlcnMASCkAAMgSAABpAHZpAGlpaQB2aWlpAGZpaQB2aWlmAADcEgAAaWkAAJgoAADcEgAARCgAAJgoAAB2aWkATjEwZW1zY3JpcHRlbjExbWVtb3J5X3ZpZXdJY0VFAABIKQAAGBMAAGgTAACYKAAA4BMAAMASAACYKAAATjEwZW1zY3JpcHRlbjN2YWxFAABIKQAAVBMAAE5TdDNfXzIxMmJhc2ljX3N0cmluZ0ljTlNfMTFjaGFyX3RyYWl0c0ljRUVOU185YWxsb2NhdG9ySWNFRUVFAE5TdDNfXzIyMV9fYmFzaWNfc3RyaW5nX2NvbW1vbklMYjFFRUUAAAAASCkAAK8TAADMKQAAcBMAAAAAAAABAAAA2BMAAAAAAABpaWlpaWkAAGgTAACYKAAA4BMAAGlpaWkAAAAARCgAAEQoAADAEgAAmCgAQbAoC6cilp+0rxuR3sVFdeguDzJKX7RWlct/alRqSPILe837k208d17DM0fA8XEyMyc1aEcfTqwjQl8AN6RQbUgkkXyhTgAAARkCMhrGA98z7htox0sEZOAONI3vgRzBafjICExxBYplL+EkDyE1k47a8BKCRR21wn1qJ/m5yZoJeE3kcqYGv4tiZt0w/eKYJbMQkSKINtCUzo+W273x0hNcgzhGQB5CtqPDSH5uazooVPqFuj3KXpufChV5K07U5axz86dXB3DA94yAYw1nSt7tMcX+GOOlmXcmuLR8EUSS2SMgiS43P9FblbzPzZCHl7Lc/L5h8lbTqxQqXZ6EPDlTR21Boh8tQ9i3e6R2xBdJ7H8Mb/ZsoTtSKZ1VqvtghrG7zD5ay1lfsJypoFEL9RbrenUs10+u1enm563odNb06qhQWK8BAgQIECBAgB06dOjNhxMmTJgtWrR16smPAwYMGDBgwJ0nTpwlSpQ1atS1d+7BnyNGjAUKFChQoF26adK5b96hX75hwpkvXrxlyokPHjx48P3n07tr1rF//uHfo1u2ceLZr0OGESJEiA0aNGjQvWfOgR8+fPjtx5M7duzFlzNmzIUXLly4bdqpT54hQoQVKlSoTZopUqRVqkmSOXLk1bdz5tG/Y8aRP3785deze/bx/+Pbq0uWMWLElTdu3KVXrkGCGTJkyI0HDhw4cODdp1OmUaJZsnny+e/DmytWrEWKCRIkSJA9evT19/P768uLCxYsWLB9+unPgxs2bNitR44BAgQIECBAgB06dOjNhxMmTJgtWrR16smPAwYMGDBgwJ0nTpwlSpQ1atS1d+7BnyNGjAUKFChQoF26adK5b96hX75hwpkvXrxlyokPHjx48P3n07tr1rF//uHfo1u2ceLZr0OGESJEiA0aNGjQvWfOgR8+fPjtx5M7duzFlzNmzIUXLly4bdqpT54hQoQVKlSoTZopUqRVqkmSOXLk1bdz5tG/Y8aRP3785deze/bx/+Pbq0uWMWLElTdu3KVXrkGCGTJkyI0HDhw4cODdp1OmUaJZsnny+e/DmytWrEWKCRIkSJA9evT19/P768uLCxYsWLB9+unPgxs2bNitR44BAgAAAAABAAAAAQAAAAIAAAACAAAABAAAAE5TdDNfXzIxMmJhc2ljX3N0cmluZ0loTlNfMTFjaGFyX3RyYWl0c0loRUVOU185YWxsb2NhdG9ySWhFRUVFAADMKQAAiBcAAAAAAAABAAAA2BMAAAAAAABOU3QzX18yMTJiYXNpY19zdHJpbmdJd05TXzExY2hhcl90cmFpdHNJd0VFTlNfOWFsbG9jYXRvckl3RUVFRQAAzCkAAOAXAAAAAAAAAQAAANgTAAAAAAAATlN0M19fMjEyYmFzaWNfc3RyaW5nSURzTlNfMTFjaGFyX3RyYWl0c0lEc0VFTlNfOWFsbG9jYXRvcklEc0VFRUUAAADMKQAAOBgAAAAAAAABAAAA2BMAAAAAAABOU3QzX18yMTJiYXNpY19zdHJpbmdJRGlOU18xMWNoYXJfdHJhaXRzSURpRUVOU185YWxsb2NhdG9ySURpRUVFRQAAAMwpAACUGAAAAAAAAAEAAADYEwAAAAAAAE4xMGVtc2NyaXB0ZW4xMW1lbW9yeV92aWV3SWFFRQAASCkAAPAYAABOMTBlbXNjcmlwdGVuMTFtZW1vcnlfdmlld0loRUUAAEgpAAAYGQAATjEwZW1zY3JpcHRlbjExbWVtb3J5X3ZpZXdJc0VFAABIKQAAQBkAAE4xMGVtc2NyaXB0ZW4xMW1lbW9yeV92aWV3SXRFRQAASCkAAGgZAABOMTBlbXNjcmlwdGVuMTFtZW1vcnlfdmlld0lpRUUAAEgpAACQGQAATjEwZW1zY3JpcHRlbjExbWVtb3J5X3ZpZXdJakVFAABIKQAAuBkAAE4xMGVtc2NyaXB0ZW4xMW1l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";if(!isDataURI(wasmBinaryFile)){wasmBinaryFile=locateFile(wasmBinaryFile)}function getBinary(file){try{if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}var binary=tryParseAsDataURI(file);if(binary){return binary}if(readBinary){return readBinary(file)}else{throw"both async and sync fetching of the wasm failed"}}catch(err){abort(err)}}function getBinaryPromise(){if(!wasmBinary&&(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER)){if(typeof fetch==="function"&&!isFileURI(wasmBinaryFile)){return fetch(wasmBinaryFile,{credentials:"same-origin"}).then(function(response){if(!response["ok"]){throw"failed to load wasm binary file at '"+wasmBinaryFile+"'"}return response["arrayBuffer"]()}).catch(function(){return getBinary(wasmBinaryFile)})}else{if(readAsync){return new Promise(function(resolve,reject){readAsync(wasmBinaryFile,function(response){resolve(new Uint8Array(response))},reject)})}}}return Promise.resolve().then(function(){return getBinary(wasmBinaryFile)})}function createWasm(){var info={"a":asmLibraryArg};function receiveInstance(instance,module){var exports=instance.exports;Module["asm"]=exports;wasmMemory=Module["asm"]["A"];updateGlobalBufferAndViews(wasmMemory.buffer);wasmTable=Module["asm"]["C"];addOnInit(Module["asm"]["B"]);removeRunDependency("wasm-instantiate")}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}function instantiateArrayBuffer(receiver){return getBinaryPromise().then(function(binary){return WebAssembly.instantiate(binary,info)}).then(function(instance){return instance}).then(receiver,function(reason){err("failed to asynchronously prepare wasm: "+reason);abort(reason)})}function instantiateAsync(){if(!wasmBinary&&typeof WebAssembly.instantiateStreaming==="function"&&!isDataURI(wasmBinaryFile)&&!isFileURI(wasmBinaryFile)&&typeof fetch==="function"){return fetch(wasmBinaryFile,{credentials:"same-origin"}).then(function(response){var result=WebAssembly.instantiateStreaming(response,info);return result.then(receiveInstantiationResult,function(reason){err("wasm streaming compile failed: "+reason);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(receiveInstantiationResult)})})}else{return instantiateArrayBuffer(receiveInstantiationResult)}}if(Module["instantiateWasm"]){try{var exports=Module["instantiateWasm"](info,receiveInstance);return exports}catch(e){err("Module.instantiateWasm callback failed with error: "+e);return false}}instantiateAsync().catch(readyPromiseReject);return{}}function callRuntimeCallbacks(callbacks){while(callbacks.length>0){var callback=callbacks.shift();if(typeof callback=="function"){callback(Module);continue}var func=callback.func;if(typeof func==="number"){if(callback.arg===undefined){getWasmTableEntry(func)()}else{getWasmTableEntry(func)(callback.arg)}}else{func(callback.arg===undefined?null:callback.arg)}}}var wasmTableMirror=[];function getWasmTableEntry(funcPtr){var func=wasmTableMirror[funcPtr];if(!func){if(funcPtr>=wasmTableMirror.length)wasmTableMirror.length=funcPtr+1;wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func}var structRegistrations={};function runDestructors(destructors){while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}}function simpleReadValueFromPointer(pointer){return this["fromWireType"](HEAPU32[pointer>>2])}var awaitingDependencies={};var registeredTypes={};var typeDependencies={};var char_0=48;var char_9=57;function makeLegalFunctionName(name){if(undefined===name){return"_unknown"}name=name.replace(/[^a-zA-Z0-9_]/g,"$");var f=name.charCodeAt(0);if(f>=char_0&&f<=char_9){return"_"+name}else{return name}}function createNamedFunction(name,body){name=makeLegalFunctionName(name);var f=function(){"use strict";return body.apply(this,arguments)};try{Object.defineProperty(f,"name",{value:name})}catch(e){}return f}function extendError(baseErrorType,errorName){var errorClass=createNamedFunction(errorName,function(message){this.name=errorName;this.message=message;var stack=new Error(message).stack;if(stack!==undefined){this.stack=this.toString()+"\n"+stack.replace(/^Error(:[^\n]*)?\n/,"")}});errorClass.prototype=Object.create(baseErrorType.prototype);errorClass.prototype.constructor=errorClass;errorClass.prototype.toString=function(){if(this.message===undefined){return this.name}else{return this.name+": "+this.message}};return errorClass}var InternalError=undefined;function throwInternalError(message){throw new InternalError(message)}function whenDependentTypesAreResolved(myTypes,dependentTypes,getTypeConverters){myTypes.forEach(function(type){typeDependencies[type]=dependentTypes});function onComplete(typeConverters){var myTypeConverters=getTypeConverters(typeConverters);if(myTypeConverters.length!==myTypes.length){throwInternalError("Mismatched type converter count")}for(var i=0;i<myTypes.length;++i){registerType(myTypes[i],myTypeConverters[i])}}var typeConverters=new Array(dependentTypes.length);var unregisteredTypes=[];var registered=0;dependentTypes.forEach(function(dt,i){if(registeredTypes.hasOwnProperty(dt)){typeConverters[i]=registeredTypes[dt]}else{unregisteredTypes.push(dt);if(!awaitingDependencies.hasOwnProperty(dt)){awaitingDependencies[dt]=[]}awaitingDependencies[dt].push(function(){typeConverters[i]=registeredTypes[dt];++registered;if(registered===unregisteredTypes.length){onComplete(typeConverters)}})}});if(0===unregisteredTypes.length){onComplete(typeConverters)}}function __embind_finalize_value_object(structType){var reg=structRegistrations[structType];delete structRegistrations[structType];var rawConstructor=reg.rawConstructor;var rawDestructor=reg.rawDestructor;var fieldRecords=reg.fields;var fieldTypes=fieldRecords.map(function(field){return field.getterReturnType}).concat(fieldRecords.map(function(field){return field.setterArgumentType}));whenDependentTypesAreResolved([structType],fieldTypes,function(fieldTypes){var fields={};fieldRecords.forEach(function(field,i){var fieldName=field.fieldName;var getterReturnType=fieldTypes[i];var getter=field.getter;var getterContext=field.getterContext;var setterArgumentType=fieldTypes[i+fieldRecords.length];var setter=field.setter;var setterContext=field.setterContext;fields[fieldName]={read:function(ptr){return getterReturnType["fromWireType"](getter(getterContext,ptr))},write:function(ptr,o){var destructors=[];setter(setterContext,ptr,setterArgumentType["toWireType"](destructors,o));runDestructors(destructors)}}});return[{name:reg.name,"fromWireType":function(ptr){var rv={};for(var i in fields){rv[i]=fields[i].read(ptr)}rawDestructor(ptr);return rv},"toWireType":function(destructors,o){for(var fieldName in fields){if(!(fieldName in o)){throw new TypeError('Missing field: "'+fieldName+'"')}}var ptr=rawConstructor();for(fieldName in fields){fields[fieldName].write(ptr,o[fieldName])}if(destructors!==null){destructors.push(rawDestructor,ptr)}return ptr},"argPackAdvance":8,"readValueFromPointer":simpleReadValueFromPointer,destructorFunction:rawDestructor}]})}function __embind_register_bigint(primitiveType,name,size,minRange,maxRange){}function getShiftFromSize(size){switch(size){case 1:return 0;case 2:return 1;case 4:return 2;case 8:return 3;default:throw new TypeError("Unknown type size: "+size)}}function embind_init_charCodes(){var codes=new Array(256);for(var i=0;i<256;++i){codes[i]=String.fromCharCode(i)}embind_charCodes=codes}var embind_charCodes=undefined;function readLatin1String(ptr){var ret="";var c=ptr;while(HEAPU8[c]){ret+=embind_charCodes[HEAPU8[c++]]}return ret}var BindingError=undefined;function throwBindingError(message){throw new BindingError(message)}function registerType(rawType,registeredInstance,options={}){if(!("argPackAdvance"in registeredInstance)){throw new TypeError("registerType registeredInstance requires argPackAdvance")}var name=registeredInstance.name;if(!rawType){throwBindingError('type "'+name+'" must have a positive integer typeid pointer')}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError("Cannot register type '"+name+"' twice")}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(function(cb){cb()})}}function __embind_register_bool(rawType,name,size,trueValue,falseValue){var shift=getShiftFromSize(size);name=readLatin1String(name);registerType(rawType,{name:name,"fromWireType":function(wt){return!!wt},"toWireType":function(destructors,o){return o?trueValue:falseValue},"argPackAdvance":8,"readValueFromPointer":function(pointer){var heap;if(size===1){heap=HEAP8}else if(size===2){heap=HEAP16}else if(size===4){heap=HEAP32}else{throw new TypeError("Unknown boolean type size: "+name)}return this["fromWireType"](heap[pointer>>shift])},destructorFunction:null})}function __embind_register_constant(name,type,value){name=readLatin1String(name);whenDependentTypesAreResolved([],[type],function(type){type=type[0];Module[name]=type["fromWireType"](value);return[]})}var emval_free_list=[];var emval_handle_array=[{},{value:undefined},{value:null},{value:true},{value:false}];function __emval_decref(handle){if(handle>4&&0===--emval_handle_array[handle].refcount){emval_handle_array[handle]=undefined;emval_free_list.push(handle)}}function count_emval_handles(){var count=0;for(var i=5;i<emval_handle_array.length;++i){if(emval_handle_array[i]!==undefined){++count}}return count}function get_first_emval(){for(var i=5;i<emval_handle_array.length;++i){if(emval_handle_array[i]!==undefined){return emval_handle_array[i]}}return null}function init_emval(){Module["count_emval_handles"]=count_emval_handles;Module["get_first_emval"]=get_first_emval}var Emval={toValue:function(handle){if(!handle){throwBindingError("Cannot use deleted val. handle = "+handle)}return emval_handle_array[handle].value},toHandle:function(value){switch(value){case undefined:{return 1}case null:{return 2}case true:{return 3}case false:{return 4}default:{var handle=emval_free_list.length?emval_free_list.pop():emval_handle_array.length;emval_handle_array[handle]={refcount:1,value:value};return handle}}}};function __embind_register_emval(rawType,name){name=readLatin1String(name);registerType(rawType,{name:name,"fromWireType":function(handle){var rv=Emval.toValue(handle);__emval_decref(handle);return rv},"toWireType":function(destructors,value){return Emval.toHandle(value)},"argPackAdvance":8,"readValueFromPointer":simpleReadValueFromPointer,destructorFunction:null})}function ensureOverloadTable(proto,methodName,humanName){if(undefined===proto[methodName].overloadTable){var prevFunc=proto[methodName];proto[methodName]=function(){if(!proto[methodName].overloadTable.hasOwnProperty(arguments.length)){throwBindingError("Function '"+humanName+"' called with an invalid number of arguments ("+arguments.length+") - expects one of ("+proto[methodName].overloadTable+")!")}return proto[methodName].overloadTable[arguments.length].apply(this,arguments)};proto[methodName].overloadTable=[];proto[methodName].overloadTable[prevFunc.argCount]=prevFunc}}function exposePublicSymbol(name,value,numArguments){if(Module.hasOwnProperty(name)){if(undefined===numArguments||undefined!==Module[name].overloadTable&&undefined!==Module[name].overloadTable[numArguments]){throwBindingError("Cannot register public name '"+name+"' twice")}ensureOverloadTable(Module,name,name);if(Module.hasOwnProperty(numArguments)){throwBindingError("Cannot register multiple overloads of a function with the same number of arguments ("+numArguments+")!")}Module[name].overloadTable[numArguments]=value}else{Module[name]=value;if(undefined!==numArguments){Module[name].numArguments=numArguments}}}function enumReadValueFromPointer(name,shift,signed){switch(shift){case 0:return function(pointer){var heap=signed?HEAP8:HEAPU8;return this["fromWireType"](heap[pointer])};case 1:return function(pointer){var heap=signed?HEAP16:HEAPU16;return this["fromWireType"](heap[pointer>>1])};case 2:return function(pointer){var heap=signed?HEAP32:HEAPU32;return this["fromWireType"](heap[pointer>>2])};default:throw new TypeError("Unknown integer type: "+name)}}function __embind_register_enum(rawType,name,size,isSigned){var shift=getShiftFromSize(size);name=readLatin1String(name);function ctor(){}ctor.values={};registerType(rawType,{name:name,constructor:ctor,"fromWireType":function(c){return this.constructor.values[c]},"toWireType":function(destructors,c){return c.value},"argPackAdvance":8,"readValueFromPointer":enumReadValueFromPointer(name,shift,isSigned),destructorFunction:null});exposePublicSymbol(name,ctor)}function getTypeName(type){var ptr=___getTypeName(type);var rv=readLatin1String(ptr);_free(ptr);return rv}function requireRegisteredType(rawType,humanName){var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(humanName+" has unknown type "+getTypeName(rawType))}return impl}function __embind_register_enum_value(rawEnumType,name,enumValue){var enumType=requireRegisteredType(rawEnumType,"enum");name=readLatin1String(name);var Enum=enumType.constructor;var Value=Object.create(enumType.constructor.prototype,{value:{value:enumValue},constructor:{value:createNamedFunction(enumType.name+"_"+name,function(){})}});Enum.values[enumValue]=Value;Enum[name]=Value}function floatReadValueFromPointer(name,shift){switch(shift){case 2:return function(pointer){return this["fromWireType"](HEAPF32[pointer>>2])};case 3:return function(pointer){return this["fromWireType"](HEAPF64[pointer>>3])};default:throw new TypeError("Unknown float type: "+name)}}function __embind_register_float(rawType,name,size){var shift=getShiftFromSize(size);name=readLatin1String(name);registerType(rawType,{name:name,"fromWireType":function(value){return value},"toWireType":function(destructors,value){return value},"argPackAdvance":8,"readValueFromPointer":floatReadValueFromPointer(name,shift),destructorFunction:null})}function new_(constructor,argumentList){if(!(constructor instanceof Function)){throw new TypeError("new_ called with constructor type "+typeof constructor+" which is not a function")}var dummy=createNamedFunction(constructor.name||"unknownFunctionName",function(){});dummy.prototype=constructor.prototype;var obj=new dummy;var r=constructor.apply(obj,argumentList);return r instanceof Object?r:obj}function craftInvokerFunction(humanName,argTypes,classType,cppInvokerFunc,cppTargetFunc){var argCount=argTypes.length;if(argCount<2){throwBindingError("argTypes array size mismatch! Must at least get return value and 'this' types!")}var isClassMethodFunc=argTypes[1]!==null&&classType!==null;var needsDestructorStack=false;for(var i=1;i<argTypes.length;++i){if(argTypes[i]!==null&&argTypes[i].destructorFunction===undefined){needsDestructorStack=true;break}}var returns=argTypes[0].name!=="void";var argsList="";var argsListWired="";for(var i=0;i<argCount-2;++i){argsList+=(i!==0?", ":"")+"arg"+i;argsListWired+=(i!==0?", ":"")+"arg"+i+"Wired"}var invokerFnBody="return function "+makeLegalFunctionName(humanName)+"("+argsList+") {\n"+"if (arguments.length !== "+(argCount-2)+") {\n"+"throwBindingError('function "+humanName+" called with ' + arguments.length + ' arguments, expected "+(argCount-2)+" args!');\n"+"}\n";if(needsDestructorStack){invokerFnBody+="var destructors = [];\n"}var dtorStack=needsDestructorStack?"destructors":"null";var args1=["throwBindingError","invoker","fn","runDestructors","retType","classParam"];var args2=[throwBindingError,cppInvokerFunc,cppTargetFunc,runDestructors,argTypes[0],argTypes[1]];if(isClassMethodFunc){invokerFnBody+="var thisWired = classParam.toWireType("+dtorStack+", this);\n"}for(var i=0;i<argCount-2;++i){invokerFnBody+="var arg"+i+"Wired = argType"+i+".toWireType("+dtorStack+", arg"+i+"); // "+argTypes[i+2].name+"\n";args1.push("argType"+i);args2.push(argTypes[i+2])}if(isClassMethodFunc){argsListWired="thisWired"+(argsListWired.length>0?", ":"")+argsListWired}invokerFnBody+=(returns?"var rv = ":"")+"invoker(fn"+(argsListWired.length>0?", ":"")+argsListWired+");\n";if(needsDestructorStack){invokerFnBody+="runDestructors(destructors);\n"}else{for(var i=isClassMethodFunc?1:2;i<argTypes.length;++i){var paramName=i===1?"thisWired":"arg"+(i-2)+"Wired";if(argTypes[i].destructorFunction!==null){invokerFnBody+=paramName+"_dtor("+paramName+"); // "+argTypes[i].name+"\n";args1.push(paramName+"_dtor");args2.push(argTypes[i].destructorFunction)}}}if(returns){invokerFnBody+="var ret = retType.fromWireType(rv);\n"+"return ret;\n"}else{}invokerFnBody+="}\n";args1.push(invokerFnBody);var invokerFunction=new_(Function,args1).apply(null,args2);return invokerFunction}function heap32VectorToArray(count,firstElement){var array=[];for(var i=0;i<count;i++){array.push(HEAP32[(firstElement>>2)+i])}return array}function replacePublicSymbol(name,value,numArguments){if(!Module.hasOwnProperty(name)){throwInternalError("Replacing nonexistant public symbol")}if(undefined!==Module[name].overloadTable&&undefined!==numArguments){Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}}function dynCallLegacy(sig,ptr,args){var f=Module["dynCall_"+sig];return args&&args.length?f.apply(null,[ptr].concat(args)):f.call(null,ptr)}function dynCall(sig,ptr,args){if(sig.includes("j")){return dynCallLegacy(sig,ptr,args)}return getWasmTableEntry(ptr).apply(null,args)}function getDynCaller(sig,ptr){var argCache=[];return function(){argCache.length=arguments.length;for(var i=0;i<arguments.length;i++){argCache[i]=arguments[i]}return dynCall(sig,ptr,argCache)}}function embind__requireFunction(signature,rawFunction){signature=readLatin1String(signature);function makeDynCaller(){if(signature.includes("j")){return getDynCaller(signature,rawFunction)}return getWasmTableEntry(rawFunction)}var fp=makeDynCaller();if(typeof fp!=="function"){throwBindingError("unknown function pointer with signature "+signature+": "+rawFunction)}return fp}var UnboundTypeError=undefined;function throwUnboundTypeError(message,types){var unboundTypes=[];var seen={};function visit(type){if(seen[type]){return}if(registeredTypes[type]){return}if(typeDependencies[type]){typeDependencies[type].forEach(visit);return}unboundTypes.push(type);seen[type]=true}types.forEach(visit);throw new UnboundTypeError(message+": "+unboundTypes.map(getTypeName).join([", "]))}function __embind_register_function(name,argCount,rawArgTypesAddr,signature,rawInvoker,fn){var argTypes=heap32VectorToArray(argCount,rawArgTypesAddr);name=readLatin1String(name);rawInvoker=embind__requireFunction(signature,rawInvoker);exposePublicSymbol(name,function(){throwUnboundTypeError("Cannot call "+name+" due to unbound types",argTypes)},argCount-1);whenDependentTypesAreResolved([],argTypes,function(argTypes){var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));replacePublicSymbol(name,craftInvokerFunction(name,invokerArgsArray,null,rawInvoker,fn),argCount-1);return[]})}function integerReadValueFromPointer(name,shift,signed){switch(shift){case 0:return signed?function readS8FromPointer(pointer){return HEAP8[pointer]}:function readU8FromPointer(pointer){return HEAPU8[pointer]};case 1:return signed?function readS16FromPointer(pointer){return HEAP16[pointer>>1]}:function readU16FromPointer(pointer){return HEAPU16[pointer>>1]};case 2:return signed?function readS32FromPointer(pointer){return HEAP32[pointer>>2]}:function readU32FromPointer(pointer){return HEAPU32[pointer>>2]};default:throw new TypeError("Unknown integer type: "+name)}}function __embind_register_integer(primitiveType,name,size,minRange,maxRange){name=readLatin1String(name);if(maxRange===-1){maxRange=4294967295}var shift=getShiftFromSize(size);var fromWireType=value=>value;if(minRange===0){var bitshift=32-8*size;fromWireType=(value=>value<<bitshift>>>bitshift)}var isUnsignedType=name.includes("unsigned");var checkAssertions=(value,toTypeName)=>{};var toWireType;if(isUnsignedType){toWireType=function(destructors,value){checkAssertions(value,this.name);return value>>>0}}else{toWireType=function(destructors,value){checkAssertions(value,this.name);return value}}registerType(primitiveType,{name:name,"fromWireType":fromWireType,"toWireType":toWireType,"argPackAdvance":8,"readValueFromPointer":integerReadValueFromPointer(name,shift,minRange!==0),destructorFunction:null})}function __embind_register_memory_view(rawType,dataTypeIndex,name){var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){handle=handle>>2;var heap=HEAPU32;var size=heap[handle];var data=heap[handle+1];return new TA(buffer,data,size)}name=readLatin1String(name);registerType(rawType,{name:name,"fromWireType":decodeMemoryView,"argPackAdvance":8,"readValueFromPointer":decodeMemoryView},{ignoreDuplicateRegistrations:true})}function __embind_register_std_string(rawType,name){name=readLatin1String(name);var stdStringIsUTF8=name==="std::string";registerType(rawType,{name:name,"fromWireType":function(value){var length=HEAPU32[value>>2];var str;if(stdStringIsUTF8){var decodeStartPtr=value+4;for(var i=0;i<=length;++i){var currentBytePtr=value+4+i;if(i==length||HEAPU8[currentBytePtr]==0){var maxRead=currentBytePtr-decodeStartPtr;var stringSegment=UTF8ToString(decodeStartPtr,maxRead);if(str===undefined){str=stringSegment}else{str+=String.fromCharCode(0);str+=stringSegment}decodeStartPtr=currentBytePtr+1}}}else{var a=new Array(length);for(var i=0;i<length;++i){a[i]=String.fromCharCode(HEAPU8[value+4+i])}str=a.join("")}_free(value);return str},"toWireType":function(destructors,value){if(value instanceof ArrayBuffer){value=new Uint8Array(value)}var getLength;var valueIsOfTypeString=typeof value==="string";if(!(valueIsOfTypeString||value instanceof Uint8Array||value instanceof Uint8ClampedArray||value instanceof Int8Array)){throwBindingError("Cannot pass non-string to std::string")}if(stdStringIsUTF8&&valueIsOfTypeString){getLength=(()=>lengthBytesUTF8(value))}else{getLength=(()=>value.length)}var length=getLength();var ptr=_malloc(4+length+1);HEAPU32[ptr>>2]=length;if(stdStringIsUTF8&&valueIsOfTypeString){stringToUTF8(value,ptr+4,length+1)}else{if(valueIsOfTypeString){for(var i=0;i<length;++i){var charCode=value.charCodeAt(i);if(charCode>255){_free(ptr);throwBindingError("String has UTF-16 code units that do not fit in 8 bits")}HEAPU8[ptr+4+i]=charCode}}else{for(var i=0;i<length;++i){HEAPU8[ptr+4+i]=value[i]}}}if(destructors!==null){destructors.push(_free,ptr)}return ptr},"argPackAdvance":8,"readValueFromPointer":simpleReadValueFromPointer,destructorFunction:function(ptr){_free(ptr)}})}function __embind_register_std_wstring(rawType,charSize,name){name=readLatin1String(name);var decodeString,encodeString,getHeap,lengthBytesUTF,shift;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16;getHeap=(()=>HEAPU16);shift=1}else if(charSize===4){decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32;getHeap=(()=>HEAPU32);shift=2}registerType(rawType,{name:name,"fromWireType":function(value){var length=HEAPU32[value>>2];var HEAP=getHeap();var str;var decodeStartPtr=value+4;for(var i=0;i<=length;++i){var currentBytePtr=value+4+i*charSize;if(i==length||HEAP[currentBytePtr>>shift]==0){var maxReadBytes=currentBytePtr-decodeStartPtr;var stringSegment=decodeString(decodeStartPtr,maxReadBytes);if(str===undefined){str=stringSegment}else{str+=String.fromCharCode(0);str+=stringSegment}decodeStartPtr=currentBytePtr+charSize}}_free(value);return str},"toWireType":function(destructors,value){if(!(typeof value==="string")){throwBindingError("Cannot pass non-string to C++ string type "+name)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length>>shift;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},"argPackAdvance":8,"readValueFromPointer":simpleReadValueFromPointer,destructorFunction:function(ptr){_free(ptr)}})}function __embind_register_value_object(rawType,name,constructorSignature,rawConstructor,destructorSignature,rawDestructor){structRegistrations[rawType]={name:readLatin1String(name),rawConstructor:embind__requireFunction(constructorSignature,rawConstructor),rawDestructor:embind__requireFunction(destructorSignature,rawDestructor),fields:[]}}function __embind_register_value_object_field(structType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext){structRegistrations[structType].fields.push({fieldName:readLatin1String(fieldName),getterReturnType:getterReturnType,getter:embind__requireFunction(getterSignature,getter),getterContext:getterContext,setterArgumentType:setterArgumentType,setter:embind__requireFunction(setterSignature,setter),setterContext:setterContext})}function __embind_register_void(rawType,name){name=readLatin1String(name);registerType(rawType,{isVoid:true,name:name,"argPackAdvance":0,"fromWireType":function(){return undefined},"toWireType":function(destructors,o){return undefined}})}function __emval_incref(handle){if(handle>4){emval_handle_array[handle].refcount+=1}}function __emval_take_value(type,argv){type=requireRegisteredType(type,"_emval_take_value");var v=type["readValueFromPointer"](argv);return Emval.toHandle(v)}function _abort(){abort("")}function _emscripten_memcpy_big(dest,src,num){HEAPU8.copyWithin(dest,src,src+num)}function emscripten_realloc_buffer(size){try{wasmMemory.grow(size-buffer.byteLength+65535>>>16);updateGlobalBufferAndViews(wasmMemory.buffer);return 1}catch(e){}}function _emscripten_resize_heap(requestedSize){var oldSize=HEAPU8.length;requestedSize=requestedSize>>>0;var maxHeapSize=2147483648;if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignUp(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=emscripten_realloc_buffer(newSize);if(replacement){return true}}return false}var SYSCALLS={mappings:{},buffers:[null,[],[]],printChar:function(stream,curr){var buffer=SYSCALLS.buffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer,0));buffer.length=0}else{buffer.push(curr)}},varargs:undefined,get:function(){SYSCALLS.varargs+=4;var ret=HEAP32[SYSCALLS.varargs-4>>2];return ret},getStr:function(ptr){var ret=UTF8ToString(ptr);return ret},get64:function(low,high){return low}};function _fd_close(fd){return 0}function _fd_seek(fd,offset_low,offset_high,whence,newOffset){}function _fd_write(fd,iov,iovcnt,pnum){var num=0;for(var i=0;i<iovcnt;i++){var ptr=HEAP32[iov>>2];var len=HEAP32[iov+4>>2];iov+=8;for(var j=0;j<len;j++){SYSCALLS.printChar(fd,HEAPU8[ptr+j])}num+=len}HEAP32[pnum>>2]=num;return 0}function _setTempRet0(val){setTempRet0(val)}InternalError=Module["InternalError"]=extendError(Error,"InternalError");embind_init_charCodes();BindingError=Module["BindingError"]=extendError(Error,"BindingError");init_emval();UnboundTypeError=Module["UnboundTypeError"]=extendError(Error,"UnboundTypeError");var ASSERTIONS=false;function intArrayToString(array){var ret=[];for(var i=0;i<array.length;i++){var chr=array[i];if(chr>255){if(ASSERTIONS){assert(false,"Character code "+chr+" ("+String.fromCharCode(chr)+") at offset "+i+" not in 0x00-0xFF.")}chr&=255}ret.push(String.fromCharCode(chr))}return ret.join("")}var decodeBase64=typeof atob==="function"?atob:function(input){var keyStr="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";var output="";var chr1,chr2,chr3;var enc1,enc2,enc3,enc4;var i=0;input=input.replace(/[^A-Za-z0-9\+\/\=]/g,"");do{enc1=keyStr.indexOf(input.charAt(i++));enc2=keyStr.indexOf(input.charAt(i++));enc3=keyStr.indexOf(input.charAt(i++));enc4=keyStr.indexOf(input.charAt(i++));chr1=enc1<<2|enc2>>4;chr2=(enc2&15)<<4|enc3>>2;chr3=(enc3&3)<<6|enc4;output=output+String.fromCharCode(chr1);if(enc3!==64){output=output+String.fromCharCode(chr2)}if(enc4!==64){output=output+String.fromCharCode(chr3)}}while(i<input.length);return output};function intArrayFromBase64(s){if(typeof ENVIRONMENT_IS_NODE==="boolean"&&ENVIRONMENT_IS_NODE){var buf=Buffer.from(s,"base64");return new Uint8Array(buf["buffer"],buf["byteOffset"],buf["byteLength"])}try{var decoded=decodeBase64(s);var bytes=new Uint8Array(decoded.length);for(var i=0;i<decoded.length;++i){bytes[i]=decoded.charCodeAt(i)}return bytes}catch(_){throw new Error("Converting base64 string to bytes failed.")}}function tryParseAsDataURI(filename){if(!isDataURI(filename)){return}return intArrayFromBase64(filename.slice(dataURIPrefix.length))}var asmLibraryArg={"p":__embind_finalize_value_object,"s":__embind_register_bigint,"y":__embind_register_bool,"f":__embind_register_constant,"x":__embind_register_emval,"o":__embind_register_enum,"a":__embind_register_enum_value,"n":__embind_register_float,"c":__embind_register_function,"e":__embind_register_integer,"b":__embind_register_memory_view,"m":__embind_register_std_string,"i":__embind_register_std_wstring,"q":__embind_register_value_object,"d":__embind_register_value_object_field,"z":__embind_register_void,"j":__emval_decref,"k":__emval_incref,"g":__emval_take_value,"h":_abort,"w":_emscripten_memcpy_big,"u":_emscripten_resize_heap,"v":_fd_close,"r":_fd_seek,"l":_fd_write,"t":_setTempRet0};var asm=createWasm();var ___wasm_call_ctors=Module["___wasm_call_ctors"]=function(){return(___wasm_call_ctors=Module["___wasm_call_ctors"]=Module["asm"]["B"]).apply(null,arguments)};var _free=Module["_free"]=function(){return(_free=Module["_free"]=Module["asm"]["D"]).apply(null,arguments)};var _malloc=Module["_malloc"]=function(){return(_malloc=Module["_malloc"]=Module["asm"]["E"]).apply(null,arguments)};var ___getTypeName=Module["___getTypeName"]=function(){return(___getTypeName=Module["___getTypeName"]=Module["asm"]["F"]).apply(null,arguments)};var ___embind_register_native_and_builtin_types=Module["___embind_register_native_and_builtin_types"]=function(){return(___embind_register_native_and_builtin_types=Module["___embind_register_native_and_builtin_types"]=Module["asm"]["G"]).apply(null,arguments)};var dynCall_jiji=Module["dynCall_jiji"]=function(){return(dynCall_jiji=Module["dynCall_jiji"]=Module["asm"]["H"]).apply(null,arguments)};var calledRun;function ExitStatus(status){this.name="ExitStatus";this.message="Program terminated with exit("+status+")";this.status=status}dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(args){args=args||arguments_;if(runDependencies>0){return}preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve(Module);if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(function(){setTimeout(function(){Module["setStatus"]("")},1);doRun()},1)}else{doRun()}}Module["run"]=run;if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();14-15-16- return ggwave_factory.ready17-}18-);19-})();20-if (typeof exports === 'object' && typeof module === 'object')21- module.exports = ggwave_factory;22-else if (typeof define === 'function' && define['amd'])23- define([], function() { return ggwave_factory; });24-else if (typeof exports === 'object')25- exports["ggwave_factory"] = ggwave_factory;26-27-// peach-browser patch: expose factory on globalThis for ES-module dynamic import().28-if (typeof globalThis !== 'undefined') globalThis.ggwave_factory = ggwave_factory;29-src/background/chirp-sync-static.test.ts+11 −13
@@ -64,23 +64,21 @@6464 expect(entrySrc).toContain("productionSyncRunner");6565 });6666 67- it("sync-trigger filters to trustSource:'direct' — vouched devices excluded", () => {67+ it("sync-trigger syncs both direct and user-confirmed vouched hosts", () => {6868 const triggerSrc = src("./sync-trigger.ts");69- expect(triggerSrc).toContain('"direct"');70- expect(triggerSrc).toMatch(/trustSource.*direct|direct.*trustSource/);69+ expect(triggerSrc).toMatch(/trustSource === "direct"/);70+ // Mesh introduction is pointless if a device the user confirmed out of the71+ // MV-7 pending queue is then never dialled.72+ expect(triggerSrc).toMatch(/trustSource === "vouched"/);7173 });7274 73- it("App.tsx wires startMDNSListener with onCleanup registered before the import resolves", () => {75+ it("the browser never listens — no pairing server or audio-chirp module survives", () => {7476 const appSrc = src("../popup/App.tsx");75- expect(appSrc).toContain("startMDNSListener");76- expect(appSrc).toContain("onCleanup");77- // onCleanup must appear BEFORE the .then() call that starts the listener,78- // i.e. stopMDNS must be captured into the closure before dynamic import.79- const cleanupIdx = appSrc.indexOf("onCleanup(() => stopMDNS");80- const thenIdx = appSrc.indexOf("startMDNSListener");81- expect(cleanupIdx).toBeGreaterThan(-1);82- expect(thenIdx).toBeGreaterThan(-1);83- expect(cleanupIdx).toBeLessThan(thenIdx);77+ expect(appSrc).not.toContain("startMDNSListener");78+ expect(appSrc).not.toContain("openPairingServer");79+ expect(appSrc).not.toContain("chirp-pair-capacitor");80+ // Adoption dials out like everything else now.81+ expect(appSrc).toContain("dialPairingChannel");8482 });8583 8684 it("vault_id uses the full 64-char BLAKE3 hex digest", () => {src/popup/App.tsx+5 −9
@@ -199,7 +199,7 @@199199 const [settingsInitialSection, setSettingsInitialSection] = createSignal<string | null>(null);200200 // When the sync card's "Chirp"/"Code" buttons open the sync view, deep-link201201 // straight into that pairing path instead of the rest screen.202- const [syncPairIntent, setSyncPairIntent] = createSignal<"chirp" | "desktop" | null>(null);202+ const [syncPairIntent, setSyncPairIntent] = createSignal<"desktop" | null>(null);203203 const [settingsActiveCard, setSettingsActiveCard] = createSignal<string>("protection");204204 const [upgradeHighlight, setUpgradeHighlight] = createSignal<ProFeature | null>(null);205205 const [sortOrder, setSortOrder] = createSignal<SortOrder>("url-first");@@ -581,11 +581,10 @@581581 }582582 });583583 584- // mDNS peer-discovery trigger. Register onCleanup synchronously so585- // SolidJS captures it within the reactive owner — onCleanup inside a586- // .then() callback may miss the capture window and leak the listener.587- let stopMDNS: () => void = () => undefined;588- onCleanup(() => stopMDNS());584+ // The mDNS peer-discovery trigger went with the Capacitor pairing585+ // plugin. Reconnection is address-cache first, subnet probe second586+ // (chirp-sync-runner.ts); relocating a peer that changed networks is an587+ // Android-side discovery concern now.589588 if (isNativePlatform()) {590589 // Android runs the background/sync dispatcher in-process with the591590 // popup, so the production runner has to be registered from the@@ -597,9 +596,6 @@597596 })598597 );599598 }600- void import("./chirp-pair-capacitor").then(({ startMDNSListener }) => {601- stopMDNS = startMDNSListener();602- });603599 }604600 605601 // Background auto-lock broadcasts VAULT_LOCKED so the popup drops stalesrc/popup/chirp-pair-capacitor.test.ts+0 −108
@@ -1,108 +0,0 @@1-import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";2-import { startMDNSListener } from "./chirp-pair-capacitor";3-4-interface RegisteredListener {5- event: string;6- handler: (data: unknown) => void;7-}8-9-function installMockCapacitor(opts: {10- withSyncPlugin?: boolean;11- withPeerDiscoveredEvent?: boolean;12- appActive?: boolean;13-} = {}): { listeners: RegisteredListener[]; removeCount: { v: number } } {14- const listeners: RegisteredListener[] = [];15- const removeCount = { v: 0 };16- const syncPlugin = opts.withSyncPlugin === false ? undefined : {17- startPairingServer: vi.fn(),18- stopPairingServer: vi.fn(),19- sendFrame: vi.fn(),20- addListener: opts.withPeerDiscoveredEvent === false21- ? undefined22- : vi.fn((event: string, handler: (data: unknown) => void) => {23- listeners.push({ event, handler });24- return { remove: () => { removeCount.v++; } };25- }),26- };27- const appPlugin = {28- getState: vi.fn(async () => ({ isActive: opts.appActive ?? true })),29- };30- (globalThis as { Capacitor?: unknown }).Capacitor = {31- isNativePlatform: () => true,32- Plugins: { SyncPlugin: syncPlugin, App: appPlugin },33- };34- return { listeners, removeCount };35-}36-37-describe("startMDNSListener", () => {38- let sentMessages: Array<{ type: string; payload?: unknown }>;39-40- beforeEach(() => {41- sentMessages = [];42- (globalThis as { chrome?: unknown }).chrome = {43- runtime: {44- sendMessage: vi.fn(async (msg: { type: string; payload?: unknown }) => {45- sentMessages.push(msg);46- }),47- },48- };49- });50-51- afterEach(() => {52- delete (globalThis as { Capacitor?: unknown }).Capacitor;53- delete (globalThis as { chrome?: unknown }).chrome;54- });55-56- it("is a no-op when the SyncPlugin is unavailable", () => {57- installMockCapacitor({ withSyncPlugin: false });58- const stop = startMDNSListener();59- expect(typeof stop).toBe("function");60- stop();61- });62-63- it("registers a peerDiscovered listener when the plugin exposes addListener", () => {64- const { listeners } = installMockCapacitor();65- startMDNSListener();66- expect(listeners.length).toBe(1);67- expect(listeners[0].event).toBe("peerDiscovered");68- });69-70- it("fires TRIGGER_SYNC with reason 'mdns' when peerDiscovered arrives in the foreground", async () => {71- const { listeners } = installMockCapacitor({ appActive: true });72- startMDNSListener();73- listeners[0].handler({ deviceId: "pixel-7", ip: "192.168.1.42", port: 8765 });74- // sendMessage runs inside the handler's `await`; let microtasks flush.75- await new Promise((r) => setTimeout(r, 0));76- expect(sentMessages).toEqual([77- {78- type: "TRIGGER_SYNC",79- payload: { reason: "mdns", deviceId: "pixel-7", discoveredIp: "192.168.1.42", discoveredPort: 8765 },80- },81- ]);82- });83-84- it("does NOT fire TRIGGER_SYNC when the app is backgrounded", async () => {85- const { listeners } = installMockCapacitor({ appActive: false });86- startMDNSListener();87- listeners[0].handler({ deviceId: "pixel-7", ip: "192.168.1.42", port: 8765 });88- await new Promise((r) => setTimeout(r, 0));89- expect(sentMessages).toEqual([]);90- });91-92- it("ignores malformed events (missing deviceId or ip)", async () => {93- const { listeners } = installMockCapacitor({ appActive: true });94- startMDNSListener();95- listeners[0].handler({ ip: "192.168.1.42" });96- listeners[0].handler({ deviceId: "pixel-7" });97- listeners[0].handler(null);98- await new Promise((r) => setTimeout(r, 0));99- expect(sentMessages).toEqual([]);100- });101-102- it("returns a teardown that removes the native listener", () => {103- const { removeCount } = installMockCapacitor();104- const stop = startMDNSListener();105- stop();106- expect(removeCount.v).toBe(1);107- });108-});src/popup/chirp-pair-capacitor.ts+0 −213
@@ -1,213 +0,0 @@1-// Capacitor adapter for the initiator-role pairing server, plus the2-// mDNS peer-discovery listener that fires the sync trigger when a known3-// paired device appears on the local network.4-//5-// The native SyncPlugin on Android (and eventually iOS) owns the6-// WebSocket server on CHIRP_PAIRING_PORT. JS does not have raw socket7-// listeners. This adapter wraps the plugin's start/stop calls and its8-// event stream into the PairingTransport shape the orchestrator expects.9-//10-// Protocol with native:11-// - startPairingServer({port}) opens the server and returns immediately12-// - The plugin emits 'peach:pair-connected' when the first peer connects:13-// { connectionId: string, peerIp: string }14-// - The plugin emits 'peach:pair-frame' for each inbound binary frame:15-// { connectionId: string, frameB64: string }16-// - The plugin emits 'peach:pair-closed' when the connection drops:17-// { connectionId: string }18-// - sendFrame({connectionId, frameB64}) sends a binary frame out19-// - stopPairingServer() closes the server and any open connection20-//21-// The 90 s accept timeout matches the chirp window. If no peer connects22-// in that window the adapter rejects with a `pair_timeout` error so the23-// orchestrator surfaces a `handshake_failed` phase.24-25-import type { PairingTransport } from "./chirp-pair";26-import { CHIRP_PAIRING_PORT } from "./chirp-pair";27-28-const ACCEPT_TIMEOUT_MS = 90_000;29-30-interface SyncPluginShape {31- startPairingServer(opts: { port: number }): Promise<void>;32- stopPairingServer(): Promise<void>;33- sendFrame(opts: { connectionId: string; frameB64: string }): Promise<void>;34- addListener(event: string, handler: (data: unknown) => void): { remove: () => void };35-}36-37-function getSyncPlugin(): SyncPluginShape | null {38- const cap = (globalThis as { Capacitor?: { Plugins?: Record<string, unknown> } }).Capacitor;39- const plugin = cap?.Plugins?.SyncPlugin as SyncPluginShape | undefined;40- return plugin ?? null;41-}42-43-export async function openCapacitorPairingServer(): Promise<{44- transport: PairingTransport;45- peerIp: string;46-}> {47- const plugin = getSyncPlugin();48- if (!plugin) throw new Error("SyncPlugin unavailable — not running on Capacitor");49-50- await plugin.startPairingServer({ port: CHIRP_PAIRING_PORT });51-52- let connectionId: string | null = null;53- const inbox: Uint8Array[] = [];54- const waiters: Array<(b: Uint8Array | null) => void> = [];55- let closed = false;56-57- const onConnected = plugin.addListener("peach:pair-connected", (data) => {58- const d = data as { connectionId: string; peerIp: string };59- if (connectionId !== null) return; // ignore extra connections — first wins60- connectionId = d.connectionId;61- acceptResolve?.({ connectionId: d.connectionId, peerIp: d.peerIp });62- });63-64- const onFrame = plugin.addListener("peach:pair-frame", (data) => {65- const d = data as { connectionId: string; frameB64: string };66- if (d.connectionId !== connectionId) return;67- const bytes = base64ToBytes(d.frameB64);68- const w = waiters.shift();69- if (w) w(bytes); else inbox.push(bytes);70- });71-72- const onClosed = plugin.addListener("peach:pair-closed", (data) => {73- const d = data as { connectionId: string };74- if (d.connectionId !== connectionId) return;75- closed = true;76- while (waiters.length > 0) waiters.shift()!(null);77- });78-79- let acceptResolve: ((v: { connectionId: string; peerIp: string }) => void) | null = null;80- let acceptReject: ((e: Error) => void) | null = null;81- const acceptPromise = new Promise<{ connectionId: string; peerIp: string }>((res, rej) => {82- acceptResolve = res;83- acceptReject = rej;84- });85- const timer = setTimeout(() => acceptReject?.(new Error("pair_timeout")), ACCEPT_TIMEOUT_MS);86-87- let accepted: { connectionId: string; peerIp: string };88- try {89- accepted = await acceptPromise;90- } catch (err) {91- clearTimeout(timer);92- onConnected.remove(); onFrame.remove(); onClosed.remove();93- await plugin.stopPairingServer().catch(() => undefined);94- throw err;95- }96- clearTimeout(timer);97-98- const transport: PairingTransport = {99- async send(msg: Uint8Array) {100- await plugin.sendFrame({101- connectionId: accepted.connectionId,102- frameB64: bytesToBase64(msg),103- });104- },105- async receive(): Promise<Uint8Array> {106- if (closed) throw new Error("pair_transport_closed");107- const v = inbox.shift();108- if (v) return v;109- return new Promise<Uint8Array>((resolve, reject) => {110- waiters.push((bytes) => {111- if (bytes === null) reject(new Error("pair_transport_closed"));112- else resolve(bytes);113- });114- });115- },116- async close() {117- onConnected.remove(); onFrame.remove(); onClosed.remove();118- await plugin.stopPairingServer().catch(() => undefined);119- },120- };121-122- return { transport, peerIp: accepted.peerIp };123-}124-125-/**126- * Subscribe to mDNS peer-discovery events from the native SyncPlugin.127- * Returns a teardown that should be called from `onCleanup`. Each128- * `peerDiscovered` payload identifies a paired device by deviceId plus the129- * (ip, port) the native side just resolved; we update the device's cached130- * IP and fire a `TRIGGER_SYNC` with reason "mdns".131- *132- * Gracefully no-op if the SyncPlugin doesn't expose the event yet — the133- * Android side may or may not have shipped the emitter; the JS-side wiring134- * stays in place either way so we don't need a coordinated rollout.135- */136-export function startMDNSListener(): () => void {137- const plugin = getSyncPluginExtended();138- if (!plugin?.addListener) return () => undefined;139- let sub: { remove: () => void } | null = null;140- try {141- sub = plugin.addListener("peerDiscovered", (data: unknown) => {142- void handlePeerDiscovered(data);143- });144- } catch {145- return () => undefined;146- }147- return () => { try { sub?.remove(); } catch { /* ignore */ } };148-}149-150-async function handlePeerDiscovered(data: unknown): Promise<void> {151- const event = data as { deviceId?: string; ip?: string; port?: number } | null | undefined;152- if (!event?.deviceId || typeof event.ip !== "string") return;153-154- // Foreground guard — when the OS has backgrounded the app, mDNS resolves155- // are still delivered but firing a sync now would burn battery for no156- // user-visible benefit. The next foreground entry will trigger sync via157- // the existing popup-foreground path.158- const appActive = await isAppActive();159- if (!appActive) return;160-161- try {162- // Pass the resolved address so the background can update lastKnownIp163- // before attempting the session — saves the runner from a stale-IP164- // probe on the very next sync round.165- await chrome.runtime.sendMessage({166- type: "TRIGGER_SYNC",167- payload: {168- reason: "mdns",169- deviceId: event.deviceId,170- discoveredIp: event.ip,171- discoveredPort: typeof event.port === "number" ? event.port : undefined,172- },173- });174- } catch {175- // Background may not be listening — non-fatal; next vault-change or176- // foreground entry will pick up the work.177- }178-}179-180-interface ExtendedSyncPlugin extends SyncPluginShape {181- addListener(event: "peerDiscovered", handler: (data: unknown) => void): { remove: () => void };182-}183-184-function getSyncPluginExtended(): ExtendedSyncPlugin | null {185- return getSyncPlugin() as ExtendedSyncPlugin | null;186-}187-188-async function isAppActive(): Promise<boolean> {189- const cap = (globalThis as { Capacitor?: { Plugins?: Record<string, unknown> } }).Capacitor;190- const appPlugin = cap?.Plugins?.App as191- | { getState?: () => Promise<{ isActive: boolean }> }192- | undefined;193- if (!appPlugin?.getState) return true;194- try {195- const state = await appPlugin.getState();196- return state.isActive !== false;197- } catch {198- return true;199- }200-}201-202-function base64ToBytes(b64: string): Uint8Array {203- const bin = atob(b64);204- const out = new Uint8Array(bin.length);205- for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);206- return out;207-}208-209-function bytesToBase64(bytes: Uint8Array): string {210- let bin = "";211- for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);212- return btoa(bin);213-}src/popup/chirp-pair.adopt-source.test.ts+0 −265
@@ -1,265 +0,0 @@1-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";2-import { computeVaultId } from "../shared/sync-identity";3-import { routeSourcePostSASFrame, runChirpAdoptionReceiver } from "./chirp-pair";4-import {5- memoryTransportPair,6- runPairingResponderOpenFrame,7- type LocalIdentity,8- type PairingTransport,9-} from "../shared/chirp-handshake";10-import { createPairingAttempt } from "../shared/chirp-pairing";11-import { __setGgwaveModuleForTest } from "../shared/ggwave";12-import { encodeChirpPayload } from "../shared/chirp-payload";13-import { CHIRP_PAYLOAD_LENGTH, DEVICE_TYPE_EXTENSION, type ChirpPayload } from "../shared/sync-types";14-15-const localIdentity: LocalIdentity = {16- deviceId: "source-device",17- deviceName: "Source",18- deviceType: "extension",19- longTermPublicKeyHex: "ab".repeat(32),20- signingPublicKeyHex: "cd".repeat(32),21-};22-23-function channel(): PairingTransport & { sent: Uint8Array[]; close: ReturnType<typeof vi.fn> } {24- const sent: Uint8Array[] = [];25- return {26- sent,27- close: vi.fn(async () => {}),28- async send(msg) { sent.push(msg); },29- async receive() {30- return new TextEncoder().encode(JSON.stringify({31- type: "ADOPT_IDENTITY",32- device_id: "receiver-device",33- device_name: "Receiver",34- device_type: "extension",35- long_term_public_key_hex: "ef".repeat(32),36- }));37- },38- };39-}40-41-function fakeGgwave() {42- return {43- ProtocolId: { GGWAVE_PROTOCOL_ULTRASOUND_FASTEST: 5 },44- GGWAVE_OPERATING_MODE_RX: 1,45- GGWAVE_OPERATING_MODE_TX: 2,46- GGWAVE_OPERATING_MODE_RX_AND_TX: 3,47- getDefaultParameters() {48- return {49- sampleRateInp: 48000,50- sampleRateOut: 48000,51- samplesPerFrame: 1024,52- soundMarkerThreshold: 3,53- payloadLength: -1,54- operatingMode: 0,55- };56- },57- init() { return 1; },58- free() {},59- encode(_inst: number, data: Uint8Array | string) {60- const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;61- const out = new Float32Array(Math.max(bytes.length, 128));62- for (let i = 0; i < bytes.length; i++) out[i] = bytes[i] / 255;63- for (let i = bytes.length; i < out.length; i++) out[i] = 99;64- return out;65- },66- decode(_inst: number, samples: Float32Array | Int16Array) {67- if (samples.length < CHIRP_PAYLOAD_LENGTH) return null;68- const buf = new Uint8Array(CHIRP_PAYLOAD_LENGTH);69- for (let i = 0; i < CHIRP_PAYLOAD_LENGTH; i++) {70- const v = (samples as Float32Array)[i];71- if (v === 99) return null;72- buf[i] = Math.round(v * 255);73- }74- return buf;75- },76- };77-}78-79-function fakeAudio(injectedChirp: ChirpPayload) {80- const bytes = encodeChirpPayload(injectedChirp);81- let pending = new Float32Array(128);82- for (let i = 0; i < bytes.length; i++) pending[i] = bytes[i] / 255;83- for (let i = bytes.length; i < pending.length; i++) pending[i] = 99;84- return async () => ({85- sampleRate: 48000,86- async play() { /* no-op */ },87- async pullSamples() {88- const out = pending;89- pending = new Float32Array(0);90- return out;91- },92- async close() { /* no-op */ },93- });94-}95-96-describe("source-side ADOPT_REQUEST routing", () => {97- beforeEach(() => __setGgwaveModuleForTest(fakeGgwave()));98- afterEach(() => __setGgwaveModuleForTest(null));99-100- it("T-A26: routes ADOPT_REQUEST to grantAdoption after SAS confirmation and consent", async () => {101- const rootKeyHex = "77".repeat(32);102- const ch = channel();103- const completePairing = vi.fn();104- const result = await routeSourcePostSASFrame(105- {106- sas: "895245",107- frame: {108- type: "ADOPT_REQUEST",109- device_id: "receiver-ephemeral",110- device_name: "Receiver",111- device_type: "extension",112- },113- channel: ch,114- },115- localIdentity,116- {117- confirmSAS: vi.fn(async () => true),118- completePairing,119- grantAdoptionConsent: vi.fn(async () => true),120- fetchAdoptionMaterial: vi.fn(async () => ({121- rootKeyHex,122- vaultId: await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex"))),123- license: "PPK-test",124- })),125- onProgress: vi.fn(),126- },127- );128-129- const grant = JSON.parse(new TextDecoder().decode(ch.sent[0]));130- expect(result).toEqual(expect.objectContaining({ deviceId: "receiver-device" }));131- expect(grant).toMatchObject({132- type: "ADOPT_GRANT",133- root_key_hex: rootKeyHex,134- vault_id: await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex"))),135- license: "PPK-test",136- });137- expect(completePairing).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "receiver-device" }));138- });139-140- it("T-A27: routes PAIR_IDENTITY to the existing pairing path", async () => {141- const ch = channel();142- const completePairing = vi.fn();143- const result = await routeSourcePostSASFrame(144- {145- sas: "895245",146- frame: {147- type: "PAIR_IDENTITY",148- device_id: "phone",149- device_name: "Phone",150- device_type: "android",151- long_term_public_key_hex: "aa".repeat(32),152- },153- channel: ch,154- },155- localIdentity,156- {157- confirmSAS: vi.fn(async () => true),158- completePairing,159- onProgress: vi.fn(),160- },161- );162-163- const response = JSON.parse(new TextDecoder().decode(ch.sent[0]));164- expect(response.type).toBe("PAIR_IDENTITY");165- expect(result).toEqual(expect.objectContaining({ deviceId: "phone" }));166- expect(completePairing).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "phone" }));167- });168-169- it("T-A28: unknown frame closes the channel gracefully", async () => {170- const ch = channel();171- await expect(routeSourcePostSASFrame(172- { sas: "895245", frame: { type: "WHAT_IS_THIS" }, channel: ch },173- localIdentity,174- {175- confirmSAS: vi.fn(async () => true),176- completePairing: vi.fn(),177- onProgress: vi.fn(),178- },179- )).rejects.toThrow("unexpected_pairing_frame");180-181- expect(ch.close).toHaveBeenCalledTimes(1);182- });183-184- it("T-A29: source root key bytes are zeroed after grantAdoption regardless of outcome", async () => {185- const fillSpy = vi.spyOn(Uint8Array.prototype, "fill");186- await routeSourcePostSASFrame(187- {188- sas: "895245",189- frame: {190- type: "ADOPT_REQUEST",191- device_id: "receiver-ephemeral",192- device_name: "Receiver",193- device_type: "extension",194- },195- channel: channel(),196- },197- localIdentity,198- {199- confirmSAS: vi.fn(async () => true),200- completePairing: vi.fn(),201- grantAdoptionConsent: vi.fn(async () => true),202- fetchAdoptionMaterial: vi.fn(async () => ({203- rootKeyHex: "88".repeat(32),204- vaultId: await computeVaultId(Uint8Array.from(Buffer.from("88".repeat(32), "hex"))),205- })),206- onProgress: vi.fn(),207- },208- );209-210- expect(fillSpy).toHaveBeenCalledWith(0);211- });212-213- it("T-A33: receiver onboarding orchestrator sends ADOPT_REQUEST and persists the granted source", async () => {214- const sourceAttempt = createPairingAttempt(DEVICE_TYPE_EXTENSION);215- const [receiverTransport, sourceTransport] = memoryTransportPair();216- const rootKeyHex = "99".repeat(32);217- const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));218- const receiverUpsert = vi.fn();219- const sourceUpsert = vi.fn();220-221- const source = (async () => {222- const postNoise = await runPairingResponderOpenFrame(223- sourceAttempt.ephemeralPrivateKey,224- sourceAttempt.ephemeralPublicKey,225- sourceTransport,226- );227- return routeSourcePostSASFrame(postNoise, localIdentity, {228- confirmSAS: vi.fn(async () => true),229- completePairing: sourceUpsert,230- grantAdoptionConsent: vi.fn(async () => true),231- fetchAdoptionMaterial: vi.fn(async () => ({ rootKeyHex, vaultId })),232- onProgress: vi.fn(),233- });234- })();235-236- const receiver = await runChirpAdoptionReceiver({237- deviceName: "Receiver",238- deviceType: "extension",239- openAudio: fakeAudio(sourceAttempt.outgoingPayload),240- openPairingServer: vi.fn(async () => ({241- transport: { ...receiverTransport, close: vi.fn(async () => {}) },242- peerIp: "127.0.0.1",243- })),244- confirmSAS: vi.fn(async () => true),245- createVaultFromRootKey: vi.fn(async (adoptedRootKeyHex) => {246- expect(adoptedRootKeyHex).toBe(rootKeyHex);247- return { vaultId };248- }),249- deriveLocalIdentity: vi.fn(async () => ({250- deviceId: "receiver-device",251- deviceName: "Receiver",252- deviceType: "extension" as const,253- longTermPublicKeyHex: "ef".repeat(32),254- signingPublicKeyHex: "12".repeat(32),255- })),256- completePairing: receiverUpsert,257- onProgress: vi.fn(),258- });259- await source;260-261- expect(receiver).toEqual(expect.objectContaining({ deviceId: "source-device" }));262- expect(receiverUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "source-device" }));263- expect(sourceUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "receiver-device" }));264- });265-});src/popup/chirp-pair.test.ts+0 −218
@@ -1,218 +0,0 @@1-import { afterEach, beforeEach, describe, expect, it } from "vitest";2-import { runChirpPairing, ChirpPairingFailure, gateOnSAS } from "./chirp-pair";3-import { __setGgwaveModuleForTest } from "../shared/ggwave";4-import { encodeChirpPayload } from "../shared/chirp-payload";5-import { generateKeyPair } from "@stablelib/x25519";6-import {7- CHIRP_PAYLOAD_LENGTH,8- DEVICE_TYPE_ANDROID,9- type ChirpPayload,10- type PairingProgress,11-} from "../shared/sync-types";12-13-function fakeGgwave() {14- return {15- ProtocolId: { GGWAVE_PROTOCOL_ULTRASOUND_FASTEST: 5 },16- GGWAVE_OPERATING_MODE_RX: 1, GGWAVE_OPERATING_MODE_TX: 2, GGWAVE_OPERATING_MODE_RX_AND_TX: 3,17- getDefaultParameters() {18- return { sampleRateInp: 48000, sampleRateOut: 48000, samplesPerFrame: 1024, soundMarkerThreshold: 3, payloadLength: -1, operatingMode: 0 };19- },20- init() { return 1; },21- free() {},22- encode(_inst: number, data: Uint8Array | string) {23- const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;24- const out = new Float32Array(Math.max(bytes.length, 128));25- for (let i = 0; i < bytes.length; i++) out[i] = bytes[i] / 255;26- for (let i = bytes.length; i < out.length; i++) out[i] = 99;27- return out;28- },29- decode(_inst: number, samples: Float32Array | Int16Array) {30- if (samples.length < CHIRP_PAYLOAD_LENGTH) return null;31- const buf = new Uint8Array(CHIRP_PAYLOAD_LENGTH);32- for (let i = 0; i < CHIRP_PAYLOAD_LENGTH; i++) {33- const v = (samples as Float32Array)[i];34- if (v === 99) return null;35- buf[i] = Math.round(v * 255);36- }37- return buf;38- },39- };40-}41-42-function fakeAudio(injectedChirp: ChirpPayload | null) {43- let pending: Float32Array;44- if (injectedChirp) {45- const bytes = encodeChirpPayload(injectedChirp);46- pending = new Float32Array(128);47- for (let i = 0; i < bytes.length; i++) pending[i] = bytes[i] / 255;48- for (let i = bytes.length; i < pending.length; i++) pending[i] = 99;49- } else {50- pending = new Float32Array(0);51- }52- return async () => ({53- sampleRate: 48000,54- async play() { /* no-op */ },55- async pullSamples() { const out = pending; pending = new Float32Array(0); return out; },56- async close() { /* no-op */ },57- });58-}59-60-describe("runChirpPairing (popup orchestrator)", () => {61- beforeEach(() => __setGgwaveModuleForTest(fakeGgwave()));62- afterEach(() => __setGgwaveModuleForTest(null));63-64- it("emits listening progress before audio I/O starts", async () => {65- const trail: PairingProgress[] = [];66- // Use a short-timeout chirp exchange by injecting no-chirp audio.67- // The orchestrator will time out, but we only assert on early progress.68- try {69- await runChirpPairing({70- async fetchLocalIdentity() {71- return { deviceId: "ext", deviceName: "Firefox", deviceType: "extension", longTermPublicKeyHex: "0".repeat(64) };72- },73- async completePairing() { /* unreached */ },74- async confirmSAS() { return true; },75- async getLocalSubnet() { return "192.168.1"; },76- chirpTimeoutMs: 200,77- openAudio: fakeAudio(null),78- async openPairingTransport() { throw new Error("unreached"); },79- onProgress(p) { trail.push(p); },80- });81- } catch { /* expected timeout */ }82-83- expect(trail[0].phase).toBe("idle");84- expect(trail[1].phase).toBe("listening");85- });86-87- it("emits handshake then handshake_failed when no peer responds on the subnet", async () => {88- const peerEphemeral = generateKeyPair();89- const peerChirp: ChirpPayload = {90- version: 0x01,91- deviceType: DEVICE_TYPE_ANDROID,92- ephemeralPublicKey: peerEphemeral.publicKey,93- sessionNonce: new Uint8Array(16).fill(3),94- };95- const trail: PairingProgress[] = [];96-97- await expect(98- runChirpPairing({99- async fetchLocalIdentity() {100- return { deviceId: "ext", deviceName: "Firefox", deviceType: "extension", longTermPublicKeyHex: "0".repeat(64) };101- },102- async completePairing() { /* unreached */ },103- async confirmSAS() { return true; },104- async getLocalSubnet() { return "192.168.1"; },105- openAudio: fakeAudio(peerChirp),106- async openPairingTransport() { throw new Error("connection refused"); },107- onProgress(p) { trail.push(p); },108- }),109- ).rejects.toBeInstanceOf(ChirpPairingFailure);110-111- expect(trail.some((p) => p.phase === "handshake")).toBe(true);112- expect(trail.at(-1)?.phase).toBe("handshake_failed");113- }, 30_000);114-115- it("returns handshake_failed when getLocalSubnet returns null", async () => {116- const peerEphemeral = generateKeyPair();117- const peerChirp: ChirpPayload = {118- version: 0x01,119- deviceType: DEVICE_TYPE_ANDROID,120- ephemeralPublicKey: peerEphemeral.publicKey,121- sessionNonce: new Uint8Array(16).fill(2),122- };123- const trail: PairingProgress[] = [];124-125- await expect(126- runChirpPairing({127- async fetchLocalIdentity() {128- return { deviceId: "ext", deviceName: "Firefox", deviceType: "extension", longTermPublicKeyHex: "0".repeat(64) };129- },130- async completePairing() { /* unreached */ },131- async confirmSAS() { return true; },132- async getLocalSubnet() { return null; },133- openAudio: fakeAudio(peerChirp),134- async openPairingTransport() { throw new Error("unreached"); },135- onProgress(p) { trail.push(p); },136- }),137- ).rejects.toMatchObject({ phase: "handshake_failed" });138- expect(trail.at(-1)?.errorMessage).toBe("no_subnet");139- });140-141- it("rejects responder role with non-extension local identity", async () => {142- await expect(143- runChirpPairing({144- async fetchLocalIdentity() {145- return { deviceId: "android", deviceName: "Pixel", deviceType: "android", longTermPublicKeyHex: "0".repeat(64) };146- },147- async completePairing() { /* unreached */ },148- async confirmSAS() { return true; },149- async getLocalSubnet() { return "192.168.1"; },150- chirpTimeoutMs: 200,151- openAudio: fakeAudio(null),152- async openPairingTransport() { throw new Error("unreached"); },153- onProgress() { /* noop */ },154- }, "responder"),155- ).rejects.toThrow(/responder requires extension/);156- });157-158- it("rejects initiator role with extension local identity", async () => {159- await expect(160- runChirpPairing({161- async fetchLocalIdentity() {162- return { deviceId: "ext", deviceName: "Firefox", deviceType: "extension", longTermPublicKeyHex: "0".repeat(64) };163- },164- async completePairing() { /* unreached */ },165- async confirmSAS() { return true; },166- async getLocalSubnet() { return null; },167- chirpTimeoutMs: 200,168- openAudio: fakeAudio(null),169- async openPairingTransport() { throw new Error("unreached"); },170- async openPairingServer() { throw new Error("unreached"); },171- onProgress() { /* noop */ },172- }, "initiator"),173- ).rejects.toThrow(/initiator requires non-extension/);174- });175-176- it("rejects initiator role when openPairingServer dep is missing", async () => {177- await expect(178- runChirpPairing({179- async fetchLocalIdentity() {180- return { deviceId: "android", deviceName: "Pixel", deviceType: "android", longTermPublicKeyHex: "0".repeat(64) };181- },182- async completePairing() { /* unreached */ },183- async confirmSAS() { return true; },184- async getLocalSubnet() { return null; },185- chirpTimeoutMs: 200,186- openAudio: fakeAudio(null),187- async openPairingTransport() { throw new Error("unreached"); },188- onProgress() { /* noop */ },189- }, "initiator"),190- ).rejects.toThrow(/openPairingServer/);191- });192-});193-194-describe("gateOnSAS (PCH-01-024 — the SAS gate is MANDATORY before persist)", () => {195- const device = { deviceId: "peer" } as unknown as Parameters<typeof gateOnSAS>[1]["device"];196-197- function makeDeps(confirm: boolean) {198- const progress: PairingProgress[] = [];199- const deps = {200- confirmSAS: async () => confirm,201- onProgress: (p: PairingProgress) => progress.push(p),202- } as unknown as Parameters<typeof gateOnSAS>[0];203- return { deps, progress };204- }205-206- it("returns the device when the user confirms the SAS", async () => {207- const { deps } = makeDeps(true);208- await expect(gateOnSAS(deps, { device, sas: "123456" })).resolves.toBe(device);209- });210-211- it("aborts with handshake_failed when the user rejects the SAS", async () => {212- const { deps, progress } = makeDeps(false);213- await expect(gateOnSAS(deps, { device, sas: "123456" })).rejects.toBeInstanceOf(214- ChirpPairingFailure,215- );216- expect(progress.at(-1)?.phase).toBe("handshake_failed");217- });218-});src/popup/chirp-pair.ts+0 −547
@@ -1,547 +0,0 @@1-// Popup-side orchestrator for Phase-1 chirp pairing.2-//3-// Runs end-to-end inside the popup or the settings page (it depends on4-// `getUserMedia`, which is unavailable in the MV3 service worker). The5-// background's role is limited to handing us the local identity and6-// persisting the resulting PairedDevice record.7-//8-// Flow:9-// 1. fetch local identity via CHIRP_GET_LOCAL_IDENTITY10-// 2. open Web Audio + microphone11-// 3. encode our chirp and broadcast on a 4s loop while listening12-// 4. on chirp decoded, transition to handshake phase13-// 5. probe local subnet on the well-known pairing port for the peer's14-// WebSocket; complete Noise NK over it; exchange long-term identities15-// 6. send CHIRP_COMPLETE_PAIRING to persist the new pair16-//17-// The orchestrator notifies the UI through an `onProgress` callback so18-// `ChirpPairingFlow` can drive its three-act animation in lockstep with19-// the real protocol state.20-21-import {22- ChirpExchangeError,23- createPairingAttempt,24- openBrowserAudioIO,25- runChirpExchange,26-} from "../shared/chirp-pairing";27-import {28- handlePairIdentityFrame,29- runPairingInitiatorOpenChannel,30- runPairingInitiator,31- runPairingResponderOpenFrame,32- type LocalIdentity,33- type PairingHandshakeResult,34- type PostNoiseFrame,35-} from "../shared/chirp-handshake";36-import { computeVaultId } from "../shared/sync-identity";37-import { grantAdoption, isAdoptRequest, requestAdoption, type AdoptRequest } from "../shared/chirp-adopt";38-import {39- DEVICE_TYPE_EXTENSION,40- DEVICE_TYPE_ANDROID,41- DEVICE_TYPE_IOS,42- type DeviceType,43- type PairedDevice,44- type PairingProgress,45-} from "../shared/sync-types";46-47-/**48- * Well-known TCP port used during Phase-1 pairing. Android opens a49- * WebSocket server on this port only while the pairing screen is active;50- * the extension scans the /24 for it after the chirp exchange completes.51- * The persistent sync port (random high port) is announced via the52- * encrypted PAIR_IDENTITY frame and stored on the PairedDevice.53- */54-export const CHIRP_PAIRING_PORT = 49142;55-56-export interface PairingTransport {57- send(msg: Uint8Array): Promise<void>;58- receive(): Promise<Uint8Array>;59- close(): Promise<void>;60-}61-62-export interface ChirpPairingDeps {63- /** Background round-trip — returns the local public identity. */64- fetchLocalIdentity(): Promise<LocalIdentity>;65- /** Background round-trip — persists the paired device. */66- completePairing(paired: PairedDevice): Promise<void>;67- /**68- * Show the Short Authentication String and resolve true only when the user69- * confirms it matches the code displayed on the OTHER device. Pairing is70- * aborted (the peer is NOT persisted) on false. This is the out-of-band71- * mutual-authentication step that defeats a LAN/audio-range attacker who72- * completes the Noise NK handshake in place of the real device — their73- * handshake produces a different SAS than the genuine peer shows, so the74- * user declines (audit HIGH — pairing initiator authentication).75- */76- confirmSAS(sas: string): Promise<boolean>;77- /** Optional — cache the peer's IP for next session's fast path. */78- cacheLastKnownIp?(pubkeyHex: string, ip: string): Promise<void>;79- /**80- * Optional — activate a Pro license the peer sent in its identity frame.81- * Called ONLY after the SAS is confirmed. The implementation verifies the82- * signature and ignores it if this device is already Pro. This is how one83- * Pro device grants Pro to the other when pairing (either direction).84- */85- activatePeerLicense?(license: string): Promise<void>;86- /** Source-device adoption consent gate, shown only after SAS confirmation. */87- grantAdoptionConsent?(request: AdoptRequest, sas: string): Promise<boolean>;88- /** Source-device grant material, fetched only after SAS and consent. */89- fetchAdoptionMaterial?(): Promise<{ rootKeyHex: string; vaultId: string; license?: string }>;90- /**91- * RESPONDER ONLY — returns "192.168.1" for a /24; null if not detectable.92- * Used to scan the LAN for the initiator's pairing-port WebSocket server.93- */94- getLocalSubnet(): Promise<string | null>;95- /**96- * RESPONDER ONLY — connect to the initiator's WebSocket server at the97- * given endpoint. Used during the /24 scan and again for the real98- * handshake transport.99- */100- openPairingTransport(endpoint: { ip: string; port: number }): Promise<PairingTransport>;101- /**102- * INITIATOR ONLY — open a WebSocket server on CHIRP_PAIRING_PORT and103- * resolve with a transport when the responder connects. Backed by a104- * native plugin on Capacitor; not used in the extension build.105- *106- * The promise resolves once a peer connection has been accepted. If no107- * peer arrives before the chirp window expires, reject so the orchestrator108- * can surface a `handshake_failed` phase. Close the server in `.close()`.109- */110- openPairingServer?(): Promise<{111- transport: PairingTransport;112- peerIp: string;113- }>;114- /** Override the chirp exchange's `AudioIO` for tests. */115- openAudio?(): Promise<Awaited<ReturnType<typeof openBrowserAudioIO>>>;116- /** Override the audio-exchange timeout — defaults to 90s per the brief. */117- chirpTimeoutMs?: number;118- onProgress(p: PairingProgress): void;119-}120-121-export type PairingRole = "initiator" | "responder";122-123-export class ChirpPairingFailure extends Error {124- readonly phase: PairingProgress["phase"];125- constructor(phase: PairingProgress["phase"], message: string) {126- super(message);127- this.phase = phase;128- }129-}130-131-export async function runChirpPairing(132- deps: ChirpPairingDeps,133- role: PairingRole = "responder",134-): Promise<PairedDevice> {135- deps.onProgress({ phase: "idle" });136-137- const localIdentity = await deps.fetchLocalIdentity();138-139- // Each role only runs on the matching platform — the extension is always140- // responder (it scans the LAN), Android/iOS/desktop are always initiator141- // (they open the WS server). Mismatches indicate a wiring bug upstream.142- if (role === "responder" && localIdentity.deviceType !== "extension") {143- throw new ChirpPairingFailure("handshake_failed", "responder requires extension identity");144- }145- if (role === "initiator" && localIdentity.deviceType === "extension") {146- throw new ChirpPairingFailure("handshake_failed", "initiator requires non-extension identity");147- }148- if (role === "initiator" && !deps.openPairingServer) {149- throw new ChirpPairingFailure("handshake_failed", "initiator requires openPairingServer dep");150- }151-152- deps.onProgress({ phase: "listening" });153-154- const deviceTypeByte =155- localIdentity.deviceType === "android" ? DEVICE_TYPE_ANDROID :156- localIdentity.deviceType === "ios" ? DEVICE_TYPE_IOS :157- DEVICE_TYPE_EXTENSION;158- const attempt = createPairingAttempt(deviceTypeByte);159-160- let remoteChirpPublicKey: Uint8Array;161- try {162- const audio = await (deps.openAudio ?? openBrowserAudioIO)();163- const result = await runChirpExchange(audio, { attempt, timeoutMs: deps.chirpTimeoutMs });164- remoteChirpPublicKey = result.remoteChirp.ephemeralPublicKey;165- } catch (err) {166- if (err instanceof ChirpExchangeError) {167- const phase: PairingProgress["phase"] =168- err.kind === "timeout" ? "timeout" :169- err.kind === "version_mismatch" ? "version_mismatch" :170- err.kind === "mic_denied" ? "mic_denied" :171- "handshake_failed";172- deps.onProgress({ phase, errorMessage: err.message });173- throw new ChirpPairingFailure(phase, err.message);174- }175- throw err;176- }177-178- deps.onProgress({ phase: "handshake" });179-180- if (role === "responder") {181- return runResponderHandshake(deps, attempt, localIdentity);182- }183- return runInitiatorHandshake(deps, remoteChirpPublicKey, localIdentity);184-}185-186-export interface ChirpAdoptionReceiverDeps {187- deviceName: string;188- deviceType?: DeviceType;189- generateDeviceId?(): string;190- createVaultFromRootKey(rootKeyHex: string): Promise<{ vaultId: string }>;191- deriveLocalIdentity(): Promise<LocalIdentity>;192- completePairing(paired: PairedDevice): Promise<void>;193- applyLicense?(license: string): Promise<void>;194- confirmSAS(sas: string): Promise<boolean>;195- openPairingServer(): Promise<{ transport: PairingTransport; peerIp: string }>;196- openAudio?(): Promise<Awaited<ReturnType<typeof openBrowserAudioIO>>>;197- chirpTimeoutMs?: number;198- onProgress(p: PairingProgress): void;199-}200-201-export async function runChirpAdoptionReceiver(202- deps: ChirpAdoptionReceiverDeps,203-): Promise<PairedDevice> {204- deps.onProgress({ phase: "idle" });205- deps.onProgress({ phase: "listening" });206-207- const attempt = createPairingAttempt(deviceTypeByteFor(deps.deviceType ?? "extension"));208- let remoteChirpPublicKey: Uint8Array;209- try {210- const audio = await (deps.openAudio ?? openBrowserAudioIO)();211- const result = await runChirpExchange(audio, { attempt, timeoutMs: deps.chirpTimeoutMs });212- remoteChirpPublicKey = result.remoteChirp.ephemeralPublicKey;213- } catch (err) {214- if (err instanceof ChirpExchangeError) {215- const phase: PairingProgress["phase"] =216- err.kind === "timeout" ? "timeout" :217- err.kind === "version_mismatch" ? "version_mismatch" :218- err.kind === "mic_denied" ? "mic_denied" :219- "handshake_failed";220- deps.onProgress({ phase, errorMessage: err.message });221- throw new ChirpPairingFailure(phase, err.message);222- }223- throw err;224- }225-226- deps.onProgress({ phase: "handshake" });227- let server: { transport: PairingTransport; peerIp: string };228- try {229- server = await deps.openPairingServer();230- } catch (err) {231- deps.onProgress({ phase: "handshake_failed", errorMessage: (err as Error).message });232- throw new ChirpPairingFailure("handshake_failed", (err as Error).message);233- }234-235- try {236- const postNoise = await runPairingInitiatorOpenChannel(remoteChirpPublicKey, server.transport);237- const confirmed = await deps.confirmSAS(postNoise.sas);238- if (!confirmed) {239- deps.onProgress({ phase: "handshake_failed", errorMessage: "sas_rejected" });240- throw new ChirpPairingFailure("handshake_failed", "sas_rejected");241- }242- const source = await requestAdoption(postNoise.channel, {243- deviceName: deps.deviceName,244- generateDeviceId: deps.generateDeviceId ?? generateUuid,245- createVaultFromRootKey: deps.createVaultFromRootKey,246- deriveLocalIdentity: deps.deriveLocalIdentity,247- upsertPairedDevice: deps.completePairing,248- applyLicense: deps.applyLicense,249- onError(error) {250- throw new Error(error);251- },252- });253- if (!source) throw new Error("adoption_failed");254- deps.onProgress({ phase: "paired", peerDeviceName: source.deviceName });255- return source;256- } catch (err) {257- if (err instanceof ChirpPairingFailure) throw err;258- deps.onProgress({ phase: "handshake_failed", errorMessage: (err as Error).message });259- throw new ChirpPairingFailure("handshake_failed", (err as Error).message);260- } finally {261- await server.transport.close().catch(() => undefined);262- }263-}264-265-export interface SourcePostSASDeps {266- confirmSAS(sas: string): Promise<boolean>;267- completePairing(device: PairedDevice): Promise<void>;268- grantAdoptionConsent?(request: AdoptRequest, sas: string): Promise<boolean>;269- fetchAdoptionMaterial?(): Promise<{ rootKeyHex: string; vaultId: string; license?: string }>;270- activatePeerLicense?(license: string): Promise<void>;271- onProgress(p: PairingProgress): void;272-}273-274-export async function routeSourcePostSASFrame(275- postNoise: PostNoiseFrame,276- localIdentity: LocalIdentity,277- deps: SourcePostSASDeps,278-): Promise<PairedDevice | null> {279- if (isAdoptRequest(postNoise.frame)) {280- const sasConfirmed = await deps.confirmSAS(postNoise.sas);281- if (!sasConfirmed) {282- await closeIfPossible(postNoise.channel);283- throw new ChirpPairingFailure("handshake_failed", "sas_rejected");284- }285- if (!deps.grantAdoptionConsent || !deps.fetchAdoptionMaterial) {286- await closeIfPossible(postNoise.channel);287- throw new ChirpPairingFailure("handshake_failed", "adoption_not_supported");288- }289-290- const approved = await deps.grantAdoptionConsent(postNoise.frame, postNoise.sas);291- if (!approved) {292- await closeIfPossible(postNoise.channel);293- throw new ChirpPairingFailure("handshake_failed", "adoption_rejected");294- }295-296- const material = await deps.fetchAdoptionMaterial();297- if (!/^[0-9a-f]{64}$/.test(material.rootKeyHex)) throw new Error("ADOPT_INVALID_ROOT_KEY");298- const rootKeyBytes = hexToBytes(material.rootKeyHex);299- try {300- const expectedVaultId = await computeVaultId(rootKeyBytes);301- if (expectedVaultId !== material.vaultId) throw new Error("ADOPT_VAULT_ID_MISMATCH");302- const adopted = await grantAdoption(postNoise.channel, postNoise.frame, {303- sourceIdentity: localIdentity,304- rootKeyHex: async () => material.rootKeyHex,305- vaultId: async () => material.vaultId,306- license: async () => material.license,307- onConsent: async () => true,308- upsertPairedDevice: deps.completePairing,309- });310- if (adopted) deps.onProgress({ phase: "paired", peerDeviceName: adopted.deviceName });311- return adopted;312- } finally {313- rootKeyBytes.fill(0);314- }315- }316-317- if ((postNoise.frame as { type?: unknown } | null)?.type === "PAIR_IDENTITY") {318- const result = await handlePairIdentityFrame(319- postNoise.frame,320- postNoise.channel,321- localIdentity,322- postNoise.sas,323- );324- const paired = await gateOnSAS(deps, result);325- await activatePeerLicenseIfAny(deps, result);326- await deps.completePairing(paired);327- return paired;328- }329-330- await closeIfPossible(postNoise.channel);331- throw new ChirpPairingFailure("handshake_failed", "unexpected_pairing_frame");332-}333-334-async function runResponderHandshake(335- deps: ChirpPairingDeps,336- attempt: ReturnType<typeof createPairingAttempt>,337- localIdentity: LocalIdentity,338-): Promise<PairedDevice> {339- // The peer is somewhere on the LAN; we don't know its IP yet because340- // the chirp payload deliberately omits it. Scan the /24 for the341- // pairing-mode WebSocket port.342- const subnet = await deps.getLocalSubnet();343- if (!subnet) {344- deps.onProgress({ phase: "handshake_failed", errorMessage: "no_subnet" });345- throw new ChirpPairingFailure("handshake_failed", "could not determine local subnet");346- }347-348- const peerEndpoint = await firstResponsivePeer(deps, subnet, CHIRP_PAIRING_PORT);349- if (!peerEndpoint) {350- deps.onProgress({ phase: "handshake_failed", errorMessage: "peer_not_found" });351- throw new ChirpPairingFailure("handshake_failed", "could not reach paired peer on subnet");352- }353-354- const transport = await deps.openPairingTransport(peerEndpoint);355- let postNoise: PostNoiseFrame;356- try {357- postNoise = await runPairingResponderOpenFrame(358- attempt.ephemeralPrivateKey,359- attempt.ephemeralPublicKey,360- transport,361- );362- } catch (err) {363- await transport.close();364- deps.onProgress({ phase: "handshake_failed", errorMessage: (err as Error).message });365- throw new ChirpPairingFailure("handshake_failed", (err as Error).message);366- }367- const paired = await routeSourcePostSASFrame(postNoise, localIdentity, deps);368- await transport.close();369- if (!paired) {370- throw new ChirpPairingFailure("handshake_failed", "adoption_rejected");371- }372- await deps.cacheLastKnownIp?.(paired.longTermPublicKeyHex ?? "", peerEndpoint.ip);373- deps.onProgress({ phase: "paired", peerDeviceName: paired.deviceName });374- return paired;375-}376-377-/**378- * Surface the SAS for user confirmation and abort the pairing if it is not379- * confirmed to match the peer device. Shared by both roles.380- */381-// Exported for the PCH-01-024 regression test that pins the SAS gate as382-// MANDATORY (the chirp-handshake.ts security comment depends on this staying true).383-export async function gateOnSAS(384- deps: Pick<ChirpPairingDeps, "confirmSAS" | "onProgress">,385- result: PairingHandshakeResult,386-): Promise<PairedDevice> {387- let confirmed: boolean;388- try {389- confirmed = await deps.confirmSAS(result.sas);390- } catch (err) {391- deps.onProgress({ phase: "handshake_failed", errorMessage: (err as Error).message });392- throw new ChirpPairingFailure("handshake_failed", (err as Error).message);393- }394- if (!confirmed) {395- deps.onProgress({ phase: "handshake_failed", errorMessage: "sas_rejected" });396- throw new ChirpPairingFailure(397- "handshake_failed",398- "pairing rejected — the verification codes did not match",399- );400- }401- return result.device;402-}403-404-/**405- * If the (now SAS-confirmed) peer advertised a Pro license, hand it to the406- * background to verify + activate. Best-effort: a bad/duplicate license never407- * fails the pairing — the device link still succeeds.408- */409-async function activatePeerLicenseIfAny(410- deps: Pick<ChirpPairingDeps, "activatePeerLicense">,411- result: PairingHandshakeResult,412-): Promise<void> {413- if (!result.peerLicense || !deps.activatePeerLicense) return;414- try {415- await deps.activatePeerLicense(result.peerLicense);416- } catch {417- // Invalid signature, already-Pro, or storage error — pairing stands.418- }419-}420-421-async function closeIfPossible(channel: unknown): Promise<void> {422- await (channel as { close?: () => Promise<void> }).close?.();423-}424-425-function hexToBytes(hex: string): Uint8Array {426- const out = new Uint8Array(hex.length / 2);427- for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);428- return out;429-}430-431-function deviceTypeByteFor(type: DeviceType) {432- return type === "android" ? DEVICE_TYPE_ANDROID :433- type === "ios" ? DEVICE_TYPE_IOS :434- DEVICE_TYPE_EXTENSION;435-}436-437-function generateUuid(): string {438- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {439- return crypto.randomUUID();440- }441- const bytes = new Uint8Array(16);442- crypto.getRandomValues(bytes as Uint8Array<ArrayBuffer>);443- bytes[6] = (bytes[6] & 0x0f) | 0x40;444- bytes[8] = (bytes[8] & 0x3f) | 0x80;445- const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");446- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;447-}448-449-async function runInitiatorHandshake(450- deps: ChirpPairingDeps,451- remoteChirpPublicKey: Uint8Array,452- localIdentity: LocalIdentity,453-): Promise<PairedDevice> {454- // Initiator opened a WebSocket server on CHIRP_PAIRING_PORT before the455- // chirp exchange started; here we wait for the responder (extension)456- // to finish its /24 scan and connect to us.457- let server: { transport: PairingTransport; peerIp: string };458- try {459- server = await deps.openPairingServer!();460- } catch (err) {461- deps.onProgress({ phase: "handshake_failed", errorMessage: (err as Error).message });462- throw new ChirpPairingFailure("handshake_failed", (err as Error).message);463- }464-465- let result: PairingHandshakeResult;466- try {467- result = await runPairingInitiator(468- remoteChirpPublicKey,469- localIdentity,470- server.transport,471- );472- } catch (err) {473- await server.transport.close();474- deps.onProgress({ phase: "handshake_failed", errorMessage: (err as Error).message });475- throw new ChirpPairingFailure("handshake_failed", (err as Error).message);476- }477- await server.transport.close();478-479- // Same out-of-band SAS confirmation as the responder side — the initiator480- // (mobile vault host) must not sync to an unverified peer either.481- const paired = await gateOnSAS(deps, result);482- await activatePeerLicenseIfAny(deps, result);483-484- await deps.completePairing(paired);485- await deps.cacheLastKnownIp?.(paired.longTermPublicKeyHex ?? "", server.peerIp);486- deps.onProgress({ phase: "paired", peerDeviceName: paired.deviceName });487- return paired;488-}489-490-async function firstResponsivePeer(491- deps: ChirpPairingDeps,492- subnet: string,493- port: number,494-): Promise<{ ip: string; port: number } | null> {495- // We don't have the peer's long-term key yet (that's what we're about496- // to exchange), so we can't run a Noise XX probe like the steady-state497- // sync path does. The pairing-port server accepts any incoming Noise NK498- // request during its short pairing window; the first WebSocket that499- // accepts our connection is the peer.500- //501- // Per-IP timeout strategy: browser WebSocket constructors against an502- // unbound TCP port can hang for several seconds before erroring, which503- // would stretch a /24 scan to minutes. Each probe is raced against a504- // 250ms timer via Promise.race — if the timer wins, we treat the IP as505- // a miss and close the underlying socket if it lands after the loss so506- // we don't leak connections. With 16-wide batching this bounds the full507- // scan to ~254 * 250ms / 16 ≈ 4 seconds in the worst case.508- const PROBE_TIMEOUT_MS = 250;509- const batch = 16;510- for (let start = 1; start <= 254; start += batch) {511- const end = Math.min(start + batch - 1, 254);512- const ips: string[] = [];513- for (let i = start; i <= end; i++) ips.push(`${subnet}.${i}`);514-515- const results = await Promise.all(516- ips.map(async (ip) => {517- let timer: ReturnType<typeof setTimeout> | undefined;518- const timeout = new Promise<null>((resolve) => {519- timer = setTimeout(() => resolve(null), PROBE_TIMEOUT_MS);520- });521- const probe = (async () => {522- try {523- const t = await deps.openPairingTransport({ ip, port });524- return { transport: t, ip, port };525- } catch {526- return null;527- }528- })();529- const winner = await Promise.race([probe, timeout]);530- if (winner && "transport" in winner) {531- if (timer !== undefined) clearTimeout(timer);532- await winner.transport.close();533- return { ip: winner.ip, port: winner.port };534- }535- // Timer won — make sure a late-arriving open socket is closed so536- // we don't leak a connection past the scan.537- probe.then((late) => {538- if (late) void late.transport.close();539- }).catch(() => { /* already a miss */ });540- return null;541- }),542- );543- const hit = results.find((r): r is { ip: string; port: number } => r !== null);544- if (hit) return hit;545- }546- return null;547-}src/popup/components/ChirpPairingFlow.test.tsx+0 −102
@@ -1,102 +0,0 @@1-// @vitest-environment jsdom2-3-import { afterEach, describe, expect, it, vi } from "vitest";4-import { ChirpPairingFlow } from "./ChirpPairingFlow";5-import type { PairingProgress } from "../../shared/sync-types";6-import { renderView } from "../views/view-test-utils";7-8-function progress(phase: PairingProgress["phase"], extra: Partial<PairingProgress> = {}): PairingProgress {9- return { phase, ...extra };10-}11-12-describe("ChirpPairingFlow", () => {13- const cleanups: Array<() => void> = [];14- afterEach(() => {15- while (cleanups.length) cleanups.pop()!();16- });17-18- function mount(progress: PairingProgress, handlers: { onCancel?: () => void; onRetry?: () => void; onDismiss?: () => void } = {}) {19- const r = renderView(20- <ChirpPairingFlow21- progress={progress}22- onCancel={handlers.onCancel ?? (() => {})}23- onRetry={handlers.onRetry ?? (() => {})}24- onDismiss={handlers.onDismiss ?? (() => {})}25- />,26- );27- cleanups.push(r.dispose);28- return r;29- }30-31- it("renders the listening copy in Act 1", () => {32- const { container } = mount(progress("listening"));33- expect(container.textContent).toContain("Hold your phone near your laptop");34- expect(container.textContent).toContain("Peach will pair automatically");35- });36-37- it("renders the handshake copy in Act 2", () => {38- const { container } = mount(progress("handshake"));39- expect(container.textContent).toContain("Pairing…");40- });41-42- it("renders the paired copy with the peer device name", () => {43- const { container } = mount(progress("paired", { peerDeviceName: "June's Pixel 9" }));44- expect(container.textContent).toContain("Paired!");45- expect(container.textContent).toContain("Connected to June's Pixel 9");46- });47-48- it("Cancel button calls onCancel in the listening state", () => {49- const onCancel = vi.fn();50- const { container } = mount(progress("listening"), { onCancel });51- const btn = Array.from(container.querySelectorAll("button")).find(52- (b) => b.textContent === "Cancel",53- );54- expect(btn).toBeTruthy();55- btn!.click();56- expect(onCancel).toHaveBeenCalled();57- });58-59- it("Try again button calls onRetry on timeout", () => {60- const onRetry = vi.fn();61- const { container } = mount(progress("timeout"), { onRetry });62- expect(container.textContent).toContain("Couldn’t find your phone");63- const btn = Array.from(container.querySelectorAll("button")).find(64- (b) => b.textContent === "Try again",65- );66- expect(btn).toBeTruthy();67- btn!.click();68- expect(onRetry).toHaveBeenCalled();69- });70-71- it("shows the microphone-permission help text on mic_denied", () => {72- const { container } = mount(progress("mic_denied"));73- expect(container.textContent).toContain("Microphone access needed");74- expect(container.textContent).toContain("No audio is recorded or stored");75- });76-77- it("shows the version-mismatch copy", () => {78- const { container } = mount(progress("version_mismatch"));79- expect(container.textContent).toContain("Update Peach on both devices");80- });81-82- it("auto-dismisses after 2.5s in the paired state", () => {83- vi.useFakeTimers();84- try {85- const onDismiss = vi.fn();86- mount(progress("paired", { peerDeviceName: "Pixel" }), { onDismiss });87- // The createEffect that arms the dismissal runs synchronously on88- // mount because the phase is already "paired"; advancing the timers89- // by 2500ms fires the setTimeout callback.90- vi.advanceTimersByTime(2500);91- expect(onDismiss).toHaveBeenCalled();92- } finally {93- vi.useRealTimers();94- }95- });96-97- it("data-phase attribute mirrors the progress phase", () => {98- const { container } = mount(progress("chirping"));99- const root = container.querySelector(".peach-chirp-pair") as HTMLElement;100- expect(root.dataset.phase).toBe("chirping");101- });102-});src/popup/components/ChirpPairingFlow.tsx+6 −388
@@ -1,393 +1,11 @@1-// ChirpPairingFlow — the audio pairing screen.1+// Pairing birds — the two illustrated characters used on the pairing screens.22 //3-// Renders the three-act bird animation specified in the brief §6.2:4-// Act 1 (waiting) — desktop finch chirps, soft rings pulse from its beak5-// Act 2 (handshake) — mobile swallow hops in, sound-wave arcs fly between6-// the two birds, both heads tilt toward each other7-// Act 3 (paired) — waves resolve into a glowing connection dot; both8-// birds wing-flutter and bob together9-//10-// Phase mapping (PairingPhase from sync-types):11-// idle | listening | chirping → Act 112-// handshake → Act 213-// paired → Act 314-// timeout → desktop bird droops, retry CTA15-// mic_denied → static stage, browser-settings hint16-// version_mismatch → static stage, update hint, retry CTA17-// handshake_failed → retry CTA18-//19-// The orchestrator (popup/chirp-pair.ts) pushes phase updates into20-// `progress` and the component renders them. This component owns no21-// microphone or socket state of its own.3+// These are all that remains of the audio-chirp pairing animation this file is4+// named after: the flow itself was removed with Chirp, but the birds outlived5+// it and still animate above the code-entry, connecting, SAS and paired6+// screens (ChirpSync.tsx's `desktopBirdStage`).227 23-import { Component, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js";24-import type { PairingPhase, PairingProgress } from "../../shared/sync-types";25-26-export interface ChirpPairingFlowProps {27- progress: PairingProgress;28- /** Called when the user taps Retry on a terminal-error state. */29- onRetry: () => void;30- /** Called when the user taps Cancel/back on the idle/listening state. */31- onCancel: () => void;32- /** Called once the paired-state dismissal animation completes. */33- onDismiss: () => void;34-}35-36-const PAIRED_DISMISS_MS = 2_500;37-// During handshake, swap which bird the flight-wave emanates from so the38-// chirp feels reciprocated. Desktop wave first, then mobile.39-const HANDSHAKE_DESKTOP_WAVE_MS = 900;40-const HANDSHAKE_MOBILE_WAVE_MS = 1_900;41-42-export const ChirpPairingFlow: Component<ChirpPairingFlowProps> = (props) => {43- const [dismissArmed, setDismissArmed] = createSignal(false);44- const [waveSide, setWaveSide] = createSignal<"desktop" | "mobile" | null>(null);45-46- createEffect(() => {47- if (props.progress.phase === "paired" && !dismissArmed()) {48- setDismissArmed(true);49- const t = setTimeout(() => props.onDismiss(), PAIRED_DISMISS_MS);50- onCleanup(() => clearTimeout(t));51- }52- });53-54- // Swap which side the flight-wave plays from during Act 2.55- createEffect(() => {56- if (props.progress.phase !== "handshake") {57- setWaveSide(null);58- return;59- }60- setWaveSide("desktop");61- const t1 = setTimeout(() => setWaveSide("mobile"), HANDSHAKE_DESKTOP_WAVE_MS);62- const t2 = setTimeout(() => setWaveSide(null), HANDSHAKE_MOBILE_WAVE_MS);63- onCleanup(() => { clearTimeout(t1); clearTimeout(t2); });64- });65-66- const phase = createMemo(() => props.progress.phase);67- const desktopPose = createMemo(() => poseFor("desktop", phase()));68- const mobilePose = createMemo(() => poseFor("mobile", phase()));69-70- const showDesktopChirp = createMemo(() => {71- const p = phase();72- return p === "listening" || p === "chirping" || (p === "handshake" && waveSide() !== "mobile");73- });74- const showMobileChirp = createMemo(() => phase() === "handshake" && waveSide() === "mobile");75-76- return (77- <div78- class="peach-chirp-pair"79- data-phase={phase()}80- role="dialog"81- aria-label="Pair a device"82- onClick={() => phase() === "paired" && props.onDismiss()}83- >84- <PairingHeader onBack={() => props.onCancel()} phase={phase()} />85-86- <div class="peach-chirp-chip-row">87- <StatusChip phase={phase()} />88- </div>89-90- <div class={`peach-chirp-stage act-${phase()}`} aria-hidden="true">91- <div class="peach-chirp-bird-slot peach-chirp-bird-slot-desktop">92- <DesktopBird pose={desktopPose()} />93- <Show when={showDesktopChirp()}>94- <ChirpArcs direction="right" />95- </Show>96- </div>97- <div class="peach-chirp-bird-slot peach-chirp-bird-slot-mobile">98- <MobileBird pose={mobilePose()} />99- <Show when={showMobileChirp()}>100- <ChirpArcs direction="left" />101- </Show>102- </div>103- <div class="peach-chirp-flight-track">104- <FlightWave from="desktop" visible={waveSide() === "desktop"} />105- <FlightWave from="mobile" visible={waveSide() === "mobile"} />106- <ConnectionDot visible={phase() === "paired"} />107- </div>108- <div class="peach-chirp-motes">109- <For8>110- {(i) => <span class="peach-chirp-mote" style={{ "--mote-i": i }} />}111- </For8>112- </div>113- </div>114-115- <PairingCopy progress={props.progress} />116- <PairingAction117- progress={props.progress}118- onCancel={() => props.onCancel()}119- onRetry={() => props.onRetry()}120- onDone={() => props.onDismiss()}121- />122-123- <Show when={phase() === "mic_denied"}>124- <p class="peach-chirp-mic-help">125- Update your browser's site settings to allow Peach to access the microphone,126- then try again.127- </p>128- </Show>129-130- <PairingFooter phase={phase()} />131-132- {/* Polite live region so the act transitions are announced to133- assistive tech even though the stage itself is decorative. */}134- <div class="peach-sr-only" aria-live="polite">{liveText(props.progress)}</div>135- </div>136- );137-};138-139-function poseFor(side: "desktop" | "mobile", phase: PairingPhase): string {140- if (side === "desktop") {141- switch (phase) {142- case "listening":143- case "chirping": return "chirping";144- case "handshake": return "attentive";145- case "paired": return "happy";146- case "timeout":147- case "handshake_failed":148- case "version_mismatch": return "sad";149- default: return "idle";150- }151- }152- switch (phase) {153- case "handshake": return "attentive";154- case "paired": return "happy";155- default: return "hidden";156- }157-}158-159-function liveText(p: PairingProgress): string {160- switch (p.phase) {161- case "listening":162- case "chirping": return "Listening for the chirp";163- case "handshake": return "Pairing — exchanging keys";164- case "paired": return p.peerDeviceName ? `Paired — connected to ${p.peerDeviceName}` : "Paired";165- case "timeout": return "Couldn't find your phone";166- case "mic_denied": return "Microphone access needed";167- case "version_mismatch": return "Update Peach on both devices";168- case "handshake_failed": return "Pairing failed — try again";169- default: return "";170- }171-}172-173-// ---------------------------------------------------------------------------174-// Header, chip row, footer175-// ---------------------------------------------------------------------------176-177-const PairingHeader: Component<{ onBack: () => void; phase: PairingPhase }> = (props) => (178- <header class="peach-chirp-head">179- <button class="peach-chirp-back" aria-label="Back" onClick={() => props.onBack()}>180- <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">181- <path d="M10 3 L5 8 L10 13" />182- </svg>183- </button>184- <div class="peach-chirp-title">Sync</div>185- <div class="peach-chirp-head-spacer" />186- </header>187-);188-189-const StatusChip: Component<{ phase: PairingPhase }> = (props) => {190- const label = () => {191- switch (props.phase) {192- case "listening":193- case "chirping":194- case "idle": return "listening";195- case "handshake": return "handshake";196- case "paired": return "paired";197- case "mic_denied": return "mic blocked";198- case "version_mismatch": return "version mismatch";199- case "timeout": return "no response";200- case "handshake_failed": return "failed";201- }202- };203- return (204- <div class="peach-chirp-chip" data-phase={props.phase}>205- <span class="peach-chirp-chip-dot" />206- <span class="peach-chirp-chip-text">{label()}</span>207- </div>208- );209-};210-211-const PairingFooter: Component<{ phase: PairingPhase }> = (props) => (212- <footer class="peach-chirp-foot">213- <div class="peach-chirp-foot-divider" />214- <div class="peach-chirp-foot-text">215- {props.phase === "paired"216- ? "Long-term sync keys exchanged over Noise NK. Stored locally on both devices."217- : "Chirp Sync stays on your local network. No relay servers, no cloud accounts — just your devices in the same room."}218- </div>219- </footer>220-);221-222-// ---------------------------------------------------------------------------223-// Copy + action area (animates on phase change via key remount)224-// ---------------------------------------------------------------------------225-226-const PairingCopy: Component<{ progress: PairingProgress }> = (props) => {227- // Remount on phase change so the copy-in keyframe replays.228- const phaseKey = createMemo(() => props.progress.phase);229- return (230- <div class="peach-chirp-copy-host">231- <Show when={phaseKey()} keyed>232- {(_p) => (233- <div class={`peach-chirp-copy peach-chirp-copy-${props.progress.phase}`}>234- <div class="peach-chirp-headline">{headline(props.progress)}</div>235- <div class="peach-chirp-sub">{subtext(props.progress)}</div>236- </div>237- )}238- </Show>239- </div>240- );241-};242-243-function headline(p: PairingProgress): string {244- switch (p.phase) {245- case "idle":246- case "listening":247- case "chirping": return "Hold your phone near your laptop";248- case "handshake": return "Pairing…";249- case "paired": return "Paired!";250- case "timeout": return "Couldn’t find your phone";251- case "mic_denied": return "Microphone access needed";252- case "version_mismatch": return "Update Peach on both devices";253- case "handshake_failed": return "Something went wrong";254- }255-}256-257-function subtext(p: PairingProgress): string {258- switch (p.phase) {259- case "idle":260- case "listening":261- case "chirping": return "Peach will pair automatically when your phone is close.";262- case "handshake": return "Listening for the chirp.";263- case "paired": return p.peerDeviceName ? `Connected to ${p.peerDeviceName}` : "Connected";264- case "timeout": return "Make sure Peach is open on your phone and try again.";265- case "mic_denied": return "Peach uses your microphone to pair. No audio is recorded or stored.";266- case "version_mismatch": return "The other device is running a different protocol version.";267- case "handshake_failed": return p.errorMessage ?? "Please try again.";268- }269-}270-271-const PairingAction: Component<{272- progress: PairingProgress;273- onCancel: () => void;274- onRetry: () => void;275- onDone: () => void;276-}> = (props) => {277- const phase = () => props.progress.phase;278- return (279- <div class="peach-chirp-action">280- <Show when={phase() === "listening" || phase() === "chirping" || phase() === "idle"}>281- <div class="peach-chirp-action-waiting">282- <button class="peach-chirp-cancel peach-btn peach-btn-ghost" onClick={() => props.onCancel()}>283- Cancel284- </button>285- <div class="peach-chirp-listening">286- <span class="peach-chirp-listen-dot" />287- <span class="peach-chirp-listen-dot" />288- <span class="peach-chirp-listen-dot" />289- <span class="peach-chirp-listen-label">listening</span>290- </div>291- </div>292- </Show>293-294- <Show when={phase() === "handshake"}>295- <div class="peach-chirp-action-handshake">296- <div class="peach-chirp-meter">297- <span class="peach-chirp-meter-bar" />298- </div>299- <span class="peach-chirp-meter-label">handshake · noise NK</span>300- </div>301- </Show>302-303- <Show when={phase() === "paired"}>304- <div class="peach-chirp-action-paired">305- <div class="peach-chirp-device-confirm">306- <span class="peach-chirp-device-confirm-glyph" aria-hidden="true">307- <svg viewBox="0 0 16 16" width="14" height="14">308- <rect x="3" y="2" width="10" height="13" rx="2" fill="none" stroke="currentColor" stroke-width="1.2" />309- <circle cx="8" cy="12" r="0.9" fill="currentColor" />310- </svg>311- </span>312- <span class="peach-chirp-device-confirm-name">313- {props.progress.peerDeviceName ?? "Paired device"}314- </span>315- <span class="peach-chirp-device-confirm-trust">direct</span>316- </div>317- <button class="peach-chirp-done peach-btn peach-btn-primary" onClick={() => props.onDone()}>318- Done319- </button>320- </div>321- </Show>322-323- <Show when={isRetryable(phase())}>324- <button class="peach-chirp-retry peach-btn peach-btn-primary" onClick={() => props.onRetry()}>325- Try again326- </button>327- </Show>328- </div>329- );330-};331-332-function isRetryable(phase: PairingPhase): boolean {333- return phase === "timeout" || phase === "handshake_failed" || phase === "version_mismatch" || phase === "mic_denied";334-}335-336-// ---------------------------------------------------------------------------337-// Stage children — chirp arcs, flight wave, connection dot338-// ---------------------------------------------------------------------------339-340-const ChirpArcs: Component<{ direction: "left" | "right" }> = (props) => (341- <div class={`peach-chirp-arcs peach-chirp-arcs-${props.direction}`}>342- <span class="peach-chirp-arc" style={{ "animation-delay": "0ms" }} />343- <span class="peach-chirp-arc" style={{ "animation-delay": "560ms" }} />344- <span class="peach-chirp-arc" style={{ "animation-delay": "1120ms" }} />345- </div>346-);347-348-const FlightWave: Component<{ from: "desktop" | "mobile"; visible: boolean }> = (props) => (349- <svg350- class="peach-chirp-flight-wave"351- classList={{ "is-visible": props.visible }}352- data-from={props.from}353- viewBox="0 0 220 80"354- preserveAspectRatio="none"355- >356- <path class="peach-chirp-flight-path" d="M 10 50 Q 60 5, 110 40 T 210 30" />357- <path class="peach-chirp-flight-path-2" d="M 10 55 Q 60 14, 110 45 T 210 35" />358- </svg>359-);360-361-const ConnectionDot: Component<{ visible: boolean }> = (props) => (362- <div class="peach-chirp-dot" classList={{ "is-visible": props.visible }}>363- <span class="peach-chirp-dot-glow" />364- <span class="peach-chirp-dot-core" />365- <svg class="peach-chirp-dot-ring" viewBox="0 0 60 60">366- <circle cx="30" cy="30" r="22" fill="none" stroke-width="1" stroke-dasharray="2 4" />367- </svg>368- </div>369-);370-371-// 8-mote helper. createMemo over a fixed array is overkill — index is enough.372-const For8: Component<{ children: (i: number) => unknown }> = (props) => (373- <>374- {props.children(0)}375- {props.children(1)}376- {props.children(2)}377- {props.children(3)}378- {props.children(4)}379- {props.children(5)}380- {props.children(6)}381- {props.children(7)}382- </>383-);384-385-// ---------------------------------------------------------------------------386-// Birds — Finch (desktop) and Swallow (mobile)387-// Ported from the Chirp Pairing prototype. Bird coral/apricot hexes are388-// illustration colours, not brand accent — they live alongside the brand389-// peach exception in §3.1.390-// ---------------------------------------------------------------------------8+import { Component } from "solid-js";3919 39210 const FINCH = {39311 hi: "#f4a6b6", mid: "#dc8294", lo: "#a85870", shade: "#7a3e54",src/popup/views/ChirpSync.tsx+21 −288
@@ -1,19 +1,17 @@11 // ChirpSync — the pairing surface, opened from the Settings → Sync card.22 //33 // Settings owns the sync "hub" (paired-device list, revoke, sync-now); this4-// view is purely the pairing flow, entered straight into a method via5-// `initialPair` and returning to Settings (`onBack`) when done or cancelled:4+// view is purely the pairing flow, returning to Settings (`onBack`) when done5+// or cancelled: the user types the pairing code their phone displays, the6+// browser dials out to the address inside it, and the NK handshake + SAS run7+// from there (enter code → connecting → SAS → paired).68 //7-// - Locked behind Pro (free user) — soft upsell card, "Learn more" routes8-// to the License section.9-// - Code — the primary path: the user types the pairing code their phone10-// displays, the browser dials out to the address inside it, and the NK11-// handshake + SAS run from there (enter code → SAS → paired).12-// - Chirp — the superseded audio-pairing flow. No entry point reaches it any13-// more; the code below is still here pending the removal decision.9+// There is one path because there can only be one: the browser cannot listen,10+// so it is always the side that dials. The audio-chirp flow this file is named11+// after has been removed.1412 //15-// The NK pairing runs in the background (desktop-pairing.ts) and reaches this16-// view via the PAIR_*/DESKTOP_PAIR_* messages.13+// The pairing itself runs in the background (desktop-pairing.ts) and reaches14+// this view via the PAIR_WITH_DESKTOP / DESKTOP_PAIR_* messages.1715 1816 import {1917 Component,@@ -24,44 +22,25 @@2422 onMount,2523 } from "solid-js";2624 import { sendMessage } from "../../shared/bridge";27-import type {28- PairedDevice,29- PairingProgress,30-} from "../../shared/sync-types";31-import type { AdoptRequest } from "../../shared/chirp-adopt";32-import { ChirpPairingFlow, DesktopBird, MobileBird } from "../components/ChirpPairingFlow";33-import { AdoptionConsentModal } from "../components/AdoptionConsentModal";34-import {35- ChirpPairingFailure,36- runChirpPairing,37- type ChirpPairingDeps,38- type PairingRole,39-} from "../chirp-pair";25+import type { PairedDevice } from "../../shared/sync-types";26+import { DesktopBird, MobileBird } from "../components/ChirpPairingFlow";4027 import { formatPairingCode, parsePairingCode } from "../../shared/pairing-code";41-import { getLocalSubnet } from "../../shared/local-network";42-import { isNativePlatform } from "../../shared/native";43-import { openCapacitorPairingServer } from "../chirp-pair-capacitor";4428 4529 export interface ChirpSyncViewProps {4630 isPro: boolean;4731 onBack(): void;4832 onLearnPro(): void;4933 /**50- * Open straight into a pairing path, skipping the rest screen — set when the51- * user taps "Chirp" or "Code" on the Settings sync card. Read once on mount.34+ * Set when the user taps the pairing option on the Settings sync card. With35+ * no method there is nothing to do here, so the view returns to Settings.36+ * Read once on mount.5237 */53- initialPair?: "chirp" | "desktop" | null;38+ initialPair?: "desktop" | null;5439 /** Called after a peer's Pro license is activated, so the app re-reads Pro status. */5540 onProChanged?: () => void;5641 }5742 58-// Which pairing path the user picked, passed in via `initialPair`. The birds59-// animate the same way on both paths.60-type PairKind = "chirp" | "desktop" | null;61-62-// Desktop helper (peach-sync) code-pairing phases. The SAS and success63-// screens are shared with the chirp path; "enter"/"connecting" are unique to64-// the typed-code flow. Mirrors the background DesktopPairingError union.43+// Code-pairing phases. Mirrors the background DesktopPairingError union.6544 type DesktopPhase = "enter" | "connecting" | "sas" | "paired" | "error";6645 type DesktopPairingError =6746 | "invalid_code"@@ -106,29 +85,10 @@10685 }10786 10887 export const ChirpSyncView: Component<ChirpSyncViewProps> = (props) => {109- const [pairing, setPairing] = createSignal<PairingProgress>({ phase: "idle" });110- // Out-of-band pairing verification: the 6-digit code both devices derive111- // from the shared handshake; the user confirms it matches before the pair112- // is persisted (audit HIGH — pairing initiator authentication).113- const [sasCode, setSasCode] = createSignal<string | null>(null);114- const [sasResolve, setSasResolve] = createSignal<((ok: boolean) => void) | null>(null);115- const [adoptionConsent, setAdoptionConsent] = createSignal<{116- request: AdoptRequest;117- sas: string;118- resolve: (ok: boolean) => void;119- } | null>(null);120-121- function resolveSAS(ok: boolean) {122- const r = sasResolve();123- setSasCode(null);124- setSasResolve(null);125- r?.(ok);126- }127-128- // --- Desktop helper (peach-sync) code pairing -------------------------12988 // The whole NK pairing runs in the background; this view drives the UI and130- // the SAS confirmation round-trip via the four PAIR_*/DESKTOP_PAIR_* messages.131- const [pairKind, setPairKind] = createSignal<PairKind>(props.initialPair ?? null);89+ // the out-of-band SAS confirmation round-trip via the PAIR_WITH_DESKTOP /90+ // DESKTOP_PAIR_* messages. The SAS gate is what authenticates the peer91+ // (audit HIGH — pairing initiator authentication), not the pairing token.13292 const [desktopPhase, setDesktopPhase] = createSignal<DesktopPhase>("enter");13393 const [desktopCode, setDesktopCode] = createSignal("");13494 const [desktopSas, setDesktopSas] = createSignal<string | null>(null);@@ -149,14 +109,8 @@149109 setDesktopDevice(undefined);150110 setDesktopBusy(false);151111 setDesktopPhase("enter");152- setPairKind("desktop");153112 }154113 155- function startChirpFlow(): void {156- setPairKind("chirp");157- void handleStartPairing();158- }159-160114 function onDesktopCodeInput(value: string): void {161115 const cleaned = value.toUpperCase().replace(/[^0-9A-Z]/g, "").slice(0, 32);162116 // Group once the input outgrows a 6-digit helper code, so a pairing code@@ -213,8 +167,7 @@213167 onMount(() => {214168 // Opened straight into a pairing path from the Settings sync card. With no215169 // method there's nothing to do here — return to Settings (the hub).216- if (props.initialPair === "chirp") startChirpFlow();217- else if (props.initialPair === "desktop") startDesktopFlow();170+ if (props.initialPair === "desktop") startDesktopFlow();218171 else props.onBack();219172 220173 // Desktop pairing pushes from the background (bg → popup). The whole NK@@ -222,7 +175,6 @@222175 // result arrive here as runtime messages.223176 if (typeof chrome !== "undefined" && chrome.runtime?.onMessage) {224177 const handler = (msg: { type?: string; payload?: unknown }) => {225- if (pairKind() !== "desktop") return;226178 if (msg?.type === "DESKTOP_PAIR_SAS") {227179 const p = msg.payload as { sas?: string } | undefined;228180 if (typeof p?.sas === "string") { setDesktopSas(p.sas); setDesktopPhase("sas"); }@@ -248,148 +200,6 @@248200 });249201 });250202 251- async function handleStartPairing(): Promise<void> {252- // audioCapture is declared as optional_permissions in the manifest.253- // Chrome will silently deny getUserMedia unless we request it here,254- // during a user gesture. Firefox ignores this API but getUserMedia works255- // under its existing host permission grant.256- if (typeof chrome !== "undefined" && chrome.permissions?.request) {257- const granted = await chrome.permissions.request({258- permissions: ["audioCapture" as chrome.runtime.ManifestPermissions],259- });260- if (!granted) {261- setPairing({ phase: "mic_denied" });262- return;263- }264- }265-266- setPairing({ phase: "idle" });267-268- const deps: ChirpPairingDeps = {269- onProgress: (p) => setPairing(p),270- fetchLocalIdentity: async () => {271- const res = await sendMessage({ type: "CHIRP_GET_LOCAL_IDENTITY" });272- const r = res as { ok: boolean; data?: Parameters<ChirpPairingDeps["onProgress"]>[0] & object; error?: string };273- if (!r.ok || !r.data) throw new Error(r.error ?? "identity_unavailable");274- // The handler returns a LocalIdentity; we trust the background's shape.275- return r.data as unknown as Awaited<ReturnType<ChirpPairingDeps["fetchLocalIdentity"]>>;276- },277- completePairing: async (paired) => {278- const res = await sendMessage({279- type: "CHIRP_COMPLETE_PAIRING",280- payload: { paired },281- });282- const r = res as { ok: boolean; error?: string };283- if (!r.ok) throw new Error(r.error ?? "complete_pairing_failed");284- },285- confirmSAS: (sas) =>286- new Promise<boolean>((resolve) => {287- setSasResolve(() => resolve);288- setSasCode(sas);289- }),290- // Receive Pro from an already-Pro peer (signature-verified in the bg).291- // Skip if we're already Pro so we keep our own license.292- activatePeerLicense: async (license) => {293- if (props.isPro) return;294- const res = (await sendMessage({295- type: "ACTIVATE_LICENSE",296- payload: { key: license },297- })) as { ok: boolean; data?: { valid?: boolean; plan?: string } };298- if (res.ok && res.data?.valid && res.data?.plan === "pro") props.onProChanged?.();299- },300- grantAdoptionConsent: (request, sas) =>301- new Promise<boolean>((resolve) => {302- setAdoptionConsent({ request, sas, resolve });303- }),304- fetchAdoptionMaterial: async () => {305- const res = await sendMessage({ type: "CHIRP_GET_ADOPTION_MATERIAL" });306- const r = res as {307- ok: boolean;308- data?: { rootKeyHex?: string; vaultId?: string; license?: string };309- error?: string;310- };311- if (!r.ok || typeof r.data?.rootKeyHex !== "string" || typeof r.data?.vaultId !== "string") {312- throw new Error(r.error ?? "adoption_material_unavailable");313- }314- return {315- rootKeyHex: r.data.rootKeyHex,316- vaultId: r.data.vaultId,317- license: r.data.license,318- };319- },320- getLocalSubnet,321- openPairingTransport: async (endpoint) => {322- // Native WebSocket transport for the pairing-mode peer scan. The323- // chirp-pair orchestrator only uses send/receive/close, so a thin324- // adapter over the platform WebSocket suffices.325- return new Promise((resolve, reject) => {326- let ws: WebSocket;327- try {328- ws = new WebSocket(`ws://${endpoint.ip}:${endpoint.port}/`);329- } catch (err) {330- reject(err);331- return;332- }333- const inbox: Uint8Array[] = [];334- const waiters: Array<(b: Uint8Array) => void> = [];335- let closed = false;336- ws.binaryType = "arraybuffer";337- const timer = setTimeout(() => {338- try { ws.close(); } catch { /* ignore */ }339- reject(new Error("pairing_transport_timeout"));340- }, 2_000);341- ws.onopen = () => {342- clearTimeout(timer);343- resolve({344- async send(data: Uint8Array) {345- ws.send(data as Uint8Array<ArrayBuffer>);346- },347- receive(): Promise<Uint8Array> {348- if (closed) return Promise.reject(new Error("closed"));349- const v = inbox.shift();350- if (v) return Promise.resolve(v);351- return new Promise<Uint8Array>((res) => waiters.push(res));352- },353- async close() {354- try { ws.close(); } catch { /* ignore */ }355- },356- });357- };358- ws.onmessage = (ev) => {359- if (!(ev.data instanceof ArrayBuffer)) return;360- const bytes = new Uint8Array(ev.data);361- const w = waiters.shift();362- if (w) w(bytes); else inbox.push(bytes);363- };364- ws.onerror = () => {365- clearTimeout(timer);366- reject(new Error("pairing_transport_error"));367- };368- ws.onclose = () => {369- closed = true;370- while (waiters.length > 0) waiters.shift()!(new Uint8Array(0));371- };372- });373- },374- };375-376- const role: PairingRole = isNativePlatform() ? "initiator" : "responder";377- if (role === "initiator") deps.openPairingServer = openCapacitorPairingServer;378-379- try {380- await runChirpPairing(deps, role);381- // Success surfaces in `pairing()` as the "paired" phase; ChirpPairingFlow382- // shows it and onDismiss returns to Settings.383- } catch (err) {384- if (err instanceof ChirpPairingFailure) {385- // The orchestrator already pushed the terminal phase into `pairing()`;386- // the inline error copy renders. The user can tap "Try again"/"Cancel".387- return;388- }389- setPairing({ phase: "handshake_failed", errorMessage: (err as Error).message });390- }391- }392-393203 // --- Sub-views --------------------------------------------------394204 395205 const backArrow = (@@ -526,84 +336,7 @@526336 </div>527337 );528338 529- const chirpPairingView = () => (530- <Show531- when={sasCode()}532- fallback={533- <ChirpPairingFlow534- progress={pairing()}535- onCancel={() => props.onBack()}536- onRetry={() => void handleStartPairing()}537- onDismiss={() => props.onBack()}538- />539- }540- >541- {/* Out-of-band verification gate — pairing is NOT persisted until the542- user confirms this code matches the one on the other device. */}543- <div544- class="chirp-sas-verify"545- style="display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 18px;text-align:center;"546- >547- <div style="font-size:14px;font-weight:600;color:var(--peach-text);">Verify it's really your device</div>548- <div style="font-size:11.5px;line-height:1.45;color:var(--peach-text-muted);max-width:240px;">549- Confirm this code matches the one shown on your other device before pairing.550- </div>551- <div style="font-size:30px;font-weight:700;letter-spacing:0.32em;font-family:'IBM Plex Mono',monospace;color:var(--peach-accent);padding:6px 0 2px;">552- {sasCode()}553- </div>554- <div style="display:flex;flex-direction:column;gap:8px;width:100%;max-width:240px;">555- <button556- class="peach-btn peach-btn--accent peach-btn--md"557- type="button"558- onClick={() => resolveSAS(true)}559- >560- codes match — pair561- </button>562- <button563- class="peach-btn peach-btn--ghost peach-btn--md"564- type="button"565- onClick={() => resolveSAS(false)}566- >567- they don't match568- </button>569- </div>570- </div>571- </Show>572- );573-574- // Pairing surface dispatch: the user already chose chirp vs. code on the rest575- // screen (or the sync card), so there's no method-select step.576- const pairingView = () => (577- <Show when={pairKind() === "desktop"} fallback={chirpPairingView()}>578- {desktopView()}579- </Show>580- );581-582339 // Sync is no longer Pro-gated: anyone may attempt to pair so a free device can583340 // receive Pro from an already-Pro peer. The upsell lives on the Settings card.584- return (585- <>586- {pairingView()}587- <Show when={adoptionConsent()}>588- {(pending) => (589- <AdoptionConsentModal590- receiverDeviceName={pending().request.device_name}591- sas={pending().sas}592- verifyMasterPassword={async (password) => {593- const res = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password } });594- return Boolean((res as { ok: boolean; data?: boolean }).ok && (res as { data?: boolean }).data);595- }}596- onCancel={() => {597- pending().resolve(false);598- setAdoptionConsent(null);599- }}600- onApprove={async () => {601- pending().resolve(true);602- setAdoptionConsent(null);603- }}604- />605- )}606- </Show>607- </>608- );341+ return desktopView();609342 };src/popup/views/Settings.tsx+1 −1
@@ -61,7 +61,7 @@6161 onExport: () => void;6262 onBreachCheckDone: () => void;6363 onOpenHealth?: () => void;64- onOpenSyncPairing?: (method?: "chirp" | "desktop") => void;64+ onOpenSyncPairing?: (method?: "desktop") => void;6565 onOpenFileImport?: () => void;6666 onOpenPanic?: () => void;6767 onOpenCodexGenerate?: () => void;src/shared/chirp-pairing.test.ts+0 −191
@@ -1,191 +0,0 @@1-import { afterEach, beforeEach, describe, expect, it } from "vitest";2-import { CHIRP_PAYLOAD_LENGTH, DEVICE_TYPE_ANDROID, DEVICE_TYPE_EXTENSION } from "./sync-types";3-import { encodeChirpPayload } from "./chirp-payload";4-import {5- ChirpExchangeError,6- createPairingAttempt,7- runChirpExchange,8- type AudioIO,9-} from "./chirp-pairing";10-import { __setGgwaveModuleForTest } from "./ggwave";11-12-function fakeGgwaveThatPassesThroughBytes() {13- // Synthesize a ggwave that round-trips: encode → push raw bytes into the14- // waveform, decode → return any waveform whose first 64 samples decode as15- // a payload. This keeps the chirp-pairing logic in scope without needing16- // a real FSK codec.17- return {18- ProtocolId: { GGWAVE_PROTOCOL_ULTRASOUND_FASTEST: 5 },19- GGWAVE_OPERATING_MODE_RX: 1,20- GGWAVE_OPERATING_MODE_TX: 2,21- GGWAVE_OPERATING_MODE_RX_AND_TX: 3,22- getDefaultParameters() {23- return { sampleRateInp: 48000, sampleRateOut: 48000, samplesPerFrame: 1024, soundMarkerThreshold: 3, payloadLength: -1, operatingMode: 0 };24- },25- init() { return 1; },26- free() { /* no-op */ },27- encode(_instance: number, data: Uint8Array | string, _proto: number, _vol: number) {28- const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;29- // Pack bytes as float samples in the first N positions; the rest30- // padded with sentinel value 99 so a partial read doesn't match.31- const out = new Float32Array(Math.max(bytes.length, 128));32- for (let i = 0; i < bytes.length; i++) out[i] = bytes[i] / 255;33- for (let i = bytes.length; i < out.length; i++) out[i] = 99;34- return out;35- },36- decode(_instance: number, samples: Float32Array | Int16Array) {37- if (samples.length < CHIRP_PAYLOAD_LENGTH) return null;38- // Read first 64 samples back as bytes if they look like our encoding.39- // Sentinel 99 means "no chirp here".40- const buf = new Uint8Array(CHIRP_PAYLOAD_LENGTH);41- for (let i = 0; i < CHIRP_PAYLOAD_LENGTH; i++) {42- const v = (samples as Float32Array)[i];43- if (v === 99) return null;44- buf[i] = Math.round(v * 255);45- }46- return buf;47- },48- };49-}50-51-function fakeIO(): AudioIO & {52- injectIncoming(samples: Float32Array): void;53- playedCount(): number;54- closed(): boolean;55-} {56- let pending = new Float32Array(0);57- let playCount = 0;58- let isClosed = false;59- return {60- sampleRate: 48000,61- async play(_waveform: Float32Array) {62- playCount++;63- },64- async pullSamples() {65- const out = pending;66- pending = new Float32Array(0);67- return out;68- },69- async close() {70- isClosed = true;71- },72- injectIncoming(samples: Float32Array) {73- const next = new Float32Array(pending.length + samples.length);74- next.set(pending, 0);75- next.set(samples, pending.length);76- pending = next;77- },78- playedCount: () => playCount,79- closed: () => isClosed,80- };81-}82-83-describe("createPairingAttempt", () => {84- it("rolls fresh ephemerals and a 16-byte nonce each call", () => {85- const a = createPairingAttempt(DEVICE_TYPE_EXTENSION);86- const b = createPairingAttempt(DEVICE_TYPE_EXTENSION);87- expect(a.ephemeralPublicKey).not.toEqual(b.ephemeralPublicKey);88- expect(a.sessionNonce.length).toBe(16);89- expect(a.outgoingPayload.ephemeralPublicKey).toEqual(a.ephemeralPublicKey);90- expect(a.outgoingPayload.deviceType).toBe(DEVICE_TYPE_EXTENSION);91- });92-});93-94-describe("runChirpExchange", () => {95- beforeEach(() => __setGgwaveModuleForTest(fakeGgwaveThatPassesThroughBytes()));96- afterEach(() => __setGgwaveModuleForTest(null));97-98- it("returns the remote payload when a valid chirp is received", async () => {99- const io = fakeIO();100- const ours = createPairingAttempt(DEVICE_TYPE_EXTENSION);101- const theirs = createPairingAttempt(DEVICE_TYPE_ANDROID);102- const theirBytes = encodeChirpPayload(theirs.outgoingPayload);103- // Convert the 64 bytes into the encoded waveform shape our fake produces.104- const theirWave = new Float32Array(128);105- for (let i = 0; i < theirBytes.length; i++) theirWave[i] = theirBytes[i] / 255;106- for (let i = theirBytes.length; i < theirWave.length; i++) theirWave[i] = 99;107- io.injectIncoming(theirWave);108-109- const result = await runChirpExchange(io, { attempt: ours, timeoutMs: 2000 });110- expect(result.remoteChirp.ephemeralPublicKey).toEqual(theirs.ephemeralPublicKey);111- expect(result.remoteChirp.sessionNonce).toEqual(theirs.sessionNonce);112- expect(result.remoteChirp.deviceType).toBe(DEVICE_TYPE_ANDROID);113- expect(io.closed()).toBe(true);114- });115-116- it("ignores echo of our own chirp", async () => {117- const io = fakeIO();118- const ours = createPairingAttempt(DEVICE_TYPE_EXTENSION);119- const ourBytes = encodeChirpPayload(ours.outgoingPayload);120- const ourWave = new Float32Array(128);121- for (let i = 0; i < ourBytes.length; i++) ourWave[i] = ourBytes[i] / 255;122- for (let i = ourBytes.length; i < ourWave.length; i++) ourWave[i] = 99;123- // Inject our own chirp — should be ignored124- io.injectIncoming(ourWave);125-126- // After 200ms inject a real peer chirp127- const theirs = createPairingAttempt(DEVICE_TYPE_ANDROID);128- const theirBytes = encodeChirpPayload(theirs.outgoingPayload);129- const theirWave = new Float32Array(128);130- for (let i = 0; i < theirBytes.length; i++) theirWave[i] = theirBytes[i] / 255;131- for (let i = theirBytes.length; i < theirWave.length; i++) theirWave[i] = 99;132- setTimeout(() => io.injectIncoming(theirWave), 150);133-134- const result = await runChirpExchange(io, { attempt: ours, timeoutMs: 3000 });135- expect(result.remoteChirp.ephemeralPublicKey).toEqual(theirs.ephemeralPublicKey);136- });137-138- it("times out after the configured deadline when no chirp arrives", async () => {139- const io = fakeIO();140- const ours = createPairingAttempt(DEVICE_TYPE_EXTENSION);141- await expect(runChirpExchange(io, { attempt: ours, timeoutMs: 200 })).rejects.toMatchObject({142- kind: "timeout",143- });144- expect(io.closed()).toBe(true);145- });146-147- it("raises version_mismatch on a peer with a different protocol version", async () => {148- const io = fakeIO();149- const ours = createPairingAttempt(DEVICE_TYPE_EXTENSION);150- const theirs = createPairingAttempt(DEVICE_TYPE_ANDROID);151- // Mint a chirp with a future protocol version, then re-stamp the CRC152- const tweaked = { ...theirs.outgoingPayload, version: 0x02 };153- const bytes = encodeChirpPayload({154- ...tweaked,155- // encodeChirpPayload writes a fresh CRC32 over the new bytes156- });157- const wave = new Float32Array(128);158- for (let i = 0; i < bytes.length; i++) wave[i] = bytes[i] / 255;159- for (let i = bytes.length; i < wave.length; i++) wave[i] = 99;160- io.injectIncoming(wave);161-162- await expect(runChirpExchange(io, { attempt: ours, timeoutMs: 2000 })).rejects.toMatchObject({163- kind: "version_mismatch",164- });165- });166-167- it("ignores noise chunks (no chirp detected) and keeps listening", async () => {168- const io = fakeIO();169- const ours = createPairingAttempt(DEVICE_TYPE_EXTENSION);170- // Push pure noise — fake decoder returns null because samples have value 99171- io.injectIncoming(new Float32Array(256).fill(99));172-173- const theirs = createPairingAttempt(DEVICE_TYPE_ANDROID);174- const theirBytes = encodeChirpPayload(theirs.outgoingPayload);175- const theirWave = new Float32Array(128);176- for (let i = 0; i < theirBytes.length; i++) theirWave[i] = theirBytes[i] / 255;177- for (let i = theirBytes.length; i < theirWave.length; i++) theirWave[i] = 99;178- setTimeout(() => io.injectIncoming(theirWave), 100);179-180- const result = await runChirpExchange(io, { attempt: ours, timeoutMs: 3000 });181- expect(result.remoteChirp.ephemeralPublicKey).toEqual(theirs.ephemeralPublicKey);182- });183-});184-185-describe("ChirpExchangeError", () => {186- it("carries a kind tag", () => {187- const err = new ChirpExchangeError("timeout");188- expect(err.kind).toBe("timeout");189- expect(err).toBeInstanceOf(Error);190- });191-});src/shared/chirp-pairing.ts+0 −291
@@ -1,291 +0,0 @@1-// Audio pairing driver — orchestrates the two-chirp exchange that2-// bootstraps trust between two devices in the same room.3-//4-// Both devices simultaneously:5-// - play their chirp on a 4-second loop6-// - listen to the microphone, attempting to decode an incoming chirp7-//8-// Once each side has decoded the OTHER side's PEAC chirp, the pairing9-// transitions to its post-chirp phase (Noise NK over WebSocket — driven10-// by the coordinator in `chirp-pairing-handshake.ts`, not by this module).11-//12-// This module is deliberately transport-agnostic at its boundaries: it13-// owns the Web Audio + getUserMedia layer and nothing else. The chirp14-// produced here is bit-identical with what the Android `ggwave-android`15-// JNI binding produces — the ggwave-fastest ultrasonic protocol is the16-// shared envelope.17-//18-// Replay protection note: the `sessionNonce` in the outgoing chirp is19-// distinct on every pairing attempt (`createPairingAttempt` rolls a fresh20-// 16-byte random nonce); a captured chirp replayed on retry produces a21-// nonce mismatch on the second-leg key exchange and is rejected.22-23-import { generateKeyPair } from "@stablelib/x25519";24-import { decodeChirpPayload, ChirpDecodeError } from "./chirp-payload";25-import { createChirpCodec } from "./ggwave";26-import {27- CHIRP_PROTOCOL_VERSION,28- type ChirpPayload,29- type DeviceTypeByte,30- CHIRP_PAYLOAD_LENGTH,31-} from "./sync-types";32-import { encodeChirpPayload } from "./chirp-payload";33-34-const CHIRP_LOOP_INTERVAL_MS = 4_000;35-const DEFAULT_TIMEOUT_MS = 90_000;36-const LISTEN_FRAME_SAMPLES = 4096;37-38-export interface PairingAttempt {39- ephemeralPrivateKey: Uint8Array; // X25519 — never leaves this module40- ephemeralPublicKey: Uint8Array;41- sessionNonce: Uint8Array; // 16 bytes42- outgoingPayload: ChirpPayload;43-}44-45-export function createPairingAttempt(deviceType: DeviceTypeByte): PairingAttempt {46- const kp = generateKeyPair();47- const nonce = new Uint8Array(16);48- cryptoFill(nonce);49- return {50- ephemeralPrivateKey: kp.secretKey,51- ephemeralPublicKey: kp.publicKey,52- sessionNonce: nonce,53- outgoingPayload: {54- version: CHIRP_PROTOCOL_VERSION,55- deviceType,56- ephemeralPublicKey: kp.publicKey,57- sessionNonce: nonce,58- },59- };60-}61-62-function cryptoFill(buf: Uint8Array): void {63- if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {64- crypto.getRandomValues(buf as Uint8Array<ArrayBuffer>);65- return;66- }67- // SAFETY: every supported runtime (MV3 service worker, popup page,68- // Capacitor WebView) exposes globalThis.crypto.getRandomValues. The69- // fallback is therefore only ever hit in a broken environment, and70- // failing closed is the right behaviour.71- throw new Error("crypto.getRandomValues unavailable");72-}73-74-export interface ChirpExchangeResult {75- remoteChirp: ChirpPayload;76-}77-78-export interface ChirpExchangeOptions {79- attempt: PairingAttempt;80- timeoutMs?: number;81- /** Notified each time we abandon a malformed/non-PEAC frame. */82- onDiscard?: (reason: ChirpDecodeError["kind"]) => void;83- /** Notified each time we replay our own chirp. */84- onChirpPlayed?: (count: number) => void;85-}86-87-/**88- * Audio I/O abstraction. Wired to real Web Audio + getUserMedia in89- * `runChirpExchange`; tests pass a deterministic fake so the play/listen90- * choreography can be exercised in jsdom.91- */92-export interface AudioIO {93- sampleRate: number;94- play(waveform: Float32Array): Promise<void>;95- pullSamples(): Promise<Float32Array>;96- close(): Promise<void>;97-}98-99-/**100- * Run the two-chirp exchange against a provided `AudioIO`. Returns once we101- * have decoded a valid PEAC chirp from a peer; throws on timeout, mic102- * failure, or a version-mismatch chirp from a too-new peer.103- *104- * The function continues chirping while it listens — the two devices105- * always overlap; ggwave's frequency sub-bands inside the ultrasonic106- * range let both sides transmit and receive simultaneously.107- */108-export async function runChirpExchange(109- io: AudioIO,110- opts: ChirpExchangeOptions,111-): Promise<ChirpExchangeResult> {112- const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;113- const codec = await createChirpCodec({ sampleRate: io.sampleRate });114- const ourBytes = encodeChirpPayload(opts.attempt.outgoingPayload);115- const ourWaveform = codec.encode(ourBytes);116-117- const deadline = Date.now() + timeoutMs;118- let playCount = 0;119- let stopped = false;120- let wakePlayLoop: (() => void) | null = null;121-122- function abortPlayLoop() {123- stopped = true;124- if (wakePlayLoop) wakePlayLoop();125- }126-127- function interruptibleSleep(ms: number): Promise<void> {128- return new Promise<void>((resolve) => {129- const t = setTimeout(resolve, ms);130- wakePlayLoop = () => { clearTimeout(t); resolve(); };131- });132- }133-134- const playLoop = (async () => {135- while (!stopped && Date.now() < deadline) {136- try {137- await io.play(ourWaveform);138- playCount++;139- opts.onChirpPlayed?.(playCount);140- } catch {141- // A play failure (audio device contention, tab backgrounded) is142- // not fatal — we keep listening; the peer may already be in range.143- }144- if (stopped) break;145- const remaining = deadline - Date.now();146- const wait = Math.min(CHIRP_LOOP_INTERVAL_MS, Math.max(0, remaining));147- if (wait <= 0) break;148- await interruptibleSleep(wait);149- }150- })();151-152- try {153- while (Date.now() < deadline && !stopped) {154- const samples = await io.pullSamples();155- if (samples.length === 0) {156- await sleep(50);157- continue;158- }159- const decoded = codec.decode(samples);160- if (!decoded) continue;161- if (decoded.length !== CHIRP_PAYLOAD_LENGTH) {162- opts.onDiscard?.("length");163- continue;164- }165- try {166- const payload = decodeChirpPayload(decoded);167- // Ignore an echo of our own chirp — happens routinely in tight168- // ambient feedback. If the bytes match, drop and keep listening.169- if (sameBytes(payload.ephemeralPublicKey, opts.attempt.ephemeralPublicKey)) {170- continue;171- }172- if (payload.version !== CHIRP_PROTOCOL_VERSION) {173- throw new ChirpExchangeError("version_mismatch");174- }175- stopped = true;176- return { remoteChirp: payload };177- } catch (err) {178- if (err instanceof ChirpExchangeError) throw err;179- if (err instanceof ChirpDecodeError) {180- if (err.kind === "version") {181- // A well-formed PEAC chirp with a protocol byte we don't speak182- // is a real version mismatch — bubble it up so the UI can show183- // the "Update Peach on both devices" copy. CRC validates first,184- // so this isn't fired by a random RF glitch.185- throw new ChirpExchangeError("version_mismatch");186- }187- opts.onDiscard?.(err.kind);188- continue;189- }190- throw err;191- }192- }193- throw new ChirpExchangeError("timeout");194- } finally {195- abortPlayLoop();196- codec.free();197- await playLoop.catch(() => undefined);198- await io.close().catch(() => undefined);199- }200-}201-202-export class ChirpExchangeError extends Error {203- readonly kind: "timeout" | "version_mismatch" | "mic_denied" | "audio_unavailable";204- constructor(kind: ChirpExchangeError["kind"], message?: string) {205- super(message ?? kind);206- this.kind = kind;207- }208-}209-210-function sameBytes(a: Uint8Array, b: Uint8Array): boolean {211- if (a.length !== b.length) return false;212- for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;213- return true;214-}215-216-function sleep(ms: number): Promise<void> {217- return new Promise((res) => setTimeout(res, ms));218-}219-220-/**221- * Build a `AudioIO` backed by Web Audio + getUserMedia. Allocates an222- * AudioContext (caller must invoke from a user gesture — the click that223- * launched pairing — or playback is suspended on Chrome and Safari).224- *225- * `pullSamples` returns the latest accumulated mono buffer from the mic226- * and resets the internal accumulator. Calling repeatedly yields227- * non-overlapping slices of recent audio; ggwave's marker-based decoder228- * tolerates this chunking.229- */230-export async function openBrowserAudioIO(): Promise<AudioIO> {231- if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {232- throw new ChirpExchangeError("audio_unavailable", "getUserMedia not available");233- }234- const ctx: AudioContext = new (globalThis.AudioContext ??235- (globalThis as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)();236- let stream: MediaStream;237- try {238- stream = await navigator.mediaDevices.getUserMedia({239- audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false },240- });241- } catch (err) {242- await ctx.close();243- throw new ChirpExchangeError("mic_denied", (err as Error).message);244- }245- const source = ctx.createMediaStreamSource(stream);246-247- // ScriptProcessorNode is deprecated but is the only widely available248- // pull-style API in MV3 service workers and the Firefox popup; AudioWorklet249- // would be cleaner but requires a separate module file. The buffer size250- // 4096 corresponds to ~90ms at 48kHz — short enough that pairing feels251- // responsive, long enough that ggwave's FSK windows align.252- const processor = ctx.createScriptProcessor(LISTEN_FRAME_SAMPLES, 1, 1);253- let accumulator: Float32Array = new Float32Array(0);254-255- processor.onaudioprocess = (ev) => {256- const input = ev.inputBuffer.getChannelData(0);257- const next = new Float32Array(accumulator.length + input.length);258- next.set(accumulator, 0);259- next.set(input, accumulator.length);260- accumulator = next;261- };262-263- source.connect(processor);264- processor.connect(ctx.destination);265-266- return {267- sampleRate: ctx.sampleRate,268- async play(waveform: Float32Array): Promise<void> {269- const buffer = ctx.createBuffer(1, waveform.length, ctx.sampleRate);270- buffer.copyToChannel(waveform as Float32Array<ArrayBuffer>, 0, 0);271- const src = ctx.createBufferSource();272- src.buffer = buffer;273- src.connect(ctx.destination);274- await new Promise<void>((resolve) => {275- src.onended = () => resolve();276- src.start();277- });278- },279- async pullSamples(): Promise<Float32Array> {280- const out = accumulator;281- accumulator = new Float32Array(0);282- return out;283- },284- async close(): Promise<void> {285- processor.disconnect();286- source.disconnect();287- for (const track of stream.getTracks()) track.stop();288- await ctx.close();289- },290- };291-}src/shared/chirp-payload.test.ts+0 −106
@@ -1,106 +0,0 @@1-import { describe, expect, it } from "vitest";2-import {3- CHIRP_MAGIC_BYTES,4- CHIRP_PAYLOAD_LENGTH,5- CHIRP_PROTOCOL_VERSION,6- DEVICE_TYPE_ANDROID,7- DEVICE_TYPE_EXTENSION,8-} from "./sync-types";9-import {10- ChirpDecodeError,11- crc32,12- decodeChirpPayload,13- encodeChirpPayload,14-} from "./chirp-payload";15-16-function fixedPayload() {17- return {18- version: CHIRP_PROTOCOL_VERSION,19- deviceType: DEVICE_TYPE_EXTENSION,20- ephemeralPublicKey: Uint8Array.from(21- Array.from({ length: 32 }, (_, i) => (i * 7 + 11) & 0xff),22- ),23- sessionNonce: Uint8Array.from(24- Array.from({ length: 16 }, (_, i) => (i * 31 + 1) & 0xff),25- ),26- };27-}28-29-describe("crc32", () => {30- it("matches known IEEE 802.3 vectors", () => {31- expect(crc32(new Uint8Array())).toBe(0x00000000);32- expect(crc32(new TextEncoder().encode("123456789"))).toBe(0xcbf43926);33- expect(crc32(new TextEncoder().encode("a"))).toBe(0xe8b7be43);34- });35-});36-37-describe("chirp payload codec", () => {38- it("round-trips identity for a fixed payload", () => {39- const original = fixedPayload();40- const bytes = encodeChirpPayload(original);41- expect(bytes.length).toBe(CHIRP_PAYLOAD_LENGTH);42- const decoded = decodeChirpPayload(bytes);43- expect(decoded.version).toBe(original.version);44- expect(decoded.deviceType).toBe(original.deviceType);45- expect(decoded.ephemeralPublicKey).toEqual(original.ephemeralPublicKey);46- expect(decoded.sessionNonce).toEqual(original.sessionNonce);47- });48-49- it("writes magic bytes PEAC at offset 0", () => {50- const bytes = encodeChirpPayload(fixedPayload());51- expect(bytes.subarray(0, 4)).toEqual(CHIRP_MAGIC_BYTES);52- });53-54- it("zero-fills the reserved padding region", () => {55- const bytes = encodeChirpPayload(fixedPayload());56- for (let i = 58; i < 64; i++) expect(bytes[i]).toBe(0);57- });58-59- it("rejects wrong-length input", () => {60- expect(() => decodeChirpPayload(new Uint8Array(63))).toThrow(ChirpDecodeError);61- expect(() => decodeChirpPayload(new Uint8Array(65))).toThrow(ChirpDecodeError);62- });63-64- it("rejects bad magic", () => {65- const bytes = encodeChirpPayload(fixedPayload());66- bytes[0] = 0;67- expect(() => decodeChirpPayload(bytes)).toThrow(/magic/);68- });69-70- it("rejects unsupported version", () => {71- const bytes = encodeChirpPayload(fixedPayload());72- bytes[4] = 0xff;73- // CRC will also fail, but version is checked first74- expect(() => decodeChirpPayload(bytes)).toThrow(/version/);75- });76-77- it("rejects unknown device type", () => {78- const bytes = encodeChirpPayload(fixedPayload());79- bytes[5] = 0x09;80- expect(() => decodeChirpPayload(bytes)).toThrow(/device type/);81- });82-83- it("rejects checksum tamper", () => {84- const bytes = encodeChirpPayload(fixedPayload());85- bytes[10] ^= 1; // flip a pubkey bit86- expect(() => decodeChirpPayload(bytes)).toThrow(/CRC32/);87- });88-89- it("rejects non-zero padding", () => {90- const bytes = encodeChirpPayload(fixedPayload());91- // Mutate padding then re-patch CRC over the first 54 bytes so that the92- // padding check is reached rather than failing at CRC first.93- bytes[63] = 0xab;94- const fresh = crc32(bytes.subarray(0, 54));95- bytes[54] = (fresh >>> 24) & 0xff;96- bytes[55] = (fresh >>> 16) & 0xff;97- bytes[56] = (fresh >>> 8) & 0xff;98- bytes[57] = fresh & 0xff;99- expect(() => decodeChirpPayload(bytes)).toThrow(/padding/);100- });101-102- it("encodes android device type byte", () => {103- const bytes = encodeChirpPayload({ ...fixedPayload(), deviceType: DEVICE_TYPE_ANDROID });104- expect(bytes[5]).toBe(0x02);105- });106-});src/shared/chirp-payload.ts+0 −139
@@ -1,139 +0,0 @@1-// Audio pairing payload — 64 bytes on the wire.2-//3-// magic 4 ASCII "PEAC"4-// version 1 currently 0x015-// deviceType 1 0x01=ext, 0x02=android, 0x03=ios, 0x04=desktop6-// ephemeralPubkey 32 X25519 public key for the pairing Noise NK session7-// sessionNonce 16 random per pairing attempt; replay-detection on retry8-// checksum 4 CRC32 (IEEE 802.3) of the preceding 54 bytes9-// padding 6 zero; reserved10-//11-// No IP, no name, no version-string — those are all carried inside the12-// Noise NK encrypted channel once the chirp has done its job. The on-air13-// payload is therefore the smallest stable shape that two devices can14-// negotiate trust from.15-16-import {17- CHIRP_MAGIC_BYTES,18- CHIRP_PAYLOAD_LENGTH,19- CHIRP_PROTOCOL_VERSION,20- type ChirpPayload,21- type DeviceTypeByte,22-} from "./sync-types";23-24-const OFFSET_MAGIC = 0;25-const OFFSET_VERSION = 4;26-const OFFSET_DEVICE_TYPE = 5;27-const OFFSET_PUBKEY = 6;28-const OFFSET_NONCE = 38;29-const OFFSET_CHECKSUM = 54;30-const OFFSET_PADDING = 58;31-const CHECKSUM_COVERAGE = 54;32-33-export class ChirpDecodeError extends Error {34- readonly kind:35- | "length"36- | "magic"37- | "version"38- | "device_type"39- | "checksum"40- | "padding";41- constructor(kind: ChirpDecodeError["kind"], message: string) {42- super(message);43- this.kind = kind;44- }45-}46-47-export function encodeChirpPayload(payload: ChirpPayload): Uint8Array {48- if (payload.ephemeralPublicKey.length !== 32) {49- throw new Error("ephemeralPublicKey must be 32 bytes");50- }51- if (payload.sessionNonce.length !== 16) {52- throw new Error("sessionNonce must be 16 bytes");53- }54- const buf = new Uint8Array(CHIRP_PAYLOAD_LENGTH);55- buf.set(CHIRP_MAGIC_BYTES, OFFSET_MAGIC);56- buf[OFFSET_VERSION] = payload.version & 0xff;57- buf[OFFSET_DEVICE_TYPE] = payload.deviceType & 0xff;58- buf.set(payload.ephemeralPublicKey, OFFSET_PUBKEY);59- buf.set(payload.sessionNonce, OFFSET_NONCE);60- const crc = crc32(buf.subarray(0, CHECKSUM_COVERAGE));61- buf[OFFSET_CHECKSUM] = (crc >>> 24) & 0xff;62- buf[OFFSET_CHECKSUM + 1] = (crc >>> 16) & 0xff;63- buf[OFFSET_CHECKSUM + 2] = (crc >>> 8) & 0xff;64- buf[OFFSET_CHECKSUM + 3] = crc & 0xff;65- // padding bytes already 066- return buf;67-}68-69-export function decodeChirpPayload(buf: Uint8Array): ChirpPayload {70- if (buf.length !== CHIRP_PAYLOAD_LENGTH) {71- throw new ChirpDecodeError(72- "length",73- `chirp payload must be ${CHIRP_PAYLOAD_LENGTH} bytes, got ${buf.length}`,74- );75- }76- for (let i = 0; i < 4; i++) {77- if (buf[OFFSET_MAGIC + i] !== CHIRP_MAGIC_BYTES[i]) {78- throw new ChirpDecodeError("magic", "magic bytes are not PEAC");79- }80- }81- const version = buf[OFFSET_VERSION];82- if (version !== CHIRP_PROTOCOL_VERSION) {83- throw new ChirpDecodeError(84- "version",85- `unsupported chirp protocol version ${version}`,86- );87- }88- const deviceType = buf[OFFSET_DEVICE_TYPE];89- if (deviceType < 0x01 || deviceType > 0x04) {90- throw new ChirpDecodeError(91- "device_type",92- `unknown device type byte 0x${deviceType.toString(16)}`,93- );94- }95- const declared =96- ((buf[OFFSET_CHECKSUM] << 24) |97- (buf[OFFSET_CHECKSUM + 1] << 16) |98- (buf[OFFSET_CHECKSUM + 2] << 8) |99- buf[OFFSET_CHECKSUM + 3]) >>>100- 0;101- const computed = crc32(buf.subarray(0, CHECKSUM_COVERAGE));102- if (declared !== computed) {103- throw new ChirpDecodeError("checksum", "CRC32 mismatch");104- }105- for (let i = OFFSET_PADDING; i < CHIRP_PAYLOAD_LENGTH; i++) {106- if (buf[i] !== 0) {107- throw new ChirpDecodeError("padding", "reserved padding bytes must be zero");108- }109- }110- return {111- version,112- deviceType: deviceType as DeviceTypeByte,113- ephemeralPublicKey: buf.slice(OFFSET_PUBKEY, OFFSET_PUBKEY + 32),114- sessionNonce: buf.slice(OFFSET_NONCE, OFFSET_NONCE + 16),115- };116-}117-118-// CRC32 — IEEE 802.3 (poly 0xEDB88320, reflected). Table-driven; built119-// lazily on first use because the chirp codec is not on the hot path.120-let crcTable: Uint32Array | undefined;121-122-function buildCrcTable(): Uint32Array {123- const table = new Uint32Array(256);124- for (let i = 0; i < 256; i++) {125- let c = i;126- for (let k = 0; k < 8; k++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);127- table[i] = c >>> 0;128- }129- return table;130-}131-132-export function crc32(bytes: Uint8Array): number {133- if (!crcTable) crcTable = buildCrcTable();134- let crc = 0xffffffff;135- for (let i = 0; i < bytes.length; i++) {136- crc = crcTable[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);137- }138- return (crc ^ 0xffffffff) >>> 0;139-}src/shared/chirp-transport.probe.test.ts+0 −244
@@ -1,244 +0,0 @@1-// T-010: PCH-01-018 subnet-probe identity-leak integration2-//3-// Why this test exists: PCH-01-018 added a static-key pin check to the Noise4-// XX initiator that fires BEFORE msg3 is sent. msg3 contains the initiator's5-// long-term static public key (our sync identity). If the responder's static6-// doesn't match the expected paired-device key, the handshake must abort7-// WITHOUT ever transmitting msg3. The pin check is tested in isolation in8-// chirp-sync-transport.test.ts, but no integration test verifies the end-to-end9-// "wrong peer → no msg3" guarantee.10-//11-// Approach: wire noiseXXInitiator and noiseXXResponder together through12-// in-memory channels. The responder uses a DIFFERENT static keypair than13-// expected. We spy on the channel to confirm the initiator never sends a14-// third message (msg3 would be the initiator's encrypted static key).15-16-import { describe, expect, it } from "vitest";17-import { generateKeyPair } from "@stablelib/x25519";18-import {19- noiseXXInitiator,20- noiseXXResponder,21-} from "./noise-xx";22-23-// ---------------------------------------------------------------------------24-// In-memory bidirectional channel25-// ---------------------------------------------------------------------------26-27-interface Channel {28- send: (msg: Uint8Array) => Promise<void>;29- receive: () => Promise<Uint8Array>;30-}31-32-function createChannelPair(): { a: Channel; b: Channel } {33- const aToB: Uint8Array[] = [];34- const bToA: Uint8Array[] = [];35- const aToBWaiters: Array<(v: Uint8Array) => void> = [];36- const bToAWaiters: Array<(v: Uint8Array) => void> = [];37-38- return {39- a: {40- send: async (msg) => {41- const waiter = aToBWaiters.shift();42- if (waiter) waiter(msg.slice());43- else aToB.push(msg.slice());44- },45- receive: () => {46- const msg = bToA.shift();47- if (msg) return Promise.resolve(msg);48- return new Promise((resolve) => bToAWaiters.push(resolve));49- },50- },51- b: {52- send: async (msg) => {53- const waiter = bToAWaiters.shift();54- if (waiter) waiter(msg.slice());55- else bToA.push(msg.slice());56- },57- receive: () => {58- const msg = aToB.shift();59- if (msg) return Promise.resolve(msg);60- return new Promise((resolve) => aToBWaiters.push(resolve));61- },62- },63- };64-}65-66-function pubkeyToHex(key: Uint8Array): string {67- return Array.from(key).map(b => b.toString(16).padStart(2, "0")).join("");68-}69-70-// ---------------------------------------------------------------------------71-// Tests72-// ---------------------------------------------------------------------------73-74-describe("T-010: PCH-01-018 subnet-probe identity-leak integration", () => {75- // T-010a: the initiator aborts before msg3 when the responder's static76- // doesn't match the expected paired-device key.77- //78- // Key assertion: the responder never receives a third message. msg1 and msg279- // are exchanged (the Noise XX pre-msg3 handshake), but msg3 — which contains80- // the initiator's long-term static — is never sent.81- //82- // Bug this catches: if verifyRemoteStatic is removed from noiseXXInitiator,83- // the initiator would send msg3 (exposing our static key to an unknown peer)84- // and the responder would complete the handshake.85- it("T-010a: wrong-key responder → initiator aborts before msg3; initiator static not transmitted", async () => {86- // Initiator (our extension's sync identity)87- const initiatorKp = generateKeyPair();88- // Responder (what we EXPECT to talk to)89- const expectedResponderKp = generateKeyPair();90- // Actual responder (a different device on the LAN — the attacker scenario)91- const wrongResponderKp = generateKeyPair();92-93- const expectedHex = pubkeyToHex(expectedResponderKp.publicKey);94- const { a: initiatorChannel, b: responderChannel } = createChannelPair();95-96- // Track how many messages the responder receives97- let responderMsgCount = 0;98- const wrappedResponderReceive = async (): Promise<Uint8Array> => {99- const msg = await responderChannel.receive();100- responderMsgCount++;101- return msg;102- };103-104- // Run the responder in the background using the WRONG static key.105- // It will send msg2 with its wrong static, then wait for msg3.106- const responderPromise = noiseXXResponder(107- wrongResponderKp.secretKey,108- wrongResponderKp.publicKey,109- responderChannel.send,110- wrappedResponderReceive,111- ).catch(() => { /* responder may error when msg3 never arrives */ });112-113- // Run the initiator with a verifyRemoteStatic check against the EXPECTED key.114- const initiatorPromise = noiseXXInitiator(115- initiatorKp.secretKey,116- initiatorKp.publicKey,117- initiatorChannel.send,118- initiatorChannel.receive,119- (remoteStatic) => {120- const hex = pubkeyToHex(remoteStatic);121- if (hex !== expectedHex) {122- throw new Error(123- `PCH-01-018: unexpected peer static (got ${hex.slice(0, 8)}…, ` +124- `expected ${expectedHex.slice(0, 8)}…)`,125- );126- }127- },128- );129-130- // The initiator must throw (pin check failed), not succeed.131- await expect(initiatorPromise).rejects.toThrow(/unexpected peer static|PCH-01-018/i);132-133- // The responder received msg1 (the initiator's ephemeral) and sent msg2.134- // After that the initiator aborts — msg3 is NEVER sent. The responder135- // had its receive() called once (for msg1); it never receives msg3.136- //137- // responderMsgCount === 1: the responder only received msg1.138- // If it were 2, the initiator sent msg3 — our static was leaked.139- expect(responderMsgCount).toBe(1);140-141- // Unblock the parked responder receive() — the responder is waiting142- // for msg3 via b.receive() which reads from the a→b queue. To unblock143- // it we must push into that queue via initiatorChannel.send (a.send).144- initiatorChannel.send(new Uint8Array(0)).catch(() => {});145- await responderPromise;146- });147-148- // T-010b: a correct-key responder allows the handshake to complete.149- // This is the happy path — the pin check passes and msg3 is sent normally.150- // Bug this catches: an overly-aggressive pin check that also rejects valid peers.151- it("T-010b: correct-key responder → handshake proceeds to completion", async () => {152- const initiatorKp = generateKeyPair();153- const responderKp = generateKeyPair();154- const expectedHex = pubkeyToHex(responderKp.publicKey);155-156- const { a: initiatorChannel, b: responderChannel } = createChannelPair();157-158- const responderPromise = noiseXXResponder(159- responderKp.secretKey,160- responderKp.publicKey,161- responderChannel.send,162- responderChannel.receive,163- );164-165- const initiatorPromise = noiseXXInitiator(166- initiatorKp.secretKey,167- initiatorKp.publicKey,168- initiatorChannel.send,169- initiatorChannel.receive,170- (remoteStatic) => {171- const hex = pubkeyToHex(remoteStatic);172- if (hex !== expectedHex) {173- throw new Error("unexpected peer");174- }175- },176- );177-178- const [initiatorSession, responderSession] = await Promise.all([179- initiatorPromise,180- responderPromise,181- ]);182-183- // Both sides get a session — handshake is complete.184- expect(initiatorSession).toBeDefined();185- expect(responderSession).toBeDefined();186-187- // Verify the remote static visible to the initiator is the responder's key188- expect(pubkeyToHex(initiatorSession.remoteStaticPublicKey)).toBe(expectedHex);189- });190-191- // T-010c: no response from the responder (simulate probe timeout).192- // When the initiator's receive() never resolves, the handshake hangs.193- // This test verifies the initiator's abort path works and no static is sent.194- // In production, openChirpSyncSession wraps this in a connection timeout.195- //196- // Implementation note: we can't easily test a pure timeout here without197- // fake timers and AbortController. This test instead verifies that the198- // initiator does NOT emit msg3 before it hears msg2 — i.e., the handshake199- // is sequential and the initiator never speculatively leaks its static.200- it("T-010c: initiator sends only msg1 before receiving msg2 (no speculative msg3)", async () => {201- const initiatorKp = generateKeyPair();202- const expectedHex = pubkeyToHex(generateKeyPair().publicKey); // any valid hex203-204- const sentMessages: Uint8Array[] = [];205- let receiveCallCount = 0;206-207- // A send spy that records messages208- const mockSend = async (msg: Uint8Array) => {209- sentMessages.push(msg.slice());210- };211-212- // A receive that hangs forever (simulates no peer response)213- const mockReceive = (): Promise<Uint8Array> => {214- receiveCallCount++;215- return new Promise(() => { /* never resolves */ });216- };217-218- // Start the initiator — it will send msg1 and then wait for msg2.219- const initiatorPromise = noiseXXInitiator(220- initiatorKp.secretKey,221- initiatorKp.publicKey,222- mockSend,223- mockReceive,224- (remoteStatic) => {225- const hex = pubkeyToHex(remoteStatic);226- if (hex !== expectedHex) throw new Error("wrong peer");227- },228- );229-230- // Give the event loop a tick to let the initiator send msg1.231- await new Promise((r) => setTimeout(r, 10));232-233- // At this point: msg1 has been sent (initiator's ephemeral), but msg2234- // has not arrived. The initiator must NOT have sent msg3 (our static).235- //236- // msg1 = 32 bytes (ephemeral public key, no AEAD)237- // msg3 = 48 + 16 bytes (encrypted static + encrypted empty payload)238- expect(sentMessages).toHaveLength(1);239- expect(sentMessages[0]).toHaveLength(32); // only msg1 (32-byte ephemeral)240-241- // Clean up: the promise hangs; we don't await it.242- initiatorPromise.catch(() => {});243- });244-});src/shared/ggwave.test.ts+0 −131
@@ -1,131 +0,0 @@1-import { describe, expect, it, beforeEach, afterEach } from "vitest";2-import { CHIRP_PAYLOAD_LENGTH } from "./sync-types";3-import {4- __setGgwaveModuleForTest,5- createChirpCodec,6- GgwaveProtocol,7- loadGgwave,8-} from "./ggwave";9-10-function fakeGgwaveModule() {11- const initParams: unknown[] = [];12- const encodeCalls: Array<{ payload: Uint8Array; protocol: number; volume: number }> = [];13- const decodeCalls: Float32Array[] = [];14- let nextInstance = 1;15- const freed: number[] = [];16-17- return {18- initParams,19- encodeCalls,20- decodeCalls,21- freed,22- mod: {23- ProtocolId: { GGWAVE_PROTOCOL_ULTRASOUND_FASTEST: 5 },24- GGWAVE_OPERATING_MODE_RX: 1,25- GGWAVE_OPERATING_MODE_TX: 2,26- GGWAVE_OPERATING_MODE_RX_AND_TX: 3,27- getDefaultParameters() {28- return {29- sampleRateInp: 48000,30- sampleRateOut: 48000,31- samplesPerFrame: 1024,32- soundMarkerThreshold: 3,33- payloadLength: -1,34- operatingMode: 0,35- };36- },37- init(p: unknown) {38- initParams.push(p);39- return nextInstance++;40- },41- free(instance: number) {42- freed.push(instance);43- },44- encode(_inst: number, data: Uint8Array | string, protocol: number, volume: number) {45- const payload = typeof data === "string" ? new TextEncoder().encode(data) : data;46- encodeCalls.push({ payload, protocol, volume });47- return new Float32Array(payload.length * 100); // synthetic waveform48- },49- decode(_inst: number, samples: Float32Array | Int16Array) {50- decodeCalls.push(samples as Float32Array);51- // half the time, "succeed"; tests inject deterministic samples52- if (samples.length === 999) return null;53- return new Uint8Array(CHIRP_PAYLOAD_LENGTH); // empty-but-correct-length54- },55- },56- };57-}58-59-describe("ggwave loader", () => {60- beforeEach(() => __setGgwaveModuleForTest(null));61- afterEach(() => __setGgwaveModuleForTest(null));62-63- it("loadGgwave returns the test-injected module", async () => {64- const fake = fakeGgwaveModule();65- __setGgwaveModuleForTest(fake.mod);66- const mod = await loadGgwave();67- expect(mod).toBe(fake.mod);68- });69-70- it("createChirpCodec configures sample rate, payload length, RX+TX mode", async () => {71- const fake = fakeGgwaveModule();72- __setGgwaveModuleForTest(fake.mod);73- await createChirpCodec({ sampleRate: 44100 });74- expect(fake.initParams).toHaveLength(1);75- const p = fake.initParams[0] as Record<string, number>;76- expect(p.sampleRateInp).toBe(44100);77- expect(p.sampleRateOut).toBe(44100);78- expect(p.payloadLength).toBe(CHIRP_PAYLOAD_LENGTH);79- expect(p.operatingMode).toBe(3); // RX_AND_TX80- });81-82- it("encode forwards the payload to ggwave with ultrasonic-fastest protocol and 10% volume by default", async () => {83- const fake = fakeGgwaveModule();84- __setGgwaveModuleForTest(fake.mod);85- const codec = await createChirpCodec({ sampleRate: 48000 });86- const payload = new Uint8Array(CHIRP_PAYLOAD_LENGTH);87- payload[0] = 0x50;88- const wave = codec.encode(payload);89- expect(wave.length).toBeGreaterThan(0);90- expect(fake.encodeCalls).toHaveLength(1);91- expect(fake.encodeCalls[0].payload).toEqual(payload);92- expect(fake.encodeCalls[0].protocol).toBe(GgwaveProtocol.ULTRASOUND_FASTEST);93- expect(fake.encodeCalls[0].volume).toBe(10);94- });95-96- it("encode rejects wrong-length payload", async () => {97- const fake = fakeGgwaveModule();98- __setGgwaveModuleForTest(fake.mod);99- const codec = await createChirpCodec({ sampleRate: 48000 });100- expect(() => codec.encode(new Uint8Array(63))).toThrow();101- });102-103- it("decode forwards samples and returns the payload bytes", async () => {104- const fake = fakeGgwaveModule();105- __setGgwaveModuleForTest(fake.mod);106- const codec = await createChirpCodec({ sampleRate: 48000 });107- const samples = new Float32Array(2048);108- const decoded = codec.decode(samples);109- expect(decoded).toBeInstanceOf(Uint8Array);110- expect(decoded!.length).toBe(CHIRP_PAYLOAD_LENGTH);111- });112-113- it("decode returns null when ggwave returns null (no chirp detected)", async () => {114- const fake = fakeGgwaveModule();115- __setGgwaveModuleForTest(fake.mod);116- const codec = await createChirpCodec({ sampleRate: 48000 });117- const samples = new Float32Array(999);118- expect(codec.decode(samples)).toBeNull();119- });120-121- it("free is idempotent and prevents further encode/decode", async () => {122- const fake = fakeGgwaveModule();123- __setGgwaveModuleForTest(fake.mod);124- const codec = await createChirpCodec({ sampleRate: 48000 });125- codec.free();126- codec.free();127- expect(fake.freed).toEqual([1]);128- expect(() => codec.encode(new Uint8Array(CHIRP_PAYLOAD_LENGTH))).toThrow();129- expect(() => codec.decode(new Float32Array(0))).toThrow();130- });131-});src/shared/ggwave.ts+0 −187
@@ -1,187 +0,0 @@1-// ggwave loader — thin async wrapper around the ggwave-wasm module.2-//3-// ggwave is loaded from a vendored copy at `/ggwave/ggwave.js` rather than4-// npm-imported. Two reasons:5-//6-// 1. The browser-extension service worker can't import arbitrary npm7-// modules at runtime — every dependency has to be bundled. ggwave-wasm8-// ships a JS+WASM pair where the JS glue is small and the WASM blob9-// is ~150KB; vendoring keeps the bundle deterministic and dodges a10-// transitive-dep audit on every release.11-//12-// 2. The same loader works in popup pages, content scripts, and on the13-// Capacitor mobile build — anywhere we can fetch a static asset.14-//15-// The loader is idempotent: repeated `loadGgwave()` calls share one16-// instance. The instance owns a per-AudioContext sample rate and must be17-// rebuilt if the AudioContext rate changes between calls (which it never18-// should during the lifetime of a pairing screen).19-20-import { CHIRP_PAYLOAD_LENGTH } from "./sync-types";21-22-// ggwave-wasm ProtocolId enum subset. We only ever use the ultrasonic23-// fastest mode — see brief §3.1 — but the others are listed so the type24-// covers the upstream API surface we forward.25-export const enum GgwaveProtocol {26- AUDIBLE_NORMAL = 0,27- AUDIBLE_FAST = 1,28- AUDIBLE_FASTEST = 2,29- ULTRASOUND_NORMAL = 3,30- ULTRASOUND_FAST = 4,31- ULTRASOUND_FASTEST = 5, // GGWAVE_PROTOCOL_MT_FASTEST equivalent — chirp default32- DT_NORMAL = 6,33- DT_FAST = 7,34- DT_FASTEST = 8,35-}36-37-interface GgwaveParameters {38- sampleRateInp: number;39- sampleRateOut: number;40- samplesPerFrame: number;41- soundMarkerThreshold: number;42- payloadLength: number;43- operatingMode: number;44-}45-46-interface GgwaveModule {47- ProtocolId: Record<string, number>;48- GGWAVE_OPERATING_MODE_RX: number;49- GGWAVE_OPERATING_MODE_TX: number;50- GGWAVE_OPERATING_MODE_RX_AND_TX: number;51- getDefaultParameters(): GgwaveParameters;52- init(p: GgwaveParameters): number; // instance handle53- free(instance: number): void;54- encode(instance: number, data: Uint8Array | string, protocol: number, volumePercent: number): Float32Array;55- decode(instance: number, samples: Float32Array | Int16Array): Uint8Array | null;56-}57-58-type GgwaveFactory = () => Promise<GgwaveModule>;59-60-declare global {61- interface Window {62- ggwave_factory?: GgwaveFactory;63- }64- // eslint-disable-next-line no-var65- var ggwave_factory: GgwaveFactory | undefined;66-}67-68-let modulePromise: Promise<GgwaveModule> | null = null;69-70-/**71- * Locate the ggwave JS glue. Picked relative to the extension URL when72- * running inside Chrome/Firefox; falls back to a relative path so the same73- * loader works in the Capacitor mobile build.74- */75-function ggwaveScriptUrl(): string {76- if (typeof chrome !== "undefined" && chrome.runtime?.getURL) {77- return chrome.runtime.getURL("ggwave/ggwave.js");78- }79- return "./ggwave/ggwave.js";80-}81-82-async function injectScript(url: string): Promise<void> {83- // Service worker / module worker: importScripts is unavailable in MV384- // workers, but `import(url)` always works for a same-origin ES module.85- // The vendored ggwave.js is wrapped as a side-effecting ES module that86- // assigns globalThis.ggwave_factory on evaluation.87- await import(/* @vite-ignore */ url);88-}89-90-export async function loadGgwave(): Promise<GgwaveModule> {91- if (modulePromise) return modulePromise;92- modulePromise = (async () => {93- if (typeof globalThis.ggwave_factory !== "function") {94- try {95- await injectScript(ggwaveScriptUrl());96- } catch (err) {97- throw new Error(98- `ggwave glue could not be loaded — vendored asset missing? ${(err as Error).message}`,99- );100- }101- }102- const factory = globalThis.ggwave_factory;103- if (typeof factory !== "function") {104- throw new Error("ggwave_factory not exposed after script load");105- }106- return factory();107- })();108- return modulePromise;109-}110-111-/**112- * High-level chirp codec bound to a specific AudioContext sample rate.113- * One instance is held by the pairing coordinator for the entire pairing114- * session and discarded on completion.115- *116- * Volume is fixed at 10% — ggwave's published guidance for ultrasonic117- * pairing on internal speakers. Raising it makes pairing more reliable in118- * noisy rooms but pumps audible artefacts at the low edge of the FSK band119- * on some laptop speakers.120- */121-export interface ChirpCodec {122- /** Encode a 64-byte chirp payload into a mono float32 waveform. */123- encode(payload: Uint8Array): Float32Array;124- /** Try to decode a single chirp out of a buffer of input samples. */125- decode(samples: Float32Array): Uint8Array | null;126- /** Free the underlying ggwave instance. Idempotent. */127- free(): void;128-}129-130-export interface ChirpCodecOptions {131- sampleRate: number;132- protocol?: GgwaveProtocol;133- volumePercent?: number;134-}135-136-export async function createChirpCodec(opts: ChirpCodecOptions): Promise<ChirpCodec> {137- const mod = await loadGgwave();138- const params = mod.getDefaultParameters();139- params.sampleRateInp = opts.sampleRate;140- params.sampleRateOut = opts.sampleRate;141- params.payloadLength = CHIRP_PAYLOAD_LENGTH;142- params.operatingMode = mod.GGWAVE_OPERATING_MODE_RX_AND_TX;143- const protocol = opts.protocol ?? GgwaveProtocol.ULTRASOUND_FASTEST;144- const volume = opts.volumePercent ?? 10;145- let instance: number | null = mod.init(params);146-147- return {148- encode(payload: Uint8Array): Float32Array {149- if (instance === null) throw new Error("chirp codec is freed");150- if (payload.length !== CHIRP_PAYLOAD_LENGTH) {151- throw new Error(`chirp payload must be ${CHIRP_PAYLOAD_LENGTH} bytes`);152- }153- const result = mod.encode(instance, payload, protocol, volume);154- if (!result || result.length === 0) {155- throw new Error("ggwave.encode returned an empty waveform");156- }157- return result;158- },159- decode(samples: Float32Array): Uint8Array | null {160- if (instance === null) throw new Error("chirp codec is freed");161- const decoded = mod.decode(instance, samples);162- if (!decoded) return null;163- // ggwave returns the exact payload bytes — caller validates the164- // PEAC magic via decodeChirpPayload.165- return new Uint8Array(decoded);166- },167- free(): void {168- if (instance !== null) {169- mod.free(instance);170- instance = null;171- }172- },173- };174-}175-176-/**177- * Test-only override for the ggwave module. The chirp pairing tests inject178- * a fake module so they can exercise the play/listen loop without the179- * vendored WASM blob.180- */181-export function __setGgwaveModuleForTest(mod: GgwaveModule | null): void {182- if (mod === null) {183- modulePromise = null;184- return;185- }186- modulePromise = Promise.resolve(mod);187-}src/shared/sync-types.ts+0 −26
@@ -110,32 +110,6 @@110110 }111111 112112 // ---------------------------------------------------------------------------113-// v3 chirp pairing — phase-1 UI state114-// ---------------------------------------------------------------------------115-116-/**117- * Phase 1 pairing state, surfaced to the chirp pairing view. The view118- * binds its bird-animation acts directly to these states: `listening` and119- * `chirping` map to Act 1, `handshake` maps to Act 2, `paired` to Act 3.120- */121-export type PairingPhase =122- | "idle"123- | "listening"124- | "chirping"125- | "handshake"126- | "paired"127- | "timeout"128- | "mic_denied"129- | "version_mismatch"130- | "handshake_failed";131-132-export interface PairingProgress {133- phase: PairingPhase;134- peerDeviceName?: string;135- errorMessage?: string;136-}137-138-// ---------------------------------------------------------------------------139113 // v3 wire protocol — phase-2 WebSocket manifest sync140114 // ---------------------------------------------------------------------------141115