diff --git a/README.md b/README.md index 1be0019..be6b2f5 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ 创建房间时可按优先级填写 **1–3 个中继**(以逗号或空格分隔)。首选不可用时会尝试备用,并每 60 秒探测首选是否恢复。页面显示当前中继和在线人数;连到不同中继的成员暂时无法互通,切换后请重新核对安全码。所有成员须使用同一份房间配置。 +**想改用自己搭的中继?** 打开房间文件,展开「更换中继」,填入自己的 `wss://` 地址即可生成同一房间的新文件(密钥、口令不变,无需口令);也可在生成器页面的「已有房间文件?改用其他中继」中转换旧文件。把新文件发给所有成员后,大家就在你的中继上聊天,并重新核对安全码。详见 [更换中继](docs/DEVELOPER.md#更换中继迁移到自建中继)。 + 多中继配置使用新版房间格式,旧文件/链接需重新生成。网页房间链接只支持站点预先允许的中继,其他组合请使用下载的房间文件。配置静态网页的多个中继见 [开发说明](docs/DEVELOPER.md#多中继房间)。 ## 常见问题 diff --git a/browser-tests/retarget.mjs b/browser-tests/retarget.mjs new file mode 100644 index 0000000..1638bb0 --- /dev/null +++ b/browser-tests/retarget.mjs @@ -0,0 +1,92 @@ +// A room made for relay A is re-targeted to relay B (in the file itself, by +// the hosted converter and by the CLI); members then chat through B only. +import { chromium } from 'playwright'; +import { spawn, execFileSync } from 'node:child_process'; +import { mkdtemp, writeFile, rm, readFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { pathToFileURL } from 'node:url'; +import assert from 'node:assert/strict'; +import { makeConfig } from '../html-room/crypto.js'; +import { render } from '../scripts/build-html-room.mjs'; +import { checkHtml } from '../scripts/check-html-room.mjs'; + +const base = 19530 + Math.floor(Math.random() * 100), ports = [base, base + 200]; +const [relayA, relayB] = ports.map(p => `ws://127.0.0.1:${p}`); +const dir = await mkdtemp(tmpdir() + '/hr-retarget-'); +const servers = []; +async function start(port) { + const child = spawn(process.execPath, ['server/server.mjs'], { env: { ...process.env, PORT: String(port), HOST: '127.0.0.1' }, stdio: 'ignore' }); + servers.push(child); + for (let i = 0; i < 40; i++) { + try { if ((await fetch(`http://127.0.0.1:${port}/healthz`)).ok) return; } catch {} + await new Promise(r => setTimeout(r, 100)); + } + throw Error('relay fixture did not start'); +} +const configOf = html => JSON.parse(html.match(/type="application\/json">(.*?)<\/script>/s)[1]); +let browser; +try { + await start(ports[0]); await start(ports[1]); + const password = 'correct horse battery staple'; + const config = await makeConfig([relayA], password), original = await render(config, [relayA]); + const oldPath = dir + '/old.html'; await writeFile(oldPath, original.html); + browser = await chromium.launch({ headless: true }); + const errors = []; + const page = await browser.newPage(); page.on('pageerror', e => errors.push(e.message)); + + // 1. In-file conversion, no password needed. + await page.goto(pathToFileURL(oldPath).href); + await page.locator('#retarget summary').click(); + assert.equal(await page.locator('#rt-relay').inputValue(), relayA); + await page.locator('#rt-relay').fill('http://evil.example/path'); await page.locator('#rt-make').click(); + await page.locator('#rt-status').filter({ hasText: '生成失败' }).waitFor(); + await page.locator('#rt-relay').fill(relayB); await page.locator('#rt-make').click(); + const download = page.waitForEvent('download'); await page.locator('#rt-download').click(); + const newHtml = await readFile(await (await download).path(), 'utf8'); + const next = configOf(newHtml); + assert.deepEqual(next, { ...config, relays: [relayB] }, 'only relays change; wrapped key, salt, roomId kept'); + assert.equal(newHtml, (await render(next, [relayB])).html, 'byte-identical to a fresh build'); + await checkHtml(newHtml, original.scriptHash); + assert.match(newHtml, new RegExp(`connect-src ${relayB};`)); + const newPath = dir + '/new.html'; await writeFile(newPath, newHtml); + + // 2. Two members with the new file chat through relay B only. + const members = await Promise.all([browser.newPage(), browser.newPage()]); + const sockets = []; + for (const m of members) { + m.on('pageerror', e => errors.push(e.message)); m.on('websocket', s => sockets.push(s.url())); + await m.goto(pathToFileURL(newPath).href); + await m.locator('#password').fill(password); await m.locator('#enter').click(); + } + for (const m of members) { + await m.locator('#relay-status').filter({ hasText: relayB }).waitFor({ timeout: 15000 }); + await m.locator('#presence').filter({ hasText: '在线 2 人' }).waitFor({ timeout: 15000 }); + await m.locator('#status').filter({ hasText: '已建立签名混合加密连接' }).waitFor({ timeout: 15000 }); + } + await members[0].locator('#text').fill('自建中继你好'); await members[0].locator('#send').click(); + await members[1].locator('#messages li').filter({ hasText: '自建中继你好' }).waitFor({ timeout: 10000 }); + assert.ok(sockets.length && sockets.every(u => u === `${relayB}/hr/${config.roomId}`), 'never touched relay A'); + + // 3. Hosted converter on relay B's own page turns the old file into the same result. + const maker = await browser.newPage(); maker.on('pageerror', e => errors.push(e.message)); + await maker.goto(`http://127.0.0.1:${ports[1]}/`); + await maker.locator('#cv summary').click(); + await maker.locator('#cv-file').setInputFiles(oldPath); + await maker.locator('#cv-relay').fill(`${relayB} ${relayA}`); await maker.locator('#cv-make').click(); + const d2 = maker.waitForEvent('download'); await maker.locator('#cv-download').click(); + const hosted = await readFile(await (await d2).path(), 'utf8'); + assert.deepEqual(configOf(hosted).relays, [relayB, relayA]); + await checkHtml(hosted, original.scriptHash); + + // 4. CLI. + const cliOut = dir + '/cli.html'; + execFileSync(process.execPath, ['scripts/build-html-room.mjs', 'retarget', '--in', oldPath, '--out', cliOut, '--relay', relayB]); + assert.equal(await readFile(cliOut, 'utf8'), newHtml); + + assert.deepEqual(errors, []); + console.log('PASS: in-file, hosted and CLI relay re-targeting; chat on the new relay with the unchanged password'); +} finally { + await browser?.close(); + for (const s of servers) s.kill('SIGTERM'); + await rm(dir, { recursive: true, force: true }); +} diff --git a/dist/hr-template.html b/dist/hr-template.html index cb02fd8..fd72ddd 100644 --- a/dist/hr-template.html +++ b/dist/hr-template.html @@ -1 +1,2 @@ -私密 HTML 聊天房间

私密聊天房间

聊天脚本尚未启动,请查看下方浏览器打开说明。

若此提示一直显示,当前文件预览未能运行聊天脚本。iOS「文件」、微信或 QQ 的预览可能无法使用此房间;输入口令不会生效。请在 Safari 等浏览器中打开房间网页链接。

    +私密 HTML 聊天房间

    私密聊天房间

    聊天脚本尚未启动,请查看下方浏览器打开说明。

    若此提示一直显示,当前文件预览未能运行聊天脚本。iOS「文件」、微信或 QQ 的预览可能无法使用此房间;输入口令不会生效。请在 Safari 等浏览器中打开房间网页链接。

      diff --git a/dist/hr.html b/dist/hr.html index 49abb3a..7051e4c 100644 --- a/dist/hr.html +++ b/dist/hr.html @@ -1 +1 @@ -HTML Room · 创建或进入房间

      创建 HTML 房间

      同一份文件进入同一聊天房间,生成过程在本浏览器内完成。

      +HTML Room · 创建或进入房间

      创建 HTML 房间

      同一份文件进入同一聊天房间,生成过程在本浏览器内完成。

      已有房间文件?改用其他中继(如自建 wss://)

      选择现有房间文件并填写新中继,生成同一房间(密钥、口令不变)的新文件。文件只在本浏览器内读取,不会上传。

      diff --git a/dist/hr.js b/dist/hr.js index 14e9408..f8c078d 100644 --- a/dist/hr.js +++ b/dist/hr.js @@ -1 +1 @@ -(()=>{var Lt=Object.defineProperty;var _t=(t,e,r)=>e in t?Lt(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var _=(t,e,r)=>_t(t,typeof e!="symbol"?e+"":e,r);var M=[];var Br=new TextEncoder;var Mt=[256,512,1024,2048,4096];function se(t){let e=Mt.find(s=>s>t.length)??t.length+1,r=new Uint8Array(e);return r.set(t),r[t.length]=128,r}function oe(t){let e=t.length-1;for(;e>=0&&t[e]===0;)e--;return e>=0&&t[e]===128?t.subarray(0,e):t}var q=new TextEncoder,D=t=>Array.from(new Uint8Array(t),e=>e.toString(16).padStart(2,"0")).join("");function P(t,e){if(typeof t!="string"||t.length!==e*2||!/^[0-9a-f]+$/.test(t))throw Error("\u914D\u7F6E\u5B57\u8282\u683C\u5F0F\u9519\u8BEF");return Uint8Array.from(t.match(/../g),r=>parseInt(r,16))}var N=t=>crypto.getRandomValues(new Uint8Array(t));async function He(t){let e=typeof t=="string"?P(t,32):t;if(!(e instanceof Uint8Array)||e.length!==32)throw Error("\u623F\u95F4\u5BC6\u94A5\u957F\u5EA6\u9519\u8BEF");let r=await crypto.subtle.importKey("raw",e,"HKDF",!1,["deriveBits"]),s=async a=>new Uint8Array(await crypto.subtle.deriveBits({name:"HKDF",hash:"SHA-256",salt:new Uint8Array(32),info:q.encode(`private-talk/html-room/v1/${a}`)},r,256)),o=await s("access"),n=await s("key"),i=await s("bind");return{access:o,keyBytes:n,bind:i,roomId:D(await crypto.subtle.digest("SHA-256",o)),key:await crypto.subtle.importKey("raw",n,"AES-GCM",!1,["encrypt","decrypt"])}}function Ae(t){let e=new URL(t);if(!/^(?:[a-z0-9.-]+|\[[0-9a-f:]+\])$/.test(e.hostname))throw Error("\u4E2D\u7EE7\u4E3B\u673A\u540D\u542B\u4E0D\u5B89\u5168\u5B57\u7B26");if(e.username||e.password||e.search||e.hash||e.pathname!=="/"||!["ws:","wss:"].includes(e.protocol))throw Error("\u4E2D\u7EE7\u5FC5\u987B\u662F\u7CBE\u786E\u7684 WS/WSS \u6E90");if(e.protocol==="ws:"&&!["localhost","127.0.0.1","[::1]"].includes(e.hostname))throw Error("\u8FDC\u7A0B\u4E2D\u7EE7\u5FC5\u987B\u4F7F\u7528 WSS");return e.origin}var xe={name:"PBKDF2",hash:"SHA-256",iterations:31e4};function ie(t){let e=Array.isArray(t)?t:typeof t=="string"?t.trim().split(/[\s,]+/):[],r=[...new Set(e.map(Ae))];if(!r.length||r.length>3)throw Error("\u8BF7\u63D0\u4F9B 1 \u5230 3 \u4E2A\u4E0D\u540C\u4E2D\u7EE7");return r}var je=t=>q.encode(`private-talk/html-room/v1/wrap|${t}`);async function Ne(t,e){if(typeof t!="string"||!t.length||q.encode(t).length>1024)throw Error("\u53E3\u4EE4\u957F\u5EA6\u65E0\u6548");let r=await crypto.subtle.importKey("raw",q.encode(t),"PBKDF2",!1,["deriveKey"]);return crypto.subtle.deriveKey({...xe,salt:e},r,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}function G(t){if(!t||t.version!==2||!/^[0-9a-f]{64}$/.test(t.roomId))throw Error("\u623F\u95F4\u7248\u672C\u6216\u5730\u5740\u9519\u8BEF");if(!Array.isArray(t.relays)||JSON.stringify(ie(t.relays))!==JSON.stringify(t.relays))throw Error("\u4E2D\u7EE7\u5217\u8868\u683C\u5F0F\u9519\u8BEF");let e=["version","relays","roomId","encrypted"];if(t.encrypted===!0){let r=[...e,"salt","iv","ciphertext","kdf"];if(Object.keys(t).some(s=>!r.includes(s))||!t.kdf||Object.keys(t.kdf).length!==3||Object.entries(xe).some(([s,o])=>t.kdf[s]!==o))throw Error("\u4E0D\u652F\u6301\u7684 KDF \u914D\u7F6E");P(t.salt,16),P(t.iv,12),P(t.ciphertext,48)}else{if(t.encrypted!==!1||Object.keys(t).some(r=>![...e,"master"].includes(r)))throw Error("\u914D\u7F6E\u5B57\u6BB5\u9519\u8BEF");P(t.master,32)}return t}async function Be(t,e){let r=ie(t),s=N(32),o=await He(s),n={version:2,relays:r,roomId:o.roomId,encrypted:!!e};if(!e)return{...n,master:D(s)};let i=N(16),a=N(12),l=await Ne(e,i),c=await crypto.subtle.encrypt({name:"AES-GCM",iv:a,additionalData:je(n.roomId)},l,s);return{...n,salt:D(i),iv:D(a),ciphertext:D(c),kdf:{...xe}}}async function Re(t,e){G(t);let r=t.encrypted?new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:P(t.iv,12),additionalData:je(t.roomId)},await Ne(e,P(t.salt,16)),P(t.ciphertext,48))):P(t.master,32),s=await He(r);if(r.fill(0),s.roomId!==t.roomId)throw Error("\u623F\u95F4\u5730\u5740\u4E0E\u5BC6\u94A5\u4E0D\u5339\u914D");return s}async function Pe(t,e){let r=N(12),s=await crypto.subtle.encrypt({name:"AES-GCM",iv:r,additionalData:q.encode(`private-talk/html-room/v1/keys|${t.roomId}`)},t.key,se(q.encode(e)));return{iv:btoa(String.fromCharCode(...r)),ct:btoa(String.fromCharCode(...new Uint8Array(s)))}}async function Fe(t,e){let r=s=>Uint8Array.from(atob(s),o=>o.charCodeAt(0));return new TextDecoder().decode(oe(new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:r(e.iv),additionalData:q.encode(`private-talk/html-room/v1/keys|${t.roomId}`)},t.key,r(e.ct)))))}var ae=BigInt(4294967295),ze=BigInt(32);function Ht(t,e=!1){return e?{h:Number(t&ae),l:Number(t>>ze&ae)}:{h:Number(t>>ze&ae)|0,l:Number(t&ae)|0}}function jt(t,e=!1){let r=t.length,s=new Uint32Array(r),o=new Uint32Array(r);for(let n=0;nt?`"${t}" `:"";function F(t,e=""){if(typeof t!="number")throw new TypeError(pe(e)+"expected number, got "+typeof t);if(!Number.isSafeInteger(t)||t<0)throw new RangeError(pe(e)+"expected integer >= 0, got "+t);return t}function Nt(t,e=""){if(typeof t!="boolean")throw new TypeError(pe(e)+"expected boolean, got type="+typeof t);return t}function R(t,e,r=""){if(Xe(t)&&(e===void 0||t.length===e))return t;e!==void 0&&F(e,"length");let s=Xe(t),o=e!==void 0?` of length ${e}`:"",n=s?`length=${t.length}`:`type=${typeof t}`,i=pe(r)+"expected Uint8Array"+o+", got "+n;throw s?new RangeError(i):new TypeError(i)}var Bt=(t,e)=>{if(t===null||typeof t!="object"||Array.isArray(t))throw new TypeError((e==="object"?"":`"${e}" `)+"expected object, got type="+typeof t)},qe=(t,e)=>{Bt(t,e);let r=Object.getPrototypeOf(t);if(r!==Object.prototype&&r!==null)throw new TypeError(`"${e}" expected plain object`);if(Object.prototype.hasOwnProperty.call(t,"__proto__"))throw new TypeError(`"${e}.__proto__" is not allowed`)};function Ge(t,e=!0){if(t.destroyed)throw new Error("hash was destroyed");if(e&&t.finished)throw new Error("digest() was already called")}function Rt(t,e){R(t,void 0,"output");let r=e.outputLen;if(!(t.length>=r))throw new RangeError('"output" expected length >= '+r)}function Ke(t){return new Uint32Array(t.buffer,t.byteOffset,Math.floor(t.byteLength/4))}function et(...t){for(let e=0;e>>8&65280|t>>>24&255}function zt(t){for(let e=0;et:zt;function tt(t,e,r="opts"){return qe(t,"defaults"),e!==void 0&&qe(e,r),Object.assign(Object.create(null),t,e)}function rt(t,e={}){if(typeof t!="function")throw new TypeError('"hashCons" expected function, got type='+typeof t);e=tt({},e,"info");let r=(o,n)=>t(n).update(o).digest(),s=t(void 0);return r.outputLen=s.outputLen,r.blockLen=s.blockLen,r.canXOF=s.canXOF,r.create=o=>t(o),Object.assign(r,e),Object.freeze(r)}function Xt(t=32){F(t,"bytesLength");let e=typeof globalThis=="object"?globalThis.crypto:null;if(typeof e?.getRandomValues!="function")throw new Error("crypto.getRandomValues must be defined");if(t>65536)throw new RangeError(`"bytesLength" expected <= 65536, got ${t}`);return e.getRandomValues(new Uint8Array(t))}var ue=t=>({oid:Uint8Array.from([6,9,96,134,72,1,101,3,4,2,t])}),qt=BigInt(0),Z=BigInt(1),Gt=BigInt(2),Jt=BigInt(7),Vt=BigInt(256),Yt=BigInt(113),nt=[],st=[],ot=[];for(let t=0,e=Z,r=1,s=0;t<24;t++){[r,s]=[s,(2*r+3*s)%5],nt.push(2*(5*s+r)),st.push((t+1)*(t+2)/2%64);let o=qt;for(let n=0;n<7;n++)e=(e<>Jt)*Yt)%Vt,e&Gt&&(o^=Z<<(Z<t<>>32-r,er=(t,e,r)=>e<>>32-r,tr=(t,e,r)=>e<>>64-r,rr=(t,e,r)=>t<>>64-r,Je=(t,e,r)=>r>32?tr(t,e,r):Zt(t,e,r),Ve=(t,e,r)=>r>32?rr(t,e,r):er(t,e,r),Y=new Uint32Array(10);function nr(t,e=24){if(!(t instanceof Uint32Array))throw new TypeError('"s" expected Uint32Array(50), got type='+typeof t);if(t.length!==50)throw new RangeError('"s" expected Uint32Array(50), got length='+t.length);if(F(e,"rounds"),e<1||e>24)throw new Error('"rounds" expected integer 1..24');for(let r=24-e;r<24;r++){for(let n=0;n<10;n++)Y[n]=t[n]^t[n+10]^t[n+20]^t[n+30]^t[n+40];for(let n=0;n<10;n+=2){let i=(n+8)%10,a=(n+2)%10,l=Y[a],c=Y[a+1],d=Je(l,c,1)^Y[i],u=Ve(l,c,1)^Y[i+1];for(let v=0;v<50;v+=10)t[n+v]^=d,t[n+v+1]^=u}let s=t[2],o=t[3];for(let n=0;n<24;n++){let i=st[n],a=Je(s,o,i),l=Ve(s,o,i),c=nt[n];s=t[c],o=t[c+1],t[c]=a,t[c+1]=l}for(let n=0;n<50;n+=10){let i=t[n],a=t[n+1],l=t[n+2],c=t[n+3];t[n]^=~t[n+2]&t[n+4],t[n+1]^=~t[n+3]&t[n+5],t[n+2]^=~t[n+4]&t[n+6],t[n+3]^=~t[n+5]&t[n+7],t[n+4]^=~t[n+6]&t[n+8],t[n+5]^=~t[n+7]&t[n+9],t[n+6]^=~t[n+8]&i,t[n+7]^=~t[n+9]&a,t[n+8]^=~i&l,t[n+9]^=~a&c}t[0]^=Wt[r],t[1]^=Qt[r]}et(Y)}var at=class ct{constructor(e,r,s,o=!1,n=24){_(this,"state");_(this,"pos",0);_(this,"posOut",0);_(this,"finished",!1);_(this,"state32");_(this,"destroyed",!1);_(this,"blockLen");_(this,"suffix");_(this,"outputLen");_(this,"canXOF");_(this,"enableXOF",!1);_(this,"rounds");if(F(e,"blockLen"),F(r,"suffix"),F(n,"rounds"),Nt(o,"enableXOF"),this.blockLen=e,this.suffix=r,this.outputLen=s,this.enableXOF=o,this.canXOF=o,this.rounds=n,F(s,"outputLen"),!(0=r?Ke(e):void 0;for(let c=0;c=r){for(let u=0,v=c/4;u=s&&this.keccak();let i=Math.min(s-this.posOut,n-o);e.set(r.subarray(this.posOut,this.posOut+i),o),this.posOut+=i,o+=i}return e}xofInto(e){if(!this.enableXOF)throw new Error("XOF is not enabled");return this.writeInto(e)}xof(e){return F(e),this.xofInto(new Uint8Array(e))}digestInto(e){if(Rt(e,this),this.finished)throw new Error("digest() was already called");this.writeInto(e.length===this.outputLen?e:e.subarray(0,this.outputLen)),this.destroy()}digest(){let e=new Uint8Array(this.outputLen);return this.digestInto(e),e}destroy(){this.destroyed=!0,et(this.state)}_cloneInto(e){let{blockLen:r,suffix:s,outputLen:o,rounds:n,enableXOF:i}=this;return e||(e=new ct(r,s,o,i,n)),e.blockLen=r,e.state32.set(this.state32),e.pos=this.pos,e.posOut=this.posOut,e.finished=this.finished,e.rounds=n,e.suffix=s,e.outputLen=o,e.enableXOF=i,e.canXOF=this.canXOF,e.destroyed=this.destroyed,e}},lt=(t,e,r,s={})=>rt(()=>new at(e,t,r),s),sr=lt(6,136,32,ue(8)),or=lt(6,72,64,ue(10)),pt=(t,e,r,s={})=>rt((o={})=>(o=tt({},o),new at(e,t,o.dkLen===void 0?r:o.dkLen,!0)),s),ir=pt(31,168,16,ue(11)),ht=pt(31,136,32,ue(12));function Se(t,e="object"){if(t===null||typeof t!="object"||Array.isArray(t))throw new TypeError(e==="object"?"expected valid options object":`"${e}" expected object, got type=${typeof t}`);return t}function ar(t,e={},r={},s="object"){Se(t,s),Se(e,"fields"),Se(r,"optFields");function o(i,a,l){let c=s==="object"?`param "${String(i)}"`:`"${s}.${String(i)}"`,d=t[i];if(!Object.prototype.hasOwnProperty.call(t,i)&&(l?d!==void 0:a!=="function"))throw new TypeError(`${c} is invalid: expected own property`);if(l&&d===void 0)return;let u=typeof d;if(u!==a||d===null)throw new TypeError(`${c} is invalid: expected ${a}, got ${u}`)}let n=(i,a)=>Object.entries(i).forEach(([l,c])=>o(l,c,a));n(e,!1),n(r,!0)}function re(t,e="n"){if(typeof t!="number")throw new TypeError(`wrong u32 integer "${e}": expected number, got type=${typeof t}`);if(!Number.isSafeInteger(t)||t<0||t>4294967295)throw new RangeError(`wrong u32 integer "${e}": expected 0..4294967295, got ${t}`);return t}function ut(t){return re(t,"x"),(t&t-1)===0&&t!==0}function dt(t,e){if(re(t),typeof e!="number")throw new TypeError('"bits" expected number, got type='+typeof e);if(!Number.isSafeInteger(e)||e<0||e>32)throw new Error(`expected integer 0 <= bits <= 32, got ${e}`);let r=0;for(let s=0;s>>=1)r=r<<1|t&1;return r>>>0}function ft(t){return re(t),31-Math.clz32(t)}function Ye(t){if(!t||typeof t!="object"||typeof t.length!="number")throw new TypeError('"values" expected array-like, got type='+typeof t);let e=t.length;if(!ut(e))throw new Error("expected positive power-of-two length, got "+e);let r=ft(e);for(let s=0;s{ar(e,{N:"number",roots:"object",dit:"boolean"},{invertButterflies:"boolean",skipStages:"number",brp:"boolean"},"coreOpts");let{N:r,roots:s,dit:o,invertButterflies:n=!1,skipStages:i=0,brp:a=!0}=e;re(r,"coreOpts.N");let l=ft(r);if(!ut(r))throw new Error("FFT: Polynomial size should be power of two");re(i,"coreOpts.skipStages");let c=l===0?0:l-1;if(i>c)throw new Error(`FFT: wrong skipStages: expected 0 <= skipStages <= ${c}`);if(s.length!==r)throw new Error(`FFT: wrong roots length: expected ${r}, got ${s.length}`);let d=o!==n;return u=>{if(u.length!==r)throw new Error("FFT: wrong Polynomial length");o&&a&&Ye(u);for(let v=0,m=1;v>1,S=r>>y;for(let E=0;E{}){if(!Array.isArray(t))throw new TypeError(`"${e}" expected array, got type=${typeof t}`);for(let s=0;stypeof o=="number"?o:o.bytesLen,s=e.reduce((o,n)=>o+r(n),0);return{bytesLen:s,encode:o=>{let n=new Uint8Array(s);for(let i=0,a=0;i{R(o,s,t);let n=[];for(let i of e){let a=r(i),l=o.subarray(0,a);n.push(typeof i=="number"?l:i.decode(l)),o=o.subarray(a)}return n}}}function Te(t,e){let r=t,s=e*r.bytesLen;return{bytesLen:s,encode:o=>{let n=cr(o,"u");if(n.length!==e)throw new RangeError(`vecCoder.encode: wrong length=${n.length}. Expected: ${e}`);let i=new Uint8Array(s);for(let a=0,l=0;a{R(o,s);let n=[];for(let i=0;i32)throw new RangeError('"bits" expected <= 32, got '+t);return t===32?4294967295:~(-1<>>0}var lr=t=>{let{newPoly:e,N:r,Q:s,F:o,ROOT_OF_UNITY:n,brvBits:i,isKyber:a}=t,l=(p,h=s)=>{let w=p%h|0;return(w>=0?w|0:h+w|0)|0},c=(p,h=s)=>{let w=l(p,h)|0;return(w>h>>1?w-h|0:w)|0};function d(){let p=e(r);for(let h=0;h{throw new Error("not implemented")},m=a?{add:(p,h)=>{let w=p+h|0;return w>=s?w-s|0:w},sub:(p,h)=>{let w=p-h|0;return w<0?w+s|0:w},mul:(p,h)=>l((p|0)*(h|0))|0,inv:v}:{add:(p,h)=>l((p|0)+(h|0))|0,sub:(p,h)=>l((p|0)-(h|0))|0,mul:(p,h)=>l((p|0)*(h|0))|0,inv:v},y={N:r,roots:u,invertButterflies:!0,skipStages:a?1:0,brp:!1},f=We(m,{dit:!1,...y}),T=We(m,{dit:!0,...y}),S={encode:p=>f(p),decode:p=>{T(p);for(let h=0;hS.encode(p),decode:p=>S.decode(p)},bitsCoder:(p,h)=>{for(let g=0,b=0;g32&&Ze(b),b%=8;let w=Ze(p),A=p*(r/8);return{bytesLen:A,encode:g=>{let b=g,x=new Uint8Array(A);for(let $=0,k=0,U=0,j=0;$=8;U-=8,k>>=8)x[j++]=k&255;return x},decode:g=>{let b=e(r);for(let x=0,$=0,k=0,U=0;x=p;k-=p,$>>=p)b[U++]=h.decode($&w);return b}}}}},pr=t=>(e,r)=>{r||(r=t.blockLen);let s=new Uint8Array(e.length+2);s.set(e);let o=e.length,n=new Uint8Array(r),i=t.create({}),a=0,l=0;return{stats:()=>({calls:a,xofs:l}),get:(c,d)=>(s[o+0]=c,s[o+1]=d,i.destroy(),i=t.create({}).update(s),a++,()=>(l++,i.xofInto(n))),clean:()=>{i.destroy(),O(n,s)}}},hr=pr(ir);var I=256,C=3329,ur=3303,dr=17,L=lr({N:I,Q:C,F:ur,ROOT_OF_UNITY:dr,newPoly:t=>new Uint16Array(t),brvBits:7,isKyber:!0}),fr=Object.freeze({512:Object.freeze({N:I,Q:C,K:2,ETA1:3,ETA2:2,du:10,dv:4,RBGstrength:128}),768:Object.freeze({N:I,Q:C,K:3,ETA1:2,ETA2:2,du:10,dv:4,RBGstrength:192}),1024:Object.freeze({N:I,Q:C,K:4,ETA1:2,ETA2:2,du:11,dv:5,RBGstrength:256})}),yr=t=>{if(t>=12)return{encode:r=>r,decode:r=>r>=C?r-C:r};let e=2**(t-1);return{encode:r=>((r<r*C+e>>>t}},wr=t=>L.bitsCoder(t,t===12?{encode:e=>e,decode:e=>e>=C?e-C:e}:{encode:e=>e,decode:e=>e}),ee=t=>t===12?wr(12):L.bitsCoder(t,yr(t));function J(t,e){let r=t,s=e;for(let o=0;o=C?n-C:n}}function gr(t,e){let r=t,s=e;for(let o=0;o>1)];o&1&&(n=-n);let{c0:i,c1:a}=mr(r[2*o+0],r[2*o+1],s[2*o+0],s[2*o+1],n);r[2*o+0]=i,r[2*o+1]=a}return r}function $e(t){let e=t,r=new Uint16Array(I);for(let s=0;s>0|o[n+1]<<8)&4095,a=(o[n+1]>>4|o[n+2]<<4)&4095;i{let r=new Uint16Array(I),s=Ke(t);he(s);let o=0;for(let n=0,i=0,a=0,l=0;n>=1,o+=1,o===e?(l=a,a=0):o===2*e&&(r[i++]=L.mod(l-a),a=0,o=0)}if(he(s),o)throw new Error(`sampleCBD: leftover bits: ${o}`);return r};function te(t,e,r,s){return br(t(s*I/4,e,r),s)}var kr=t=>{let e=t,{K:r,PRF:s,XOF:o,HASH512:n,ETA1:i,ETA2:a,du:l,dv:c}=e,d=ee(1),u=ee(c),v=ee(l),m=le("publicKey",Te(ee(12),r),32),y=Te(ee(12),r),f=le("ciphertext",Te(v,r),u),T=le("seed",32,32),S=(E,p,h,w)=>{let A=[];for(let k=0;k{B(E,32,"seed");let p=new Uint8Array(33);p.set(E),p[32]=r;let h=n(p),[w,A]=T.decode(h),g=[],b=[];for(let k=0;k{let[w,A]=m.decode(E),g=o(A),b=S(w,(x,$)=>$e(g.get(x,$)),p,h);return g.clean(),b},prepare:E=>{let[p,h]=m.decode(E),w=o(h),A=[];for(let g=0;gS(p.map(x=>x.slice()),(x,$)=>A[x*r+$].slice(),g,b),clean:()=>O(p,A)}},decrypt:(E,p)=>{let[h,w]=f.decode(E),A=y.decode(p),g=new Uint16Array(I);for(let x=0;x{let f=m.subarray(0,384*e.K),T=i.encode(i.decode(Qe(f))),S=W(T,f);if(O(T),!S)throw new Error(`ML-KEM.${y}: wrong publicKey modulus`)},v=Object.freeze({...a,seed:64,msg:c,msgRand:c,secretKey:l.bytesLen});return Object.freeze({info:Object.freeze({type:"ml-kem"}),lengths:v,keygen:m=>{let y=m===void 0,f=y?Ee(d):m,T,S;try{B(f,d,"seed");let E=r.keygen(f.subarray(0,32)),p=E.publicKey;T=E.secretKey,S=s(p);let h=l.encode([T,p,S,f.subarray(32)]);return{publicKey:p,secretKey:h}}finally{T!==void 0&&O(T),S!==void 0&&O(S),y&&O(f)}},getPublicKey:m=>{let[y,f,T,S]=l.decode(m);return Uint8Array.from(f)},encapsulate:(m,y)=>{let f=y===void 0,T=f?Ee(c):y,S;try{return B(m,a.publicKey,"publicKey"),B(T,c,"message"),u(m,"encapsulate"),S=o.create().update(T).update(s(m)).digest(),{cipherText:r.encrypt(m,T,S.subarray(32,64)),sharedSecret:S.subarray(0,32)}}finally{S!==void 0&&O(S.subarray(32)),f&&O(T)}},decapsulate:(m,y)=>{B(y,l.bytesLen,"secretKey"),B(m,a.cipherText,"cipherText");let f=l.bytesLen-96,T=f+32,S=s(y.subarray(f/2,T));if(!W(S,y.subarray(T,T+32)))throw new Error("invalid secretKey: hash check failed");let[E,p,h,w]=l.decode(y),A=r.decrypt(m,E),g=o.create().update(A).update(h).digest(),b=g.subarray(0,32),x=r.encrypt(p,A,g.subarray(32,64)),$=W(m,x),k=n.create({dkLen:32}).update(w).update(m).digest();return O(A,x,g.subarray(32),$?k:b),$?b:k},prepare:m=>{B(m,a.publicKey,"publicKey"),u(m,"prepare");let y=Qe(m),f=s(y),T=r.prepare(y);return Object.freeze({publicKey:y,encapsulate:S=>{let E=S===void 0,p=E?Ee(c):S,h;try{return B(p,c,"message"),h=o.create().update(p).update(f).digest(),{cipherText:T.encrypt(p,h.subarray(32,64)),sharedSecret:h.subarray(0,32)}}finally{h!==void 0&&O(h.subarray(32)),E&&O(p)}},decapsulate:(S,E)=>{B(E,l.bytesLen,"secretKey"),B(S,a.cipherText,"cipherText");let[p,h,w,A]=l.decode(E);if(!W(h,y)||!W(w,f))throw new Error("ML-KEM.decapsulate: secretKey does not match prepared publicKey");let g=r.decrypt(S,p),b=o.create().update(g).update(f).digest(),x=b.subarray(0,32),$=T.encrypt(g,b.subarray(32,64)),k=W(S,$),U=n.create({dkLen:32}).update(A).update(S).digest();return O(g,$,b.subarray(32),k?U:x),k?x:U},clean:T.clean})}})}function Ar(t,e,r){return ht.create({dkLen:t}).update(e).update(new Uint8Array([r])).digest()}var xr={HASH256:sr,HASH512:or,KDF:ht,XOF:hr,PRF:Ar},Sr=t=>vr({...xr,...t}),de=Sr(fr[768]);var H=new TextEncoder,Oe=new TextDecoder,xt={name:"ECDH",namedCurve:"P-256"},yt={name:"ECDSA",namedCurve:"P-256"},fe={name:"ECDSA",hash:"SHA-256"},Er=2e3,Tr=100,$r=600*1e3,wt=60*1e3,Kr=20*1e3,Or=4,Cr=4e3,gt=1580,Ir=1452,Ur=/^(\d{1,9})\.(\d{1,9})$/,ye=/^[A-Za-z0-9+/]+={0,2}$/,Lr=/,"s":"([A-Za-z0-9+/]{86}==)"\}$/,X=t=>btoa(String.fromCharCode(...new Uint8Array(t))),z=t=>Uint8Array.from(atob(t),e=>e.charCodeAt(0)),mt=t=>Array.from(new Uint8Array(t),e=>e.toString(16).padStart(2,"0")).join("");async function bt(t,e){let r=await crypto.subtle.importKey("raw",t,{name:"HMAC",hash:"SHA-256"},!1,["sign"]);return new Uint8Array(await crypto.subtle.sign("HMAC",r,new Uint8Array([e])))}async function Ce(t){return{mk:await bt(t,1),next:await bt(t,2)}}async function kt(t,e){return crypto.subtle.importKey("raw",t,"AES-GCM",!1,e)}async function we(t,e,r){let s=crypto.getRandomValues(new Uint8Array(12)),o=await crypto.subtle.encrypt({name:"AES-GCM",iv:s,additionalData:H.encode(r)},t,e);return{iv:X(s),ct:X(o)}}async function Ie(t,{iv:e,ct:r},s){return new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:z(e),additionalData:H.encode(s)},t,z(r)))}var _r=(t,e)=>{let r=new Uint8Array(t.length+e.length);return r.set(t),r.set(e,t.length),r};async function vt(){let t=await crypto.subtle.generateKey(xt,!1,["deriveBits"]);return{pair:t,epk:X(await crypto.subtle.exportKey("raw",t.publicKey))}}var At=t=>`pt31a|${t.from}|${t.id}|${t.t}|${t.epk}|${t.hello?1:0}|${JSON.stringify(t.w)}`,ge=class t{static async create(e,r,s){let o=new t;return o.key=e,o.bind=r,o.me=s,o.signing=await crypto.subtle.generateKey(yt,!1,["sign","verify"]),o.spk=X(await crypto.subtle.exportKey("raw",o.signing.publicKey)),o.kem=de.keygen(),o.kpk=X(o.kem.publicKey),o.ecdh=await vt(),o.ecdhSince=Date.now(),o.oldEcdh=[],o.peers=new Map,o.changed=[],await o.rekey(0),o}get epk(){return this.ecdh.epk}async rekey(e=this.mine.epoch+1,r=Date.now()){this.mine&&r-this.ecdhSince>=Kr&&(this.oldEcdh=[{ecdh:this.ecdh,until:r+wt},...this.oldEcdh].slice(0,Or),this.ecdh=await vt(),this.ecdhSince=r),this.oldEcdh=this.oldEcdh.filter(s=>s.until>r),this.mine={epoch:e,ck:crypto.getRandomValues(new Uint8Array(32)),c:0,since:r,handed:new Map};for(let s of this.peers.values())s.sent=-1,s.gotAt=null}due(e=Date.now()){return this.mine.c>=Tr||e-this.mine.since>=$r}forget(e){this.peers.delete(e)}async pairKey(e,r,s=!1){let o=s?`${e.epk}|${r.kcIn??r.kc}`:e.epk;if(r.wraps.has(o))return r.wraps.get(o);let n=await crypto.subtle.importKey("raw",z(r.epk),xt,!1,[]),i=new Uint8Array(await crypto.subtle.deriveBits({name:"ECDH",public:n},e.pair.privateKey,256)),a=await crypto.subtle.importKey("raw",s?_r(i,r.ss):i,"HKDF",!1,["deriveKey"]),l=H.encode(`private-talk/${s?"v3.2":"v3"}/pair|${[e.epk,r.epk].sort().join("|")}`),c=await crypto.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:this.bind,info:l},a,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"]);return r.wraps.set(o,c),c}pqFor(e,r){if(r.v!==32||this.mer!==this.me&&this.peers.has(r)&&this.peers.get(r).sent!==this.mine.epoch)}async announce(e,r,s=Date.now()){return(await this.announceAll(e,r,s,1/0))[0]}async announceAll(e,r,s=Date.now(),o=Cr){let n=this.mine;for(let[y,f]of n.handed)s-f.at>wt&&n.handed.delete(y);let i=[...this.peers].some(([y,f])=>y{let y=a&&u?{q:1,k:this.kpk,...c}:{q:1,...c},f={kind:"keys",v:32,from:this.me,id:mt(crypto.getRandomValues(new Uint8Array(8))),t:s,epk:this.epk,spk:this.spk,hello:!!r&&u,w:y};f.sig=X(await crypto.subtle.sign(fe,this.signing.privateKey,H.encode(At(f)))),l.push(JSON.stringify(f)),c={},d=0,u=!1},m=()=>430+(a&&u?gt+8:0);for(let y of e){let f=this.peers.get(y);if(!f)continue;let{pq:T,can:S}=this.pqFor(y,f);if(!S)continue;let E=f.gotAt!=null&&n.handed.has(f.gotAt),p=E?f.gotAt:n.c,h=E?n.handed.get(p).ck:n.ck;E||(f.gotAt=p,n.handed.has(p)||n.handed.set(p,{ck:h,at:s}));let w=H.encode(JSON.stringify({e:n.epoch,ck:X(h),c:p})),A=T?{...await we(await this.pairKey(this.ecdh,f,!0),w,`pt32w|${this.me}|${y}|${f.epk}`),q:1,...f.kc&&{kc:f.kc}}:await we(await this.pairKey(this.ecdh,f),w,`pt3w|${this.me}|${y}|${f.epk}`),g=JSON.stringify(A).length+8;Object.keys(c).length&&m()+d+g>o&&await v(),c[y]=A,d+=g,f.sent=this.mine.epoch}return(u||Object.keys(c).length)&&await v(),l}async receive(e,r){if(!e||e.kind!=="keys"||e.from!==r||r===this.me||typeof e.epk!="string"||!ye.test(e.epk)||e.epk.length>120||!e.w||typeof e.w!="object")return null;let s=typeof e.spk=="string",o=null;if(s){if(!ye.test(e.spk)||e.spk.length>120||typeof e.sig!="string")throw new Error("bad announcement");let a=this.peers.get(r);if(o=a?.spk===e.spk?a.verify:await crypto.subtle.importKey("raw",z(e.spk),yt,!1,["verify"]),!await crypto.subtle.verify(fe,o,z(e.sig),H.encode(At(e))))throw new Error("bad signature")}let n=this.peers.get(r);n&&(n.spk??null)!==(s?e.spk:null)&&(n.spk&&this.changed.push(r),n=null),n?n.epk!==e.epk&&(s?(n.epk=e.epk,n.wraps=new Map):(n={epk:e.epk,spk:null,verify:null,wraps:new Map,recv:null,prev:null,sent:-1,gotAt:null,v:30},this.peers.set(r,n))):(n={epk:e.epk,spk:s?e.spk:null,verify:o,wraps:new Map,recv:null,prev:null,sent:-1,gotAt:null,v:s?31:30,kpk:null,ss:null,kc:null,kcIn:null,pqIn:!1},this.peers.set(r,n)),s&&e.w.q===1&&n.v!==32&&(n.v=32,n.sent=-1),s&&typeof e.w.k=="string"&&e.w.k.length===gt&&ye.test(e.w.k)&&e.w.k!==n.kpk&&(Object.assign(n,{kpk:e.w.k,ss:null,kc:null,kcIn:null,v:32}),n.wraps=new Map);let i=e.w[this.me];if(i&&typeof i.iv=="string"&&typeof i.ct=="string"){let a=i.q===1;a&&s&&(n.v=32),a&&ru.ecdh)],c=null;if(!a||n.ss)for(let u of l)try{c=JSON.parse(Oe.decode(await Ie(await this.pairKey(u,n,a),i,`${a?"pt32w":"pt3w"}|${r}|${this.me}|${u.epk}`)));break}catch{}c&&a&&this.me=0&&d.length===32&&(!n.recv||c.e>n.recv.epoch)?(n.prev=n.recv,n.recv={epoch:c.e,ck:d,c:c.c},n.pqIn=a):c&&a&&n.recv&&c.e===n.recv.epoch&&(n.pqIn=!0)}return e.hello&&(n.sent=-1),n}takeChanged(){let e=this.changed;return this.changed=[],e}ready(e,r=!1){return e.every(s=>s===this.me||this.peers.get(s)?.sent===this.mine.epoch&&(!r||this.peers.get(s).v>=31))}quantumSafe(e){return e.every(r=>r===this.me||this.peers.get(r)?.v===32&&this.peers.get(r).ss&&this.peers.get(r).pqIn)}older(e){return e.filter(r=>r!==this.me&&this.peers.get(r)?.v===30)}async pairSecret(e,r){let s=this.peers.get(e);if(!s?.ss)throw new Error("no pair secret");let o=await crypto.subtle.importKey("raw",s.ss,"HKDF",!1,["deriveKey"]),n=H.encode(`private-talk/v3.2/for|${r}|${Math.min(e,this.me)}|${Math.max(e,this.me)}`);return crypto.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:this.bind,info:n},o,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}async sealFor(e,r,s){return we(await this.pairSecret(e,s),H.encode(r),`pt32f|${this.me}|${e}|${s}`)}async openFrom(e,r,s){return Oe.decode(await Ie(await this.pairSecret(e,s),r,`pt32f|${e}|${this.me}|${s}`))}async seal(e){let r=this.mine,s=`${r.epoch}.${r.c}`,{mk:o,next:n}=await Ce(r.ck);r.ck=n,r.c++;let i=X(await crypto.subtle.sign(fe,this.signing.privateKey,H.encode(`pt31m|${this.me}|${s}|${e}`))),a=`${e.slice(0,-1)},"s":"${i}"}`;return{...await we(await kt(o,["encrypt"]),se(H.encode(a)),`pt3|${this.me}|${s}`),h:s}}async open(e,r){let s=Ur.exec(e.h??""),o=this.peers.get(r);if(!s||!o?.recv)throw new Error("no key");let n=Number(s[1]),i=Number(s[2]),a=n===o.recv.epoch?"recv":n===o.prev?.epoch?"prev":null,l=o[a];if(!a||iEr)throw new Error("out of step");let c=l.ck;for(let f=l.c;f=31){if(!y)throw new Error("unsigned");if(v=`${v.slice(0,y.index)}}`,!await crypto.subtle.verify(fe,o.verify,z(y[1]),H.encode(`pt31m|${r}|${e.h}|${v}`)))throw new Error("bad signature");m=!0}return o[a]={epoch:n,ck:u,c:i+1},{text:v,missed:i-l.c,signed:m}}async safetyCode(e){let r=i=>i===this.me?this.spk:this.peers.get(i)?.spk??this.peers.get(i)?.epk??"?",s=e.map(i=>`${i}:${r(i)}`).sort().join("|"),o=new Uint8Array(await crypto.subtle.digest("SHA-256",H.encode(`private-talk/v3.1/safety|${mt(this.bind)}|${s}`))),n=0n;for(let i of o.subarray(0,12))n=n<<8n|BigInt(i);return(n%10n**20n).toString().padStart(20,"0").replace(/(\d{5})(?=\d)/g,"$1 ")}};function be(t){let e=t.trim();return!e||Array.from(e).length>500||new TextEncoder().encode(e).byteLength>2048?null:e}var me=600*1e3,St=/^[0-9a-f]{16}$/;function Et(t,e,r=Date.now(),s,o,n){let i=Array.from(crypto.getRandomValues(new Uint8Array(8)),l=>l.toString(16).padStart(2,"0")).join(""),a=Number.isInteger(n)&&n>=0&&n<=5;return JSON.stringify({from:t,id:i,t:r,text:e,...s&&{g:s},...o&&{f:o},...a&&{q:n}})}function Tt(t,e,r,s=Date.now()){let o;try{o=JSON.parse(t)}catch{return null}if(!o||typeof o!="object"||typeof o.text!="string"||typeof o.id!="string"||!St.test(o.id)||!Number.isInteger(o.from)||o.from<1||o.from>25||o.from!==e.from||!Number.isFinite(o.t)||!Number.isFinite(e.at)||Math.abs(s-o.t)>me||Math.abs(e.at-o.t)>me)return null;let n=Number.isInteger(o.q)&&o.q>=0&&o.q<=5;if(n){if(o.text!==""&&be(o.text)!==o.text)return null}else if(be(o.text)!==o.text)return null;for(let[c,d]of r)Math.abs(s-d)>2*me&&r.delete(c);if(r.has(o.id))return null;r.set(o.id,o.t);let i=o.g&&typeof o.g=="object"&&!Array.isArray(o.g)?o.g:void 0,a=o.f&&typeof o.f=="object"&&!Array.isArray(o.f)?o.f:void 0,l=n?o.q:void 0;return{text:o.text,id:o.id,g:i,f:a,q:l}}function Ue(t,e,r,s=Date.now(),o="keys"){let n;try{n=JSON.parse(t)}catch{return null}return!n||typeof n!="object"||n.kind!==o||typeof n.id!="string"||!St.test(n.id)||!Number.isInteger(n.from)||n.from!==e.from||!Number.isFinite(n.t)||Math.abs(s-n.t)>me||r.has(n.id)?null:(r.set(n.id,n.t),n)}var Dr=()=>new Map,ke=class{constructor(e,r,{storage:s=Dr(),emit:o=()=>{}}={}){Object.assign(this,{d:e,config:r,store:s,emit:o,deviceId:D(N(16)),present:[],pins:new Map,seen:new Map,own:new Map,queued:[],receipts:[],received:new Map,group:null,ws:null,serial:Promise.resolve()}),this.store.set("deviceId",this.deviceId)}run(e){return this.serial=this.serial.then(()=>{if(!this.stopped)return e()}).catch(()=>this.emit("notice","\u6536\u5230\u65E0\u6CD5\u9A8C\u8BC1\u7684\u6570\u636E\uFF0C\u5DF2\u5FFD\u7565\u3002")),this.serial}send(e){!this.stopped&&this.ws?.readyState===1&&this.ws.send(JSON.stringify(e))}async announce(e=!1){if(!this.group)return;let r=this.group.owed(this.present);if(!(!e&&!r.length))for(let s of await this.group.announceAll(r,e))this.send({type:"key",...await Pe(this.d,s)})}ready(){return!this.stopped&&this.group&&this.present.length>1&&this.group.ready(this.present,!0)&&this.group.quantumSafe(this.present)}async incoming(e){if(this.stopped)return;if(e.type==="pong"){this.lastPong=Date.now();return}if(e.type==="wait"){this.nextSend=Date.now()+Math.min(3e4,Math.max(1e3,e.ms||6e3));return}if(e.type==="hello"){this.resume=e.resume,(!this.group||this.group.me!==e.you)&&(this.group=await ge.create(this.d.key,this.d.bind,e.you),this.own.clear(),this.received.clear(),this.receipts=[],this.present=[]),await this.announce(!0);return}if(!this.group)return;if(e.type==="members"){if(!Array.isArray(e.members)||e.members.some(i=>!Number.isInteger(i)||i<1||i>25))return;let n=this.present.filter(i=>!e.members.includes(i));for(let i of n){this.group.forget(i);for(let a of this.received.keys())a.startsWith(`${i}|`)&&this.received.delete(a)}this.present=e.members,n.length&&await this.group.rekey(),await this.announce(!0),this.emit("presence",this.present.length),await this.update();return}if(e.from===this.group.me){let n=this.own.get(e.h);e.type==="msg"&&n&&n.frame.iv===e.iv&&n.frame.ct===e.ct&&(n.echo=!0,this.emit("delivery",{id:n.id,text:"\u5DF2\u63D0\u4EA4\u4E2D\u7EE7"}));return}if(!this.present.includes(e.from))return;if(e.type==="key"){let n=Ue(await Fe(this.d,e),e,this.seen);if(!n||n.v!==32||!n.spk)return;let i=this.pins.get(e.from),a=await this.group.receive(n,e.from);if(!a)return;if(i&&i!==a.spk){this.halt(),this.emit("fatal","\u6210\u5458\u7B7E\u540D\u8EAB\u4EFD\u53D1\u751F\u53D8\u5316\uFF0C\u8BF7\u901A\u8FC7\u72EC\u7ACB\u6E20\u9053\u6838\u5B9E\u5E76\u91CD\u65B0\u6253\u5F00\u6587\u4EF6\u3002");return}this.pins.set(e.from,a.spk),a?.lost&&(a.sent=-1),await this.announce(),await this.update();return}if(!["msg","seen"].includes(e.type)||!e.h)return;let r=`${e.from}|${e.h}`,s=this.received.get(r);if(s){e.type==="msg"&&s.iv===e.iv&&s.ct===e.ct&&this.receipts.length<100&&this.receipts.push([e.from,e.h]);return}let o=await this.group.open(e,e.from);if(!(!o.signed||this.group.peers.get(e.from)?.v!==32))if(e.type==="seen"){let n=Ue(o.text,e,this.seen,Date.now(),"seen");if(n&&Array.isArray(n.l)&&n.l.length<=25)for(let i of n.l){if(!Array.isArray(i)||i[0]!==this.group.me)continue;let a=this.own.get(i[1]);a&&a.targets.includes(e.from)&&(a.confirmed.add(e.from),this.emit("delivery",{id:a.id,text:`\u5BA2\u6237\u7AEF\u786E\u8BA4 ${a.confirmed.size}/${a.targets.length}`}))}}else{let n=Tt(o.text,e,this.seen);if(!n||n.g||n.f)return;for(this.received.set(r,{iv:e.iv,ct:e.ct});this.received.size>200;)this.received.delete(this.received.keys().next().value);this.emit("message",{id:n.id,from:e.from,text:n.text}),this.receipts.length<100&&this.receipts.push([e.from,e.h])}}async update(){this.ready()&&this.emit("ready",await this.group.safetyCode(this.present)),await this.flush()}queue(e){if(this.stopped||(e=be(e),!e||this.queued.length>=20))return!1;let r=D(N(8));return this.queued.push({id:r,text:e}),this.emit("message",{id:r,from:"\u6211",text:e}),this.emit("delivery",{id:r,text:"\u6392\u961F\u4E2D"}),this.run(()=>this.flush()),!0}async flush(){if(!(!this.ready()||this.ws?.readyState!==1)&&(this.group.due()&&(await this.group.rekey(),await this.announce()),!!this.ready())){if(this.receipts.length){let e=JSON.stringify({kind:"seen",id:D(N(8)),from:this.group.me,t:Date.now(),l:this.receipts.splice(0,25)});this.send({type:"seen",...await this.group.seal(e)})}if(this.queued.length&&Date.now()>=(this.nextSend??0)){let e=this.queued.shift(),r={type:"msg",...await this.group.seal(Et(this.group.me,e.text))};for(this.own.set(r.h,{...e,frame:r,targets:this.present.filter(s=>s!==this.group.me),confirmed:new Set,at:Date.now()});this.own.size>100;)this.own.delete(this.own.keys().next().value);this.send(r),this.nextSend=Date.now()+6100}for(let[e,r]of this.seen)Date.now()-r>12e5&&this.seen.delete(e);for(;this.seen.size>4096;)this.seen.delete(this.seen.keys().next().value)}}async retryPending(){for(let e of this.own.values())!e.echo&&Date.now()-e.at>8e3&&(e.retries??0)<3&&(this.send(e.frame),e.at=Date.now(),e.retries=(e.retries??0)+1),!e.echo&&(e.retries??0)>=3&&this.emit("delivery",{id:e.id,text:"\u4E2D\u7EE7\u63D0\u4EA4\u672A\u786E\u8BA4\uFF0C\u8BF7\u6838\u5B9E\u540E\u91CD\u53D1"})}resetRelay(){for(let e of this.own.values())(e.confirmed.sizenew WebSocket(d,u)}){Object.assign(this,{relays:e,roomId:r,protocols:s,onactive:o,onmessage:n,onnotice:i,onfatal:a,ondisconnect:l,createSocket:c}),this.backoff=1e3,this.lastIndex=-1}start(){this.connect(0),this.probeTimer=setInterval(()=>{!this.stopped&&this.active?.ready&&this.active.index>0&&!this.probe&&this.connect(0,!0)},6e4),this.heartbeat=setInterval(()=>{let e=this.active;if(e?.ready){if(Date.now()-e.lastPong>65e3){this.fail(e);return}if(Date.now()-e.lastPing>=25e3){e.lastPing=Date.now();try{e.ws.send(JSON.stringify({type:"ping"}))}catch{this.fail(e)}}}},1e3)}connect(e,r=!1){if(this.stopped)return;let s={index:e,probe:r,frames:[],ready:!1,retired:!1};r?this.probe=s:this.pending=s;try{s.ws=this.createSocket(`${this.relays[e]}/hr/${this.roomId}`,this.protocols(e))}catch{this.fail(s);return}s.timeout=setTimeout(()=>this.fail(s),1e4),s.ws.onerror=()=>{!s.retired&&!s.probe&&this.onnotice("WSS \u63E1\u624B\u5931\u8D25\u53EF\u80FD\u662F\u7F51\u7EDC\u3001Origin\u3001\u9274\u6743\u6216\u8BC1\u4E66\u4FE1\u4EFB\u95EE\u9898\u3002\u6B63\u5728\u6309\u4F18\u5148\u7EA7\u91CD\u8BD5\uFF1B\u8BF7\u4F7F\u7528\u6709\u6548\u8BC1\u4E66\uFF0C\u52FF\u5173\u95ED\u8BC1\u4E66\u6821\u9A8C\u3002")},s.ws.onclose=o=>this.fail(s,o.code),s.ws.onmessage=o=>{if(s.retired||this.stopped||typeof o.data!="string"||o.data.length>7e3)return;let n;try{n=JSON.parse(o.data)}catch{return}if(!(!n||typeof n!="object")){if(s.ready){if(this.active!==s)return;n.type==="pong"&&(s.lastPong=Date.now()),this.onmessage(n);return}if(s.frames.length>=50){this.fail(s);return}s.frames.push(n),n.type==="hello"&&!s.activating&&Number.isInteger(n.you)&&n.you>=1&&n.you<=25&&typeof n.resume=="string"&&/^[0-9a-f]{32}$/.test(n.resume)&&(s.activating=!0,this.activate(s).catch(()=>this.fail(s)))}}}async activate(e){let r=this.active,s=this.lastIndex!==-1&&this.lastIndex!==e.index;if(this.active=e,this.probe===e&&(this.probe=null),this.pending===e&&(this.pending=null),r&&r!==e&&this.retire(r),await this.onactive(e.ws,e.index,s),!(e.retired||this.stopped||this.active!==e)){clearTimeout(e.timeout),this.lastIndex=e.index,this.backoff=1e3,e.ready=!0,e.lastPong=e.lastPing=Date.now();for(let o of e.frames)this.onmessage(o);e.frames=[]}}retire(e){if(!(!e||e.retired)){e.retired=!0,clearTimeout(e.timeout);try{e.ws?.close(4e3)}catch{}}}fail(e,r){if(!(e.retired||this.stopped)){if(this.retire(e),this.probe===e){this.probe=null;return}if(this.pending===e&&(this.pending=null),this.active===e&&(this.active=null,this.ondisconnect()),this.relays.length===1&&[4e3,4001,4002,4005,4006,4007,4008,1008,1009].includes(r)){this.stop(),this.onfatal();return}this.retire(this.probe),this.probe=null,clearTimeout(this.retry),this.retry=setTimeout(()=>this.connect((e.index+1)%this.relays.length),this.backoff),this.backoff=Math.min(this.backoff*2,3e4),r!==1006&&this.onnotice("\u8FDE\u63A5\u4E2D\u65AD\uFF0C\u6B63\u5728\u6309\u5217\u8868\u987A\u5E8F\u5207\u6362\u4E2D\u7EE7\uFF1B\u4E0D\u540C\u4E2D\u7EE7\u4E0A\u7684\u6210\u5458\u6682\u65F6\u65E0\u6CD5\u4E92\u901A\u3002")}}stop(){this.stopped=!0,clearTimeout(this.retry),clearInterval(this.probeTimer),clearInterval(this.heartbeat),this.retire(this.active),this.retire(this.pending),this.retire(this.probe)}};function $t(t){let e=c=>document.getElementById(c),r=c=>{e("status").textContent=c};e("preview-help").hidden=!0;let s=0,o=!1,n=0,i,a=new Map;function l(c,d){if((c==="notice"||c==="fatal")&&r(d),c==="relay-change"&&(e("safety").textContent="\u4E2D\u7EE7\u5DF2\u5207\u6362\uFF0C\u6B63\u5728\u91CD\u65B0\u9A8C\u8BC1\u6210\u5458\uFF1B\u8BF7\u91CD\u65B0\u6838\u5BF9\u5B89\u5168\u7801\u3002"),c==="presence"&&(e("presence").textContent=`\u5728\u7EBF ${d} \u4EBA\uFF08\u4E2D\u7EE7\u62A5\u544A\uFF09`),c==="ready"&&(r("\u5DF2\u5EFA\u7ACB\u7B7E\u540D\u6DF7\u5408\u52A0\u5BC6\u8FDE\u63A5"),e("safety").textContent=`\u5B89\u5168\u7801\uFF1A${d}\uFF0C\u8BF7\u901A\u8FC7\u72EC\u7ACB\u6E20\u9053\u6838\u5BF9\u3002`),c==="message"){let u=document.createElement("li");u.textContent=`${d.from}\uFF1A${d.text}`;let v=document.createElement("small");u.append(v),e("messages").append(u),a.set(d.id,v),a.size>200&&(a.delete(a.keys().next().value),e("messages").firstChild.remove())}c==="delivery"&&a.has(d.id)&&(a.get(d.id).textContent=d.text)}if(!globalThis.crypto?.subtle||!globalThis.WebSocket||!globalThis.TextEncoder)r("\u6B64\u9884\u89C8\u73AF\u5883\u4E0D\u652F\u6301\u6240\u9700\u811A\u672C\u6216\u52A0\u5BC6 API\uFF0C\u8BF7\u4F7F\u7528\u652F\u6301\u7684\u6D4F\u89C8\u5668\u6253\u5F00\uFF1BiOS\u3001\u5FAE\u4FE1\u548C QQ \u672C\u5730\u9884\u89C8\u9700\u5B9E\u6D4B\u3002"),e("enter").disabled=!0;else if(t.version)try{G(t),r(t.encrypted?"\u8BF7\u8F93\u5165\u53E3\u4EE4\u89E3\u9501\uFF1B\u89E3\u9501\u524D\u4E0D\u4F1A\u8FDE\u63A5\u4E2D\u7EE7\u3002":"\u65E0\u53E3\u4EE4\u6587\u4EF6\uFF1A\u70B9\u51FB\u8FDB\u5165\u623F\u95F4\u3002"),e("password").disabled=!1,e("enter").disabled=!1}catch{r("\u914D\u7F6E\u65E0\u6548\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u6587\u4EF6\u3002"),e("enter").disabled=!0}else r("\u65E0\u79D8\u5BC6\u6A21\u677F\uFF1A\u8BF7\u4F7F\u7528\u751F\u6210\u5668\u521B\u5EFA\u623F\u95F4\u6587\u4EF6\u3002"),e("enter").disabled=!0;e("unlock").addEventListener("submit",async c=>{if(c.preventDefault(),!(o||Date.now()["pt2",`a${D(i.d.access)}`,...i.resume&&u.lastIndex===v?[`r${i.resume}`]:[]],onactive:(v,m,y)=>i.run(()=>{y&&i.resetRelay(),i.ws=v,e("relay-status").textContent=`\u5F53\u524D\u4E2D\u7EE7\uFF1A${t.relays[m]}\uFF08\u4F18\u5148\u7EA7 ${m+1}/${t.relays.length}\uFF09`,r(y?"\u5DF2\u5207\u6362\u4E2D\u7EE7\uFF0C\u6B63\u5728\u91CD\u65B0\u9A8C\u8BC1\u6210\u5458\uFF1B\u4E0D\u540C\u4E2D\u7EE7\u4E0A\u7684\u6210\u5458\u6682\u65F6\u65E0\u6CD5\u4E92\u901A\u3002":"\u5DF2\u8FDE\u63A5\u4E2D\u7EE7\uFF0C\u6B63\u5728\u9A8C\u8BC1\u6210\u5458\u5BC6\u94A5\u2026")}),onmessage:v=>{let m=i.ws;i.run(()=>{if(m===i.ws)return i.incoming(v)})},ondisconnect:()=>{i.ws=null,e("relay-status").textContent="\u5F53\u524D\u4E2D\u7EE7\uFF1A\u5DF2\u65AD\u5F00\uFF0C\u6B63\u5728\u6309\u4F18\u5148\u7EA7\u91CD\u8BD5",l("presence",0)},onnotice:r,onfatal:()=>{i.halt(),r("\u8FDE\u63A5\u88AB\u62D2\u7EDD\u6216\u4F1A\u8BDD\u7ED3\u675F\uFF0C\u8BF7\u68C0\u67E5\u623F\u95F4\u6587\u4EF6\u3001\u4E2D\u7EE7\u914D\u7F6E\u6216\u4EBA\u6570\u9650\u5236\u540E\u91CD\u65B0\u6253\u5F00\u3002")}});i.connection=u,u.start(),i.tick=setInterval(()=>i.run(async()=>{i.ws?.readyState===1&&(Date.now()-(i.lastAnnounce??0)>1e4&&(await i.announce(!0),i.lastAnnounce=Date.now()),await i.flush(),await i.retryPending())}),1600)}catch{s++,n=Date.now()+Math.min(3e4,1e3*2**Math.min(s,5)),e("password").value="",r("\u89E3\u9501\u5931\u8D25\uFF1A\u53E3\u4EE4\u6216\u6587\u4EF6\u4E0D\u6B63\u786E\u3002\u7A0D\u540E\u518D\u8BD5\u3002\u672C\u673A\u8282\u6D41\u4E0D\u80FD\u963B\u6B62\u79BB\u7EBF\u731C\u6D4B\u3002")}finally{o=!1,setTimeout(()=>{e("enter").disabled=!1},Math.max(0,n-Date.now()))}}}),e("chat").addEventListener("submit",c=>{c.preventDefault(),i?.queue(e("text").value)?e("text").value="":r("\u6D88\u606F\u4E3A\u7A7A\u3001\u8FC7\u957F\u6216\u961F\u5217\u5DF2\u6EE1\u3002")}),addEventListener("pagehide",()=>i?.halt())}var Mr=t=>JSON.stringify(t).replace(/[<>/\u2028\u2029]/g,e=>`\\u${e.charCodeAt(0).toString(16).padStart(4,"0")}`),Hr=t=>btoa(String.fromCharCode(...new TextEncoder().encode(t))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");function _e(t){return G(t),"#hr1="+Hr(JSON.stringify(t))}function Kt(t){let e=t.version?`${t.relays[0].replace(/^ws/,"http")}/hr.html${_e(t)}`:"";return`

      \u82E5\u6B64\u63D0\u793A\u4E00\u76F4\u663E\u793A\uFF0C\u5F53\u524D\u6587\u4EF6\u9884\u89C8\u672A\u80FD\u8FD0\u884C\u804A\u5929\u811A\u672C\u3002iOS\u300C\u6587\u4EF6\u300D\u3001\u5FAE\u4FE1\u6216 QQ \u7684\u9884\u89C8\u53EF\u80FD\u65E0\u6CD5\u4F7F\u7528\u6B64\u623F\u95F4\uFF1B\u8F93\u5165\u53E3\u4EE4\u4E0D\u4F1A\u751F\u6548\u3002\u8BF7\u5728 Safari \u7B49\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u623F\u95F4\u7F51\u9875\u94FE\u63A5\u3002

      ${e?`

      \u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u540C\u4E00\u623F\u95F4\uFF08\u82E5\u65E0\u6CD5\u8DF3\u8F6C\uFF0C\u8BF7\u590D\u5236\u94FE\u63A5\u5230 Safari \u5730\u5740\u680F\uFF09\u3002

      \u94FE\u63A5\u5305\u542B\u52A0\u5165\u623F\u95F4\u7684\u80FD\u529B\uFF0C\u8BF7\u8C28\u614E\u5206\u53D1\u3002\u7F51\u9875\u4EE3\u7801\u53EF\u8BFB\u53D6\u94FE\u63A5\u4E2D\u7684\u623F\u95F4\u914D\u7F6E\uFF0C\u5176\u5B89\u5168\u6027\u4E0D\u7B49\u540C\u4E8E\u9A8C\u8BC1\u8FC7\u7684\u79BB\u7EBF\u6587\u4EF6\uFF1B\u4E2D\u7EE7\u987B\u63D0\u4F9B /hr.html \u7F51\u9875\u5165\u53E3\u3002

      `:""}
      `}function Ot(t){if(typeof t!="string"||!/^#hr1=[A-Za-z0-9_-]+$/.test(t)||t.length>8192)throw Error("\u623F\u95F4\u94FE\u63A5\u683C\u5F0F\u6216\u957F\u5EA6\u65E0\u6548");let e=t.slice(5).replace(/-/g,"+").replace(/_/g,"/"),r=Uint8Array.from(atob(e+"=".repeat((4-e.length%4)%4)),s=>s.charCodeAt(0));if(r.length>4096)throw Error("\u623F\u95F4\u914D\u7F6E\u8FC7\u957F");return G(JSON.parse(new TextDecoder("utf-8",{fatal:!0}).decode(r)))}var Le=async t=>btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t)))));async function Ct(t,e,r,s,o,n){if(G(e),!e.relays.every(d=>r.map(Ae).includes(d)))throw Error("\u4E2D\u7EE7\u6E90\u4E0D\u5728\u660E\u786E\u767D\u540D\u5355\u5185");if(t.length>2e5)throw Error("\u6A21\u677F\u8FC7\u957F");if(await Le(t)!==n)throw Error("\u5B8C\u6574\u6A21\u677F\u54C8\u5E0C\u4E0D\u7B26");let i=[...t.matchAll(/]*)>([\s\S]*?)<\/script>/g)],a=[...t.matchAll(/

      私密聊天房间

      聊天脚本尚未启动,请查看下方浏览器打开说明。

      ${previewHelp(config)}
      ${RETARGET}

        \n`; + return { html, csp }; +} + +// Same room (version, roomId, master or wrapped master untouched), new relays. +// Relays are public routing data: not part of the roomId derivation or the +// password-wrap AAD, so no password is needed and nothing secret changes. +export function withRelays(config, input) { + validate(config); + return validate({ ...config, relays: relayList(input) }); +} + +const digest = async s => btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s))))); +export async function retargetHtml(config, input, script, style) { + if (typeof script !== 'string' || typeof style !== 'string' || /<\/script/i.test(script) || /<\/style/i.test(style)) throw Error('脚本或样式不可用'); + const next = withRelays(config, input); + return { config: next, ...roomHtml(next, script, style, await digest(script), await digest(style)) }; +} + +export function configFromRoomHtml(html) { + if (typeof html !== 'string' || html.length > 400000) throw Error('不是有效的房间文件'); + const m = html.match(/\n`; + const {html,csp}=roomHtml(config,script,style,scriptHash,styleHash); return {html,scriptHash,styleHash,csp,bytes:Buffer.byteLength(html)}; } export async function buildWeb(check=false, {defaultRelay=null, outputDir=root+'dist/'}={}) { @@ -24,7 +26,7 @@ export async function buildWeb(check=false, {defaultRelay=null, outputDir=root+' if(!check)await mkdir(outputDir,{recursive:true}); const t=await render({},[]),style=(await readFile(root+'html-room/style.css','utf8')).trimEnd(); const built=await build({entryPoints:[root+'html-room/web.js'],bundle:true,write:false,format:'iife',target:['es2020'],minify:true,legalComments:'none',define:{PT_DEFAULT_RELAYS:JSON.stringify(defaultRelays),PT_SCRIPT_HASH:JSON.stringify(t.scriptHash),PT_STYLE_HASH:JSON.stringify(t.styleHash),PT_TEMPLATE_HASH:JSON.stringify(hash(t.html))}}); - const maker=`

        创建 HTML 房间

        同一份文件进入同一聊天房间,生成过程在本浏览器内完成。

        `; + const maker=`

        创建 HTML 房间

        同一份文件进入同一聊天房间,生成过程在本浏览器内完成。

        已有房间文件?改用其他中继(如自建 wss://)

        选择现有房间文件并填写新中继,生成同一房间(密钥、口令不变)的新文件。文件只在本浏览器内读取,不会上传。

        `; const body=t.html.match(/([\s\S]*?)${maker}\n`; const files={'hr-template.html':t.html,'hr.js':built.outputFiles[0].text,'hr.css':style+'\n','hr.html':page}; @@ -47,6 +49,17 @@ if(process.argv[1]===fileURLToPath(import.meta.url)){ } if(args[0]==='template' && (args.length>2 || args.slice(1).some(x=>x!=='--check')))throw Error('template 仅支持 --check'); if(args[0]==='template'){console.log(JSON.stringify(await buildWeb(args.includes('--check'))));process.exit(0);} + if(args[0]==='retarget'){ + const flags=args.slice(1); + if(flags.length%2 || flags.some((value,i)=>i%2===0&&!['--in','--out','--relay'].includes(value)) || flags.filter(x=>x==='--in').length!==1 || flags.filter(x=>x==='--out').length!==1)throw Error('用法:retarget --in 旧房间.html --out 新房间.html --relay wss://新中继 [--relay wss://备用中继]'); + const getAll=name=>flags.filter((value,i)=>i%2===1&&flags[i-1]===name); + const relays=getAll('--relay');if(!relays.length)throw Error('retarget 需要至少一个 --relay'); + // Old files are rebuilt with the current template: the room secret is kept, the code is upgraded. + const config=withRelays(configFromRoomHtml(await readFile(getAll('--in')[0],'utf8')),relays); + const r=await render(config,config.relays); + await writeFile(getAll('--out')[0],r.html,{mode:0o600,flag:'wx'}); + console.log(JSON.stringify({bytes:r.bytes,relays:config.relays,roomId:config.roomId,encrypted:config.encrypted}));process.exit(0); + } let output,relay,allowed; if(args[0]==='room'){ const flags=args.slice(1);