From dfd496c1a95953e8677d025f480ddee906fb509a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 15:43:27 -0400 Subject: [PATCH 01/13] feat: let room holders move a room to another relay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Room files gain a 更换中继 panel that re-emits the same room (master or wrapped key untouched, no password needed) with a new relay list and exact CSP connect-src. The hosted maker converts existing files, and the CLI gains a retarget command. All three paths produce byte-identical output that passes verify-room-file. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PCUwNSXQdqfDSugmFo4qmM --- README.md | 2 + browser-tests/retarget.mjs | 92 +++++++++++++++++++++++++++ dist/hr-template.html | 3 +- dist/hr.html | 2 +- dist/hr.js | 2 +- docs/DEVELOPER.md | 18 ++++++ docs/html-room/protocol.md | 4 +- docs/html-room/release-integrity.json | 12 ++-- html-room/file.js | 27 +++++++- html-room/template.js | 40 ++++++++++++ html-room/web.js | 31 +++++++-- package.json | 3 +- scripts/build-html-room.mjs | 25 ++++++-- 13 files changed, 238 insertions(+), 23 deletions(-) create mode 100644 browser-tests/retarget.mjs create mode 100644 html-room/template.js 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); From e11481a5c352e30ed42f35a27d99899edffe0893 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 16:27:45 -0400 Subject: [PATCH 02/13] feat: decentralized rooms over any broadcast, Nostr or MQTT relay 1. Relay-agnostic transport (version 3 rooms) - Relay entries bc+wss://, nostr+wss://, mqtt+wss:// select version 3; classic wss:// rooms stay version 2, unchanged. Retarget converts between them with the same key, password and roomId. - html-room/mesh.js: members admit (AES-GCM frames under a master-derived key), number (announce, claim, earliest claim wins), list and attribute each other; relays only forward. Every frame goes to every relay and is de-duplicated by IV, so one surviving relay keeps the room together. - Node server and Cloudflare Worker serve /bc/ plain broadcast. 2. Easier relays - relay/go: dependency-free Go broadcast relay, one ~6 MB binary for Windows, macOS, Linux, BSD, Android and OpenWrt routers; -tunnel prints a public bc+wss:// address via a Cloudflare quick tunnel (no domain, open port or root). - scripts/relay-quickstart.sh downloads it (SHA-256 enforced) and cloudflared; Relay binaries workflow publishes 13 platform builds. 3. External protocols - Nostr: ephemeral kind 20111 events tagged with the topic, signed with a per-session BIP-340 key (@noble/curves). - MQTT 3.1.1 over WebSocket, QoS 0, topic htmlroom/. Tests: unit (parsing, sealing, packets, membership clashes), real relays (Node, Go, signature-checking Nostr relay, aedes MQTT, Worker /bc/ under wrangler dev), browser end-to-end over four relays with relay loss. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PCUwNSXQdqfDSugmFo4qmM --- .github/workflows/relay-binaries.yml | 40 +++ README.md | 24 +- THIRD_PARTY_NOTICES.md | 2 + browser-tests/mesh.mjs | 86 +++++ dist/hr-template.html | 4 +- dist/hr.css | 2 +- dist/hr.html | 2 +- dist/hr.js | 2 +- docs/DEVELOPER.md | 42 ++- docs/html-room/protocol.md | 14 + docs/html-room/release-integrity.json | 14 +- docs/html-room/security-model.md | 10 + html-room/client.js | 17 +- html-room/crypto.js | 57 +++- html-room/file.js | 2 +- html-room/mesh.js | 276 ++++++++++++++++ html-room/session.js | 2 +- html-room/style.css | 2 +- html-room/template.js | 11 +- html-room/transport.js | 16 +- html-room/web.js | 12 +- package-lock.json | 446 +++++++++++++++++++++++--- package.json | 9 +- relay/go/Dockerfile | 10 + relay/go/go.mod | 3 + relay/go/main.go | 424 ++++++++++++++++++++++++ scripts/build-html-room.mjs | 6 +- scripts/relay-quickstart.sh | 88 +++++ server/server.mjs | 38 ++- src/rules.js | 15 + src/worker.js | 29 +- test/fixtures/hr-relay-suite.mjs | 21 ++ test/fixtures/mesh-relays.mjs | 76 +++++ test/mesh-relays.test.mjs | 99 ++++++ test/mesh.test.mjs | 133 ++++++++ test/quickstart.test.mjs | 43 +++ 36 files changed, 1978 insertions(+), 99 deletions(-) create mode 100644 .github/workflows/relay-binaries.yml create mode 100644 browser-tests/mesh.mjs create mode 100644 html-room/mesh.js create mode 100644 relay/go/Dockerfile create mode 100644 relay/go/go.mod create mode 100644 relay/go/main.go create mode 100755 scripts/relay-quickstart.sh create mode 100644 test/fixtures/mesh-relays.mjs create mode 100644 test/mesh-relays.test.mjs create mode 100644 test/mesh.test.mjs create mode 100644 test/quickstart.test.mjs diff --git a/.github/workflows/relay-binaries.yml b/.github/workflows/relay-binaries.yml new file mode 100644 index 0000000..d4056b0 --- /dev/null +++ b/.github/workflows/relay-binaries.yml @@ -0,0 +1,40 @@ +name: Relay binaries +# Builds the dependency-free Go broadcast relay for every common platform and +# attaches it to the release, so scripts/relay-quickstart.sh can fetch it. +on: + push: + tags: ['*'] + workflow_dispatch: +permissions: + contents: write +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + - name: Vet and build + working-directory: relay/go + run: | + go vet ./... + mkdir -p ../../out + for t in linux/amd64 linux/arm64 linux/arm linux/386 linux/mips linux/mipsle linux/riscv64 \ + windows/amd64 windows/arm64 darwin/amd64 darwin/arm64 freebsd/amd64 android/arm64; do + os=${t%/*}; arch=${t#*/}; ext=; [ "$os" = windows ] && ext=.exe + GOOS=$os GOARCH=$arch GOMIPS=softfloat GOARM=7 CGO_ENABLED=0 \ + go build -trimpath -ldflags='-s -w' -o ../../out/htmlroom-relay-$os-$arch$ext . + done + cd ../../out && sha256sum htmlroom-relay-* > SHA256SUMS + - uses: actions/upload-artifact@v4 + with: + name: htmlroom-relay + path: out/* + - name: Attach to release + if: startsWith(github.ref, 'refs/tags/') + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 || gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --notes "HTML Room $GITHUB_REF_NAME" + gh release upload "$GITHUB_REF_NAME" out/* --clobber diff --git a/README.md b/README.md index be6b2f5..7e4c26d 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ | 每条消息最多 **500 字** | 不联网使用(聊天时必须有网) | | 断网后自动重连 | 找回已关闭页面里的消息 | -创建房间时可按优先级填写 **1–3 个中继**(以逗号或空格分隔)。首选不可用时会尝试备用,并每 60 秒探测首选是否恢复。页面显示当前中继和在线人数;连到不同中继的成员暂时无法互通,切换后请重新核对安全码。所有成员须使用同一份房间配置。 +创建房间时可按优先级填写 **1–3 个经典中继**(以逗号或空格分隔),或改用下文的[去中心化中继](#去中心化房间不依赖任何一台服务器)。首选不可用时会尝试备用,并每 60 秒探测首选是否恢复。页面显示当前中继和在线人数;连到不同中继的成员暂时无法互通,切换后请重新核对安全码。所有成员须使用同一份房间配置。 **想改用自己搭的中继?** 打开房间文件,展开「更换中继」,填入自己的 `wss://` 地址即可生成同一房间的新文件(密钥、口令不变,无需口令);也可在生成器页面的「已有房间文件?改用其他中继」中转换旧文件。把新文件发给所有成员后,大家就在你的中继上聊天,并重新核对安全码。详见 [更换中继](docs/DEVELOPER.md#更换中继迁移到自建中继)。 @@ -74,6 +74,28 @@ - 本项目还没有经过第三方专业机构的安全审计。 - 想要更放心,就一定要**核对安全码**。 +## 去中心化房间:不依赖任何一台服务器 + +创建房间时,中继除了填经典的 `wss://…`,还可以填以下三种去中心化中继,最多 5 个,可以混用: + +| 写法 | 是什么 | 谁来提供 | +| --- | --- | --- | +| `bc+wss://主机/bc/` | 只负责转发的中继 | 本项目任何部署(Cloudflare、Docker、一键安装),或下面的单文件中继;任意「同一路径收到的消息转发给其他连接」的 WebSocket 服务器都兼容 | +| `nostr+wss://…` | Nostr 中继 | 公共 Nostr 网络中的中继,或自建 | +| `mqtt+wss://…/mqtt` | MQTT 代理(WebSocket) | mosquitto、EMQX 等,路由器、树莓派都能跑 | + +消息**同时发往全部中继**并自动去重:只要有一个还通,大家就能聊天,也不会出现连到不同中继的人互相看不到的情况。成员编号、在线名单都由各成员自己协商,中继只负责转发看不懂的密文;没有房间密钥的人发来的数据会被直接丢弃。旧房间文件可以用「更换中继」直接改成去中心化房间,密钥和口令不变。 + +> 代价:没有中继替你挡住乱发数据的人(他们只能浪费流量,解不开也伪造不了消息);公共 Nostr/MQTT 中继能看到你的 IP 和收发时间,规范要求不保存临时消息,但无法保证。成员变化后请重新核对安全码。部分公共 Nostr 中继要求登录或付费,会在页面上提示被拒绝,换一个即可。 + +### 一条命令开一个中继:不要域名、不要公网 IP、不要 root + +```sh +curl -fsSL https://raw.githubusercontent.com/Freecode100Year/htmlroom/main/scripts/relay-quickstart.sh | sh -s -- --tunnel +``` + +脚本下载对应平台的单文件中继(校验 SHA-256),并通过 Cloudflare 免费临时隧道得到公网地址,最后打印一行 `bc+wss://xxxx.trycloudflare.com/bc/`,把它填进房间即可。支持 Linux、macOS、FreeBSD、WSL、Termux 和 OpenWrt。Windows 可从 [Releases](https://github.com/Freecode100Year/htmlroom/releases) 下载 `htmlroom-relay-windows-amd64.exe`,安装 [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) 后运行 `htmlroom-relay-windows-amd64.exe -tunnel`。临时隧道地址每次启动都会变化;长期使用请配域名和 HTTPS,详见 [单文件中继](docs/DEVELOPER.md#单文件中继go)。 + ## 部署自己的聊天室 ### Cloudflare:前端和中继一起部署(推荐) diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 55334d3..1d5041b 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -9,6 +9,8 @@ Copyright (c) 2026 Freecode100Year. MIT License; see LICENSE and docs/PROVENANCE lib/vendor/mlkem.js bundles ML-KEM-768 from @noble/post-quantum 0.7.1 and supporting @noble/hashes and @noble/curves code. The original source comments and license notices are retained. +html-room/mesh.js bundles BIP-340 Schnorr signing over secp256k1 from @noble/curves 1.9.7 and SHA-256 from @noble/hashes 1.8.0 (npm, exact versions in package-lock.json), used only to sign Nostr events with a per-session throwaway key. + Copyright (c) 2024 Paul Miller (https://paulmillr.com). Copyright (c) 2022 Paul Miller (paulmillr.com). diff --git a/browser-tests/mesh.mjs b/browser-tests/mesh.mjs new file mode 100644 index 0000000..bbb37c6 --- /dev/null +++ b/browser-tests/mesh.mjs @@ -0,0 +1,86 @@ +// End-to-end: a decentralized (version 3) room file over a Node /bc/ relay, +// the Go relay, a Nostr relay and an MQTT broker at once. Three members chat +// with the full signed hybrid encryption, agree on one safety code, and keep +// chatting as relays disappear. +import { chromium } from 'playwright'; +import { mkdtemp, writeFile, rm } 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 { nodeRelay, goRelay, goAvailable, nostrRelay, mqttBroker } from '../test/fixtures/mesh-relays.mjs'; + +const dir = await mkdtemp(tmpdir() + '/hr-mesh-'); +const node = await nodeRelay(), nostr = await nostrRelay(), mqtt = await mqttBroker(), go = goAvailable() ? await goRelay() : null; +let browser; +try { + const relays = [`bc+${node.url}/bc/`, `nostr+${nostr.url}`, `mqtt+${mqtt.url}`, ...(go ? [`bc+${go.url}/bc/`] : [])]; + const config = await makeConfig(relays, 'mesh pass'); + assert.equal(config.version, 3); + const path = dir + '/room.html'; await writeFile(path, (await render(config, config.relays)).html); + browser = await chromium.launch({ headless: true }); + const errors = [], pages = []; + for (let i = 0; i < 3; i++) { + const page = await browser.newPage(); + page.on('pageerror', e => errors.push(e.message)); + page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); }); + await page.goto(pathToFileURL(path).href); + await page.locator('#password').fill('mesh pass'); await page.locator('#enter').click(); + pages.push(page); + } + for (const page of pages) { + await page.locator('#relay-status').filter({ hasText: `已连接 ${relays.length}/${relays.length}` }).waitFor({ timeout: 20000 }); + await page.locator('#presence').filter({ hasText: '在线 3 人' }).waitFor({ timeout: 20000 }); + await page.locator('#safety').filter({ hasText: '安全码' }).waitFor({ timeout: 30000 }); + } + const codes = await Promise.all(pages.map(p => p.locator('#safety').textContent())); + assert.equal(new Set(codes).size, 1, 'all three see the same safety code'); + const send = async (from, text) => { + await pages[from].locator('#text').fill(text); await pages[from].locator('#send').click(); + for (const [i, p] of pages.entries()) if (i !== from) await p.locator('#messages li').filter({ hasText: text }).waitFor({ timeout: 20000 }); + }; + await send(0, '四个中继同时在线'); + await pages[1].locator('#messages li').filter({ hasText: '四个中继同时在线' }).waitFor(); + assert.equal(await pages[1].locator('#messages li', { hasText: '四个中继同时在线' }).count(), 1, 'no duplicates'); + await pages[0].locator('#messages li small').filter({ hasText: /客户端确认 2\/2/ }).first().waitFor({ timeout: 20000 }); + + await node.stop(); await go?.stop(); + for (const page of pages) await page.locator('#relay-status').filter({ hasText: '已连接 2/' }).waitFor({ timeout: 20000 }); + await send(1, '只剩 Nostr 和 MQTT'); + await mqtt.stop(); + for (const page of pages) await page.locator('#relay-status').filter({ hasText: '已连接 1/' }).waitFor({ timeout: 20000 }); + await send(2, '最后单靠 Nostr 一个中继'); + + // A member leaves: the others see it within seconds. + await pages.pop().close(); + for (const page of pages) await page.locator('#presence').filter({ hasText: '在线 2 人' }).waitFor({ timeout: 20000 }); + await send(0, '有人离开后继续'); + assert.ok(nostr.stats.rejected === 0); + + // Hosted generator: one click switches to this site's decentralized relay, + // and the resulting web link works for two members. + const site = await nodeRelay(); + try { + const maker = await browser.newPage(); maker.on('pageerror', e => errors.push(e.message)); + await maker.goto(`http://127.0.0.1:${site.port}/`); + await maker.locator('#mk-mesh').click(); + assert.equal(await maker.locator('#mk-relay').inputValue(), `bc+${site.url}/bc/`); + await maker.locator('#mk-make').click(); + await maker.locator('#mk-link-row').waitFor({ state: 'visible' }); + const link = await maker.locator('#mk-link').inputValue(); + const members = [await browser.newPage(), await browser.newPage()]; + for (const m of members) { m.on('pageerror', e => errors.push(e.message)); await m.goto(link); await m.locator('#enter').click(); } + for (const m of members) await m.locator('#safety').filter({ hasText: '安全码' }).waitFor({ timeout: 30000 }); + await members[0].locator('#text').fill('网页链接进入去中心化房间'); await members[0].locator('#send').click(); + await members[1].locator('#messages li').filter({ hasText: '网页链接进入去中心化房间' }).waitFor({ timeout: 20000 }); + await maker.locator('#mk-relay').fill('wss://a.example nostr+wss://b.example'); await maker.locator('#mk-make').click(); + await maker.locator('#mk-error').filter({ hasText: '不能与去中心化中继' }).waitFor(); + } finally { await site.stop(); } + assert.deepEqual(errors.filter(e => !/WebSocket|ERR_CONNECTION_REFUSED|net::/.test(e)), []); + console.log(`PASS: decentralized room over ${relays.length} relays (bc node${go ? ', bc go' : ''}, nostr, mqtt): 3 members, one safety code, receipts, relay loss down to Nostr alone, departure; hosted one-click decentralized room via web link`); +} finally { + await browser?.close(); + await Promise.all([node.stop(), nostr.stop(), mqtt.stop(), go?.stop()]); + await rm(dir, { recursive: true, force: true }); +} diff --git a/dist/hr-template.html b/dist/hr-template.html index fd72ddd..558972f 100644 --- a/dist/hr-template.html +++ b/dist/hr-template.html @@ -1,2 +1,2 @@ -私密 HTML 聊天房间

        私密聊天房间

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

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

          +私密 HTML 聊天房间

          私密聊天房间

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

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

            diff --git a/dist/hr.css b/dist/hr.css index 8f0d04a..9fa70d2 100644 --- a/dist/hr.css +++ b/dist/hr.css @@ -1 +1 @@ -:root{color-scheme:dark;font-family:system-ui,sans-serif;background:#121820;color:#edf2f7}body{max-width:760px;margin:auto;padding:20px}h1{font-size:1.5rem}p{line-height:1.6}aside{border:1px solid #dcac58;padding:12px;border-radius:10px;color:#ffe0a3}input,textarea,button{font:inherit;padding:12px;border:1px solid #65778c;border-radius:8px;background:#202d3b;color:inherit;box-sizing:border-box}input,textarea{width:100%;margin:8px 0}button{cursor:pointer}button:disabled{opacity:.5}ol{padding:0;list-style:none}li{padding:12px;margin:8px 0;background:#202d3b;border-radius:8px;white-space:pre-wrap;overflow-wrap:anywhere}small{display:block;color:#b9c8d8;margin-top:6px}[hidden]{display:none}#status{color:#a7d9ff}textarea{min-height:90px} +:root{color-scheme:dark;font-family:system-ui,sans-serif;background:#121820;color:#edf2f7}body{max-width:760px;margin:auto;padding:20px}h1{font-size:1.5rem}p{line-height:1.6}aside{border:1px solid #dcac58;padding:12px;border-radius:10px;color:#ffe0a3}input,textarea,button{font:inherit;padding:12px;border:1px solid #65778c;border-radius:8px;background:#202d3b;color:inherit;box-sizing:border-box}input,textarea{width:100%;margin:8px 0}button{cursor:pointer}button:disabled{opacity:.5}ol{padding:0;list-style:none}li{padding:12px;margin:8px 0;background:#202d3b;border-radius:8px;white-space:pre-wrap;overflow-wrap:anywhere}small{display:block;color:#b9c8d8;margin-top:6px}[hidden]{display:none}#status{color:#a7d9ff}textarea{min-height:90px}.hint{color:#b9c8d8;font-size:.92rem}code{background:#202d3b;padding:1px 4px;border-radius:4px;overflow-wrap:anywhere} diff --git a/dist/hr.html b/dist/hr.html index 7051e4c..69cf1b0 100644 --- a/dist/hr.html +++ b/dist/hr.html @@ -1 +1 @@ -HTML Room · 创建或进入房间

            创建 HTML 房间

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

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

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

            +HTML Room · 创建或进入房间

            创建 HTML 房间

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

            经典中继 wss://… 填 1–3 个,按优先级切换。去中心化中继填 1–5 个,消息同时发往全部:bc+wss://…(本项目或任意转发服务器)、nostr+wss://…(Nostr 中继)、mqtt+wss://…/mqtt(MQTT 代理)。两类不能混用。

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

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

            diff --git a/dist/hr.js b/dist/hr.js index f8c078d..34736e7 100644 --- a/dist/hr.js +++ b/dist/hr.js @@ -1 +1 @@ -(()=>{var Dt=Object.defineProperty;var Rt=(t,e,r)=>e in t?Dt(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var H=(t,e,r)=>Rt(t,typeof e!="symbol"?e+"":e,r);var M=[];var zr=new TextEncoder;var Bt=[256,512,1024,2048,4096];function ie(t){let e=Bt.find(o=>o>t.length)??t.length+1,r=new Uint8Array(e);return r.set(t),r[t.length]=128,r}function ae(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 G=new TextEncoder,_=t=>Array.from(new Uint8Array(t),e=>e.toString(16).padStart(2,"0")).join("");function F(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 Re(t){let e=typeof t=="string"?F(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"]),o=async a=>new Uint8Array(await crypto.subtle.deriveBits({name:"HKDF",hash:"SHA-256",salt:new Uint8Array(32),info:G.encode(`private-talk/html-room/v1/${a}`)},r,256)),s=await o("access"),n=await o("key"),i=await o("bind");return{access:s,keyBytes:n,bind:i,roomId:_(await crypto.subtle.digest("SHA-256",s)),key:await crypto.subtle.importKey("raw",n,"AES-GCM",!1,["encrypt","decrypt"])}}function xe(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 Ee={name:"PBKDF2",hash:"SHA-256",iterations:31e4};function Y(t){let e=Array.isArray(t)?t:typeof t=="string"?t.trim().split(/[\s,]+/):[],r=[...new Set(e.map(xe))];if(!r.length||r.length>3)throw Error("\u8BF7\u63D0\u4F9B 1 \u5230 3 \u4E2A\u4E0D\u540C\u4E2D\u7EE7");return r}var Ne=t=>G.encode(`private-talk/html-room/v1/wrap|${t}`);async function Be(t,e){if(typeof t!="string"||!t.length||G.encode(t).length>1024)throw Error("\u53E3\u4EE4\u957F\u5EA6\u65E0\u6548");let r=await crypto.subtle.importKey("raw",G.encode(t),"PBKDF2",!1,["deriveKey"]);return crypto.subtle.deriveKey({...Ee,salt:e},r,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}function D(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(Y(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(o=>!r.includes(o))||!t.kdf||Object.keys(t.kdf).length!==3||Object.entries(Ee).some(([o,s])=>t.kdf[o]!==s))throw Error("\u4E0D\u652F\u6301\u7684 KDF \u914D\u7F6E");F(t.salt,16),F(t.iv,12),F(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");F(t.master,32)}return t}async function Pe(t,e){let r=Y(t),o=N(32),s=await Re(o),n={version:2,relays:r,roomId:s.roomId,encrypted:!!e};if(!e)return{...n,master:_(o)};let i=N(16),a=N(12),l=await Be(e,i),c=await crypto.subtle.encrypt({name:"AES-GCM",iv:a,additionalData:Ne(n.roomId)},l,o);return{...n,salt:_(i),iv:_(a),ciphertext:_(c),kdf:{...Ee}}}async function Fe(t,e){D(t);let r=t.encrypted?new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:F(t.iv,12),additionalData:Ne(t.roomId)},await Be(e,F(t.salt,16)),F(t.ciphertext,48))):F(t.master,32),o=await Re(r);if(r.fill(0),o.roomId!==t.roomId)throw Error("\u623F\u95F4\u5730\u5740\u4E0E\u5BC6\u94A5\u4E0D\u5339\u914D");return o}async function ze(t,e){let r=N(12),o=await crypto.subtle.encrypt({name:"AES-GCM",iv:r,additionalData:G.encode(`private-talk/html-room/v1/keys|${t.roomId}`)},t.key,ie(G.encode(e)));return{iv:btoa(String.fromCharCode(...r)),ct:btoa(String.fromCharCode(...new Uint8Array(o)))}}async function qe(t,e){let r=o=>Uint8Array.from(atob(o),s=>s.charCodeAt(0));return new TextDecoder().decode(ae(new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:r(e.iv),additionalData:G.encode(`private-talk/html-room/v1/keys|${t.roomId}`)},t.key,r(e.ct)))))}var ce=BigInt(4294967295),Xe=BigInt(32);function Pt(t,e=!1){return e?{h:Number(t&ce),l:Number(t>>Xe&ce)}:{h:Number(t>>Xe&ce)|0,l:Number(t&ce)|0}}function Ft(t,e=!1){let r=t.length,o=new Uint32Array(r),s=new Uint32Array(r);for(let n=0;nt?`"${t}" `:"";function z(t,e=""){if(typeof t!="number")throw new TypeError(he(e)+"expected number, got "+typeof t);if(!Number.isSafeInteger(t)||t<0)throw new RangeError(he(e)+"expected integer >= 0, got "+t);return t}function zt(t,e=""){if(typeof t!="boolean")throw new TypeError(he(e)+"expected boolean, got type="+typeof t);return t}function P(t,e,r=""){if(Ge(t)&&(e===void 0||t.length===e))return t;e!==void 0&&z(e,"length");let o=Ge(t),s=e!==void 0?` of length ${e}`:"",n=o?`length=${t.length}`:`type=${typeof t}`,i=he(r)+"expected Uint8Array"+s+", got "+n;throw o?new RangeError(i):new TypeError(i)}var qt=(t,e)=>{if(t===null||typeof t!="object"||Array.isArray(t))throw new TypeError((e==="object"?"":`"${e}" `)+"expected object, got type="+typeof t)},Je=(t,e)=>{qt(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 Ve(t,e=!0){if(t.destroyed)throw new Error("hash was destroyed");if(e&&t.finished)throw new Error("digest() was already called")}function Xt(t,e){P(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 rt(...t){for(let e=0;e>>8&65280|t>>>24&255}function Vt(t){for(let e=0;et:Vt;function nt(t,e,r="opts"){return Je(t,"defaults"),e!==void 0&&Je(e,r),Object.assign(Object.create(null),t,e)}function ot(t,e={}){if(typeof t!="function")throw new TypeError('"hashCons" expected function, got type='+typeof t);e=nt({},e,"info");let r=(s,n)=>t(n).update(s).digest(),o=t(void 0);return r.outputLen=o.outputLen,r.blockLen=o.blockLen,r.canXOF=o.canXOF,r.create=s=>t(s),Object.assign(r,e),Object.freeze(r)}function Yt(t=32){z(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])}),Wt=BigInt(0),te=BigInt(1),Qt=BigInt(2),Zt=BigInt(7),er=BigInt(256),tr=BigInt(113),st=[],it=[],at=[];for(let t=0,e=te,r=1,o=0;t<24;t++){[r,o]=[o,(2*r+3*o)%5],st.push(2*(5*o+r)),it.push((t+1)*(t+2)/2%64);let s=Wt;for(let n=0;n<7;n++)e=(e<>Zt)*tr)%er,e&Qt&&(s^=te<<(te<t<>>32-r,sr=(t,e,r)=>e<>>32-r,ir=(t,e,r)=>e<>>64-r,ar=(t,e,r)=>t<>>64-r,Ye=(t,e,r)=>r>32?ir(t,e,r):or(t,e,r),We=(t,e,r)=>r>32?ar(t,e,r):sr(t,e,r),W=new Uint32Array(10);function cr(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(z(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++)W[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=W[a],c=W[a+1],u=Ye(l,c,1)^W[i],d=We(l,c,1)^W[i+1];for(let v=0;v<50;v+=10)t[n+v]^=u,t[n+v+1]^=d}let o=t[2],s=t[3];for(let n=0;n<24;n++){let i=it[n],a=Ye(o,s,i),l=We(o,s,i),c=st[n];o=t[c],s=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]^=rr[r],t[1]^=nr[r]}rt(W)}var lt=class pt{constructor(e,r,o,s=!1,n=24){H(this,"state");H(this,"pos",0);H(this,"posOut",0);H(this,"finished",!1);H(this,"state32");H(this,"destroyed",!1);H(this,"blockLen");H(this,"suffix");H(this,"outputLen");H(this,"canXOF");H(this,"enableXOF",!1);H(this,"rounds");if(z(e,"blockLen"),z(r,"suffix"),z(n,"rounds"),zt(s,"enableXOF"),this.blockLen=e,this.suffix=r,this.outputLen=o,this.enableXOF=s,this.canXOF=s,this.rounds=n,z(o,"outputLen"),!(0=r?Ke(e):void 0;for(let c=0;c=r){for(let d=0,v=c/4;d=o&&this.keccak();let i=Math.min(o-this.posOut,n-s);e.set(r.subarray(this.posOut,this.posOut+i),s),this.posOut+=i,s+=i}return e}xofInto(e){if(!this.enableXOF)throw new Error("XOF is not enabled");return this.writeInto(e)}xof(e){return z(e),this.xofInto(new Uint8Array(e))}digestInto(e){if(Xt(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,rt(this.state)}_cloneInto(e){let{blockLen:r,suffix:o,outputLen:s,rounds:n,enableXOF:i}=this;return e||(e=new pt(r,o,s,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=o,e.outputLen=s,e.enableXOF=i,e.canXOF=this.canXOF,e.destroyed=this.destroyed,e}},ht=(t,e,r,o={})=>ot(()=>new lt(e,t,r),o),lr=ht(6,136,32,ue(8)),pr=ht(6,72,64,ue(10)),dt=(t,e,r,o={})=>ot((s={})=>(s=nt({},s),new lt(e,t,s.dkLen===void 0?r:s.dkLen,!0)),o),hr=dt(31,168,16,ue(11)),ut=dt(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 dr(t,e={},r={},o="object"){Se(t,o),Se(e,"fields"),Se(r,"optFields");function s(i,a,l){let c=o==="object"?`param "${String(i)}"`:`"${o}.${String(i)}"`,u=t[i];if(!Object.prototype.hasOwnProperty.call(t,i)&&(l?u!==void 0:a!=="function"))throw new TypeError(`${c} is invalid: expected own property`);if(l&&u===void 0)return;let d=typeof u;if(d!==a||u===null)throw new TypeError(`${c} is invalid: expected ${a}, got ${d}`)}let n=(i,a)=>Object.entries(i).forEach(([l,c])=>s(l,c,a));n(e,!1),n(r,!0)}function oe(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 ft(t){return oe(t,"x"),(t&t-1)===0&&t!==0}function yt(t,e){if(oe(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 o=0;o>>=1)r=r<<1|t&1;return r>>>0}function wt(t){return oe(t),31-Math.clz32(t)}function Qe(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(!ft(e))throw new Error("expected positive power-of-two length, got "+e);let r=wt(e);for(let o=0;o{dr(e,{N:"number",roots:"object",dit:"boolean"},{invertButterflies:"boolean",skipStages:"number",brp:"boolean"},"coreOpts");let{N:r,roots:o,dit:s,invertButterflies:n=!1,skipStages:i=0,brp:a=!0}=e;oe(r,"coreOpts.N");let l=wt(r);if(!ft(r))throw new Error("FFT: Polynomial size should be power of two");oe(i,"coreOpts.skipStages");let c=l===0?0:l-1;if(i>c)throw new Error(`FFT: wrong skipStages: expected 0 <= skipStages <= ${c}`);if(o.length!==r)throw new Error(`FFT: wrong roots length: expected ${r}, got ${o.length}`);let u=s!==n;return d=>{if(d.length!==r)throw new Error("FFT: wrong Polynomial length");s&&a&&Qe(d);for(let v=0,g=1;v>1,S=r>>y;for(let T=0;T{}){if(!Array.isArray(t))throw new TypeError(`"${e}" expected array, got type=${typeof t}`);for(let o=0;otypeof s=="number"?s:s.bytesLen,o=e.reduce((s,n)=>s+r(n),0);return{bytesLen:o,encode:s=>{let n=new Uint8Array(o);for(let i=0,a=0;i{P(s,o,t);let n=[];for(let i of e){let a=r(i),l=s.subarray(0,a);n.push(typeof i=="number"?l:i.decode(l)),s=s.subarray(a)}return n}}}function $e(t,e){let r=t,o=e*r.bytesLen;return{bytesLen:o,encode:s=>{let n=ur(s,"u");if(n.length!==e)throw new RangeError(`vecCoder.encode: wrong length=${n.length}. Expected: ${e}`);let i=new Uint8Array(o);for(let a=0,l=0;a{P(s,o);let n=[];for(let i=0;i32)throw new RangeError('"bits" expected <= 32, got '+t);return t===32?4294967295:~(-1<>>0}var fr=t=>{let{newPoly:e,N:r,Q:o,F:s,ROOT_OF_UNITY:n,brvBits:i,isKyber:a}=t,l=(p,h=o)=>{let w=p%h|0;return(w>=0?w|0:h+w|0)|0},c=(p,h=o)=>{let w=l(p,h)|0;return(w>h>>1?w-h|0:w)|0};function u(){let p=e(r);for(let h=0;h{throw new Error("not implemented")},g=a?{add:(p,h)=>{let w=p+h|0;return w>=o?w-o|0:w},sub:(p,h)=>{let w=p-h|0;return w<0?w+o|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:d,invertButterflies:!0,skipStages:a?1:0,brp:!1},f=Ze(g,{dit:!1,...y}),$=Ze(g,{dit:!0,...y}),S={encode:p=>f(p),decode:p=>{$(p);for(let h=0;hS.encode(p),decode:p=>S.decode(p)},bitsCoder:(p,h)=>{for(let m=0,b=0;m32&&tt(b),b%=8;let w=tt(p),A=p*(r/8);return{bytesLen:A,encode:m=>{let b=m,x=new Uint8Array(A);for(let O=0,k=0,I=0,R=0;O=8;I-=8,k>>=8)x[R++]=k&255;return x},decode:m=>{let b=e(r);for(let x=0,O=0,k=0,I=0;x=p;k-=p,O>>=p)b[I++]=h.decode(O&w);return b}}}}},yr=t=>(e,r)=>{r||(r=t.blockLen);let o=new Uint8Array(e.length+2);o.set(e);let s=e.length,n=new Uint8Array(r),i=t.create({}),a=0,l=0;return{stats:()=>({calls:a,xofs:l}),get:(c,u)=>(o[s+0]=c,o[s+1]=u,i.destroy(),i=t.create({}).update(o),a++,()=>(l++,i.xofInto(n))),clean:()=>{i.destroy(),K(n,o)}}},wr=yr(hr);var U=256,C=3329,mr=3303,gr=17,L=fr({N:U,Q:C,F:mr,ROOT_OF_UNITY:gr,newPoly:t=>new Uint16Array(t),brvBits:7,isKyber:!0}),br=Object.freeze({512:Object.freeze({N:U,Q:C,K:2,ETA1:3,ETA2:2,du:10,dv:4,RBGstrength:128}),768:Object.freeze({N:U,Q:C,K:3,ETA1:2,ETA2:2,du:10,dv:4,RBGstrength:192}),1024:Object.freeze({N:U,Q:C,K:4,ETA1:2,ETA2:2,du:11,dv:5,RBGstrength:256})}),kr=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}},vr=t=>L.bitsCoder(t,t===12?{encode:e=>e,decode:e=>e>=C?e-C:e}:{encode:e=>e,decode:e=>e}),re=t=>t===12?vr(12):L.bitsCoder(t,kr(t));function J(t,e){let r=t,o=e;for(let s=0;s=C?n-C:n}}function Ar(t,e){let r=t,o=e;for(let s=0;s>1)];s&1&&(n=-n);let{c0:i,c1:a}=xr(r[2*s+0],r[2*s+1],o[2*s+0],o[2*s+1],n);r[2*s+0]=i,r[2*s+1]=a}return r}function Oe(t){let e=t,r=new Uint16Array(U);for(let o=0;o>0|s[n+1]<<8)&4095,a=(s[n+1]>>4|s[n+2]<<4)&4095;i{let r=new Uint16Array(U),o=Ke(t);de(o);let s=0;for(let n=0,i=0,a=0,l=0;n>=1,s+=1,s===e?(l=a,a=0):s===2*e&&(r[i++]=L.mod(l-a),a=0,s=0)}if(de(o),s)throw new Error(`sampleCBD: leftover bits: ${s}`);return r};function ne(t,e,r,o){return Er(t(o*U/4,e,r),o)}var Sr=t=>{let e=t,{K:r,PRF:o,XOF:s,HASH512:n,ETA1:i,ETA2:a,du:l,dv:c}=e,u=re(1),d=re(c),v=re(l),g=pe("publicKey",$e(re(12),r),32),y=$e(re(12),r),f=pe("ciphertext",$e(v,r),d),$=pe("seed",32,32),S=(T,p,h,w)=>{let A=[];for(let k=0;k{B(T,32,"seed");let p=new Uint8Array(33);p.set(T),p[32]=r;let h=n(p),[w,A]=$.decode(h),m=[],b=[];for(let k=0;k{let[w,A]=g.decode(T),m=s(A),b=S(w,(x,O)=>Oe(m.get(x,O)),p,h);return m.clean(),b},prepare:T=>{let[p,h]=g.decode(T),w=s(h),A=[];for(let m=0;mS(p.map(x=>x.slice()),(x,O)=>A[x*r+O].slice(),m,b),clean:()=>K(p,A)}},decrypt:(T,p)=>{let[h,w]=f.decode(T),A=y.decode(p),m=new Uint16Array(U);for(let x=0;x{let f=g.subarray(0,384*e.K),$=i.encode(i.decode(et(f))),S=Q($,f);if(K($),!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:g=>{let y=g===void 0,f=y?Te(u):g,$,S;try{B(f,u,"seed");let T=r.keygen(f.subarray(0,32)),p=T.publicKey;$=T.secretKey,S=o(p);let h=l.encode([$,p,S,f.subarray(32)]);return{publicKey:p,secretKey:h}}finally{$!==void 0&&K($),S!==void 0&&K(S),y&&K(f)}},getPublicKey:g=>{let[y,f,$,S]=l.decode(g);return Uint8Array.from(f)},encapsulate:(g,y)=>{let f=y===void 0,$=f?Te(c):y,S;try{return B(g,a.publicKey,"publicKey"),B($,c,"message"),d(g,"encapsulate"),S=s.create().update($).update(o(g)).digest(),{cipherText:r.encrypt(g,$,S.subarray(32,64)),sharedSecret:S.subarray(0,32)}}finally{S!==void 0&&K(S.subarray(32)),f&&K($)}},decapsulate:(g,y)=>{B(y,l.bytesLen,"secretKey"),B(g,a.cipherText,"cipherText");let f=l.bytesLen-96,$=f+32,S=o(y.subarray(f/2,$));if(!Q(S,y.subarray($,$+32)))throw new Error("invalid secretKey: hash check failed");let[T,p,h,w]=l.decode(y),A=r.decrypt(g,T),m=s.create().update(A).update(h).digest(),b=m.subarray(0,32),x=r.encrypt(p,A,m.subarray(32,64)),O=Q(g,x),k=n.create({dkLen:32}).update(w).update(g).digest();return K(A,x,m.subarray(32),O?k:b),O?b:k},prepare:g=>{B(g,a.publicKey,"publicKey"),d(g,"prepare");let y=et(g),f=o(y),$=r.prepare(y);return Object.freeze({publicKey:y,encapsulate:S=>{let T=S===void 0,p=T?Te(c):S,h;try{return B(p,c,"message"),h=s.create().update(p).update(f).digest(),{cipherText:$.encrypt(p,h.subarray(32,64)),sharedSecret:h.subarray(0,32)}}finally{h!==void 0&&K(h.subarray(32)),T&&K(p)}},decapsulate:(S,T)=>{B(T,l.bytesLen,"secretKey"),B(S,a.cipherText,"cipherText");let[p,h,w,A]=l.decode(T);if(!Q(h,y)||!Q(w,f))throw new Error("ML-KEM.decapsulate: secretKey does not match prepared publicKey");let m=r.decrypt(S,p),b=s.create().update(m).update(f).digest(),x=b.subarray(0,32),O=$.encrypt(m,b.subarray(32,64)),k=Q(S,O),I=n.create({dkLen:32}).update(A).update(S).digest();return K(m,O,b.subarray(32),k?I:x),k?x:I},clean:$.clean})}})}function $r(t,e,r){return ut.create({dkLen:t}).update(e).update(new Uint8Array([r])).digest()}var Or={HASH256:lr,HASH512:pr,KDF:ut,XOF:wr,PRF:$r},Kr=t=>Tr({...Or,...t}),fe=Kr(br[768]);var j=new TextEncoder,Ce=new TextDecoder,St={name:"ECDH",namedCurve:"P-256"},mt={name:"ECDSA",namedCurve:"P-256"},ye={name:"ECDSA",hash:"SHA-256"},Cr=2e3,Ur=100,Ir=600*1e3,gt=60*1e3,Lr=20*1e3,Hr=4,Mr=4e3,bt=1580,_r=1452,jr=/^(\d{1,9})\.(\d{1,9})$/,we=/^[A-Za-z0-9+/]+={0,2}$/,Dr=/,"s":"([A-Za-z0-9+/]{86}==)"\}$/,X=t=>btoa(String.fromCharCode(...new Uint8Array(t))),q=t=>Uint8Array.from(atob(t),e=>e.charCodeAt(0)),kt=t=>Array.from(new Uint8Array(t),e=>e.toString(16).padStart(2,"0")).join("");async function vt(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 Ue(t){return{mk:await vt(t,1),next:await vt(t,2)}}async function At(t,e){return crypto.subtle.importKey("raw",t,"AES-GCM",!1,e)}async function me(t,e,r){let o=crypto.getRandomValues(new Uint8Array(12)),s=await crypto.subtle.encrypt({name:"AES-GCM",iv:o,additionalData:j.encode(r)},t,e);return{iv:X(o),ct:X(s)}}async function Ie(t,{iv:e,ct:r},o){return new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:q(e),additionalData:j.encode(o)},t,q(r)))}var Rr=(t,e)=>{let r=new Uint8Array(t.length+e.length);return r.set(t),r.set(e,t.length),r};async function xt(){let t=await crypto.subtle.generateKey(St,!1,["deriveBits"]);return{pair:t,epk:X(await crypto.subtle.exportKey("raw",t.publicKey))}}var Et=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,o){let s=new t;return s.key=e,s.bind=r,s.me=o,s.signing=await crypto.subtle.generateKey(mt,!1,["sign","verify"]),s.spk=X(await crypto.subtle.exportKey("raw",s.signing.publicKey)),s.kem=fe.keygen(),s.kpk=X(s.kem.publicKey),s.ecdh=await xt(),s.ecdhSince=Date.now(),s.oldEcdh=[],s.peers=new Map,s.changed=[],await s.rekey(0),s}get epk(){return this.ecdh.epk}async rekey(e=this.mine.epoch+1,r=Date.now()){this.mine&&r-this.ecdhSince>=Lr&&(this.oldEcdh=[{ecdh:this.ecdh,until:r+gt},...this.oldEcdh].slice(0,Hr),this.ecdh=await xt(),this.ecdhSince=r),this.oldEcdh=this.oldEcdh.filter(o=>o.until>r),this.mine={epoch:e,ck:crypto.getRandomValues(new Uint8Array(32)),c:0,since:r,handed:new Map};for(let o of this.peers.values())o.sent=-1,o.gotAt=null}due(e=Date.now()){return this.mine.c>=Ur||e-this.mine.since>=Ir}forget(e){this.peers.delete(e)}async pairKey(e,r,o=!1){let s=o?`${e.epk}|${r.kcIn??r.kc}`:e.epk;if(r.wraps.has(s))return r.wraps.get(s);let n=await crypto.subtle.importKey("raw",q(r.epk),St,!1,[]),i=new Uint8Array(await crypto.subtle.deriveBits({name:"ECDH",public:n},e.pair.privateKey,256)),a=await crypto.subtle.importKey("raw",o?Rr(i,r.ss):i,"HKDF",!1,["deriveKey"]),l=j.encode(`private-talk/${o?"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(s,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,o=Date.now()){return(await this.announceAll(e,r,o,1/0))[0]}async announceAll(e,r,o=Date.now(),s=Mr){let n=this.mine;for(let[y,f]of n.handed)o-f.at>gt&&n.handed.delete(y);let i=[...this.peers].some(([y,f])=>y{let y=a&&d?{q:1,k:this.kpk,...c}:{q:1,...c},f={kind:"keys",v:32,from:this.me,id:kt(crypto.getRandomValues(new Uint8Array(8))),t:o,epk:this.epk,spk:this.spk,hello:!!r&&d,w:y};f.sig=X(await crypto.subtle.sign(ye,this.signing.privateKey,j.encode(Et(f)))),l.push(JSON.stringify(f)),c={},u=0,d=!1},g=()=>430+(a&&d?bt+8:0);for(let y of e){let f=this.peers.get(y);if(!f)continue;let{pq:$,can:S}=this.pqFor(y,f);if(!S)continue;let T=f.gotAt!=null&&n.handed.has(f.gotAt),p=T?f.gotAt:n.c,h=T?n.handed.get(p).ck:n.ck;T||(f.gotAt=p,n.handed.has(p)||n.handed.set(p,{ck:h,at:o}));let w=j.encode(JSON.stringify({e:n.epoch,ck:X(h),c:p})),A=$?{...await me(await this.pairKey(this.ecdh,f,!0),w,`pt32w|${this.me}|${y}|${f.epk}`),q:1,...f.kc&&{kc:f.kc}}:await me(await this.pairKey(this.ecdh,f),w,`pt3w|${this.me}|${y}|${f.epk}`),m=JSON.stringify(A).length+8;Object.keys(c).length&&g()+u+m>s&&await v(),c[y]=A,u+=m,f.sent=this.mine.epoch}return(d||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"||!we.test(e.epk)||e.epk.length>120||!e.w||typeof e.w!="object")return null;let o=typeof e.spk=="string",s=null;if(o){if(!we.test(e.spk)||e.spk.length>120||typeof e.sig!="string")throw new Error("bad announcement");let a=this.peers.get(r);if(s=a?.spk===e.spk?a.verify:await crypto.subtle.importKey("raw",q(e.spk),mt,!1,["verify"]),!await crypto.subtle.verify(ye,s,q(e.sig),j.encode(Et(e))))throw new Error("bad signature")}let n=this.peers.get(r);n&&(n.spk??null)!==(o?e.spk:null)&&(n.spk&&this.changed.push(r),n=null),n?n.epk!==e.epk&&(o?(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:o?e.spk:null,verify:s,wraps:new Map,recv:null,prev:null,sent:-1,gotAt:null,v:o?31:30,kpk:null,ss:null,kc:null,kcIn:null,pqIn:!1},this.peers.set(r,n)),o&&e.w.q===1&&n.v!==32&&(n.v=32,n.sent=-1),o&&typeof e.w.k=="string"&&e.w.k.length===bt&&we.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&&o&&(n.v=32),a&&rd.ecdh)],c=null;if(!a||n.ss)for(let d of l)try{c=JSON.parse(Ce.decode(await Ie(await this.pairKey(d,n,a),i,`${a?"pt32w":"pt3w"}|${r}|${this.me}|${d.epk}`)));break}catch{}c&&a&&this.me=0&&u.length===32&&(!n.recv||c.e>n.recv.epoch)?(n.prev=n.recv,n.recv={epoch:c.e,ck:u,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(o=>o===this.me||this.peers.get(o)?.sent===this.mine.epoch&&(!r||this.peers.get(o).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 o=this.peers.get(e);if(!o?.ss)throw new Error("no pair secret");let s=await crypto.subtle.importKey("raw",o.ss,"HKDF",!1,["deriveKey"]),n=j.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},s,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}async sealFor(e,r,o){return me(await this.pairSecret(e,o),j.encode(r),`pt32f|${this.me}|${e}|${o}`)}async openFrom(e,r,o){return Ce.decode(await Ie(await this.pairSecret(e,o),r,`pt32f|${e}|${this.me}|${o}`))}async seal(e){let r=this.mine,o=`${r.epoch}.${r.c}`,{mk:s,next:n}=await Ue(r.ck);r.ck=n,r.c++;let i=X(await crypto.subtle.sign(ye,this.signing.privateKey,j.encode(`pt31m|${this.me}|${o}|${e}`))),a=`${e.slice(0,-1)},"s":"${i}"}`;return{...await me(await At(s,["encrypt"]),ie(j.encode(a)),`pt3|${this.me}|${o}`),h:o}}async open(e,r){let o=jr.exec(e.h??""),s=this.peers.get(r);if(!o||!s?.recv)throw new Error("no key");let n=Number(o[1]),i=Number(o[2]),a=n===s.recv.epoch?"recv":n===s.prev?.epoch?"prev":null,l=s[a];if(!a||iCr)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(ye,s.verify,q(y[1]),j.encode(`pt31m|${r}|${e.h}|${v}`)))throw new Error("bad signature");g=!0}return s[a]={epoch:n,ck:d,c:i+1},{text:v,missed:i-l.c,signed:g}}async safetyCode(e){let r=i=>i===this.me?this.spk:this.peers.get(i)?.spk??this.peers.get(i)?.epk??"?",o=e.map(i=>`${i}:${r(i)}`).sort().join("|"),s=new Uint8Array(await crypto.subtle.digest("SHA-256",j.encode(`private-talk/v3.1/safety|${kt(this.bind)}|${o}`))),n=0n;for(let i of s.subarray(0,12))n=n<<8n|BigInt(i);return(n%10n**20n).toString().padStart(20,"0").replace(/(\d{5})(?=\d)/g,"$1 ")}};function ke(t){let e=t.trim();return!e||Array.from(e).length>500||new TextEncoder().encode(e).byteLength>2048?null:e}var be=600*1e3,Tt=/^[0-9a-f]{16}$/;function $t(t,e,r=Date.now(),o,s,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,...o&&{g:o},...s&&{f:s},...a&&{q:n}})}function Ot(t,e,r,o=Date.now()){let s;try{s=JSON.parse(t)}catch{return null}if(!s||typeof s!="object"||typeof s.text!="string"||typeof s.id!="string"||!Tt.test(s.id)||!Number.isInteger(s.from)||s.from<1||s.from>25||s.from!==e.from||!Number.isFinite(s.t)||!Number.isFinite(e.at)||Math.abs(o-s.t)>be||Math.abs(e.at-s.t)>be)return null;let n=Number.isInteger(s.q)&&s.q>=0&&s.q<=5;if(n){if(s.text!==""&&ke(s.text)!==s.text)return null}else if(ke(s.text)!==s.text)return null;for(let[c,u]of r)Math.abs(o-u)>2*be&&r.delete(c);if(r.has(s.id))return null;r.set(s.id,s.t);let i=s.g&&typeof s.g=="object"&&!Array.isArray(s.g)?s.g:void 0,a=s.f&&typeof s.f=="object"&&!Array.isArray(s.f)?s.f:void 0,l=n?s.q:void 0;return{text:s.text,id:s.id,g:i,f:a,q:l}}function Le(t,e,r,o=Date.now(),s="keys"){let n;try{n=JSON.parse(t)}catch{return null}return!n||typeof n!="object"||n.kind!==s||typeof n.id!="string"||!Tt.test(n.id)||!Number.isInteger(n.from)||n.from!==e.from||!Number.isFinite(n.t)||Math.abs(o-n.t)>be||r.has(n.id)?null:(r.set(n.id,n.t),n)}var Nr=()=>new Map,ve=class{constructor(e,r,{storage:o=Nr(),emit:s=()=>{}}={}){Object.assign(this,{d:e,config:r,store:o,emit:s,deviceId:_(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 o of await this.group.announceAll(r,e))this.send({type:"key",...await ze(this.d,o)})}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=Le(await qe(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}`,o=this.received.get(r);if(o){e.type==="msg"&&o.iv===e.iv&&o.ct===e.ct&&this.receipts.length<100&&this.receipts.push([e.from,e.h]);return}let s=await this.group.open(e,e.from);if(!(!s.signed||this.group.peers.get(e.from)?.v!==32))if(e.type==="seen"){let n=Le(s.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=Ot(s.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=ke(e),!e||this.queued.length>=20))return!1;let r=_(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:_(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($t(this.group.me,e.text))};for(this.own.set(r.h,{...e,frame:r,targets:this.present.filter(o=>o!==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(u,d)}){Object.assign(this,{relays:e,roomId:r,protocols:o,onactive:s,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 o={index:e,probe:r,frames:[],ready:!1,retired:!1};r?this.probe=o:this.pending=o;try{o.ws=this.createSocket(`${this.relays[e]}/hr/${this.roomId}`,this.protocols(e))}catch{this.fail(o);return}o.timeout=setTimeout(()=>this.fail(o),1e4),o.ws.onerror=()=>{!o.retired&&!o.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")},o.ws.onclose=s=>this.fail(o,s.code),o.ws.onmessage=s=>{if(o.retired||this.stopped||typeof s.data!="string"||s.data.length>7e3)return;let n;try{n=JSON.parse(s.data)}catch{return}if(!(!n||typeof n!="object")){if(o.ready){if(this.active!==o)return;n.type==="pong"&&(o.lastPong=Date.now()),this.onmessage(n);return}if(o.frames.length>=50){this.fail(o);return}o.frames.push(n),n.type==="hello"&&!o.activating&&Number.isInteger(n.you)&&n.you>=1&&n.you<=25&&typeof n.resume=="string"&&/^[0-9a-f]{32}$/.test(n.resume)&&(o.activating=!0,this.activate(o).catch(()=>this.fail(o)))}}}async activate(e){let r=this.active,o=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,o),!(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 s of e.frames)this.onmessage(s);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 Kt(t){let e=c=>document.getElementById(c),r=c=>{e("status").textContent=c};e("preview-help").hidden=!0;let o=0,s=!1,n=0,i,a=new Map;function l(c,u){if((c==="notice"||c==="fatal")&&r(u),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 ${u} \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${u}\uFF0C\u8BF7\u901A\u8FC7\u72EC\u7ACB\u6E20\u9053\u6838\u5BF9\u3002`),c==="message"){let d=document.createElement("li");d.textContent=`${u.from}\uFF1A${u.text}`;let v=document.createElement("small");d.append(v),e("messages").append(d),a.set(u.id,v),a.size>200&&(a.delete(a.keys().next().value),e("messages").firstChild.remove())}c==="delivery"&&a.has(u.id)&&(a.get(u.id).textContent=u.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{D(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(),!(s||Date.now()["pt2",`a${_(i.d.access)}`,...i.resume&&d.lastIndex===v?[`r${i.resume}`]:[]],onactive:(v,g,y)=>i.run(()=>{y&&i.resetRelay(),i.ws=v,e("relay-status").textContent=`\u5F53\u524D\u4E2D\u7EE7\uFF1A${t.relays[g]}\uFF08\u4F18\u5148\u7EA7 ${g+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 g=i.ws;i.run(()=>{if(g===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=d,d.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{o++,n=Date.now()+Math.min(3e4,1e3*2**Math.min(o,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{s=!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 Ct=t=>JSON.stringify(t).replace(/[<>/\u2028\u2029]/g,e=>`\\u${e.charCodeAt(0).toString(16).padStart(4,"0")}`),Br=t=>btoa(String.fromCharCode(...new TextEncoder().encode(t))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");function _e(t){return D(t),"#hr1="+Br(JSON.stringify(t))}function Me(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 Ut(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)),o=>o.charCodeAt(0));if(r.length>4096)throw Error("\u623F\u95F4\u914D\u7F6E\u8FC7\u957F");return D(JSON.parse(new TextDecoder("utf-8",{fatal:!0}).decode(r)))}var He=async t=>btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t)))));async function It(t,e,r,o,s,n){if(D(e),!e.relays.every(u=>r.map(xe).includes(u)))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 He(t)!==n)throw Error("\u5B8C\u6574\u6A21\u677F\u54C8\u5E0C\u4E0D\u7B26");let i=[...t.matchAll(/]*)>([\s\S]*?)<\/script>/g)],a=[...t.matchAll(/

            私密聊天房间

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

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

              +加密聊天房间

              加密聊天房间

              正在启动……如果一直停在这里,请看下面的说明。

              页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                diff --git a/dist/hr.css b/dist/hr.css index 9fa70d2..8fdaba9 100644 --- a/dist/hr.css +++ b/dist/hr.css @@ -1 +1 @@ -:root{color-scheme:dark;font-family:system-ui,sans-serif;background:#121820;color:#edf2f7}body{max-width:760px;margin:auto;padding:20px}h1{font-size:1.5rem}p{line-height:1.6}aside{border:1px solid #dcac58;padding:12px;border-radius:10px;color:#ffe0a3}input,textarea,button{font:inherit;padding:12px;border:1px solid #65778c;border-radius:8px;background:#202d3b;color:inherit;box-sizing:border-box}input,textarea{width:100%;margin:8px 0}button{cursor:pointer}button:disabled{opacity:.5}ol{padding:0;list-style:none}li{padding:12px;margin:8px 0;background:#202d3b;border-radius:8px;white-space:pre-wrap;overflow-wrap:anywhere}small{display:block;color:#b9c8d8;margin-top:6px}[hidden]{display:none}#status{color:#a7d9ff}textarea{min-height:90px}.hint{color:#b9c8d8;font-size:.92rem}code{background:#202d3b;padding:1px 4px;border-radius:4px;overflow-wrap:anywhere} +:root{color-scheme:dark;font-family:system-ui,sans-serif;background:#121820;color:#edf2f7}body{max-width:760px;margin:auto;padding:20px}h1{font-size:1.5rem}p{line-height:1.6}aside{border:1px solid #dcac58;padding:12px;border-radius:10px;color:#ffe0a3}input,textarea,button{font:inherit;padding:12px;border:1px solid #65778c;border-radius:8px;background:#202d3b;color:inherit;box-sizing:border-box}input,textarea{width:100%;margin:8px 0}button{cursor:pointer}button:disabled{opacity:.5}ol{padding:0;list-style:none}li{padding:12px;margin:8px 0;background:#202d3b;border-radius:8px;white-space:pre-wrap;overflow-wrap:anywhere}small{display:block;color:#b9c8d8;margin-top:6px}[hidden]{display:none}#status{color:#a7d9ff}textarea{min-height:90px}.hint{color:#b9c8d8;font-size:.92rem}code{background:#202d3b;padding:1px 4px;border-radius:4px;overflow-wrap:anywhere}footer{margin-top:32px;border-top:1px solid #34465a}a{color:#a7d9ff}details{margin:14px 0}summary{cursor:pointer}#mk-make,#enter,#send{background:#2f6fb0;border-color:#4a8bd0;font-weight:600;padding:12px 22px}button{margin:4px 0} diff --git a/dist/hr.html b/dist/hr.html index 69cf1b0..d359a0a 100644 --- a/dist/hr.html +++ b/dist/hr.html @@ -1 +1 @@ -HTML Room · 创建或进入房间

                创建 HTML 房间

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

                经典中继 wss://… 填 1–3 个,按优先级切换。去中心化中继填 1–5 个,消息同时发往全部:bc+wss://…(本项目或任意转发服务器)、nostr+wss://…(Nostr 中继)、mqtt+wss://…/mqtt(MQTT 代理)。两类不能混用。

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

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

                +HTML Room · 加密聊天室

                创建加密聊天室

                点一下就能建好一个房间,把文件或链接发给朋友,打开就能聊。全程在你的浏览器里完成,服务器看不到聊天内容。

                中继服务器(一般不用改)

                中继负责帮大家传消息,看不到内容。想换成自己的或多个服务器:经典 wss://… 最多 3 个,坏了会自动换下一个;去中心化 bc+wss://…、nostr+wss://…、mqtt+wss://…/mqtt 最多 5 个,同时使用。两类不能混用。

                已经有房间文件?换一个中继服务器

                选择房间文件,填上新的中继地址,会生成同一个房间的新文件,口令不变。文件只在你的浏览器里读取,不会上传。

                HTML Room 开源项目:github.com/Freecode100Year/htmlroom

                diff --git a/dist/hr.js b/dist/hr.js index 34736e7..9a9fa2f 100644 --- a/dist/hr.js +++ b/dist/hr.js @@ -1 +1 @@ -(()=>{var No=Object.defineProperty;var Mo=(e,t,r)=>t in e?No(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r;var st=(e,t,r)=>Mo(e,typeof t!="symbol"?t+"":t,r);var q=[];var Ui=new TextEncoder;var qo=[256,512,1024,2048,4096];function ye(e){let t=qo.find(n=>n>e.length)??e.length+1,r=new Uint8Array(t);return r.set(e),r[e.length]=128,r}function me(e){let t=e.length-1;for(;t>=0&&e[t]===0;)t--;return t>=0&&e[t]===128?e.subarray(0,t):e}var At=new TextEncoder,z=e=>Array.from(new Uint8Array(e),t=>t.toString(16).padStart(2,"0")).join("");function yt(e,t){if(typeof e!="string"||e.length!==t*2||!/^[0-9a-f]+$/.test(e))throw Error("\u914D\u7F6E\u5B57\u8282\u683C\u5F0F\u9519\u8BEF");return Uint8Array.from(e.match(/../g),r=>parseInt(r,16))}var tt=e=>crypto.getRandomValues(new Uint8Array(e));async function jr(e){let t=typeof e=="string"?yt(e,32):e;if(!(t instanceof Uint8Array)||t.length!==32)throw Error("\u623F\u95F4\u5BC6\u94A5\u957F\u5EA6\u9519\u8BEF");let r=await crypto.subtle.importKey("raw",t,"HKDF",!1,["deriveBits"]),n=async u=>new Uint8Array(await crypto.subtle.deriveBits({name:"HKDF",hash:"SHA-256",salt:new Uint8Array(32),info:At.encode(`private-talk/html-room/v1/${u}`)},r,256)),o=await n("access"),s=await n("key"),i=await n("bind"),c=async u=>new Uint8Array(await crypto.subtle.deriveBits({name:"HKDF",hash:"SHA-256",salt:new Uint8Array(32),info:At.encode(`private-talk/html-room/v3/${u}`)},r,256)),a=await c("mesh"),f=z(await c("topic")).slice(0,32),d=await crypto.subtle.importKey("raw",a,"AES-GCM",!1,["encrypt","decrypt"]);return a.fill(0),{access:o,keyBytes:s,bind:i,topic:f,meshKey:d,roomId:z(await crypto.subtle.digest("SHA-256",o)),key:await crypto.subtle.importKey("raw",s,"AES-GCM",!1,["encrypt","decrypt"])}}var jo=/^(?:[a-z0-9.-]+|\[[0-9a-f:]+\])$/,Po=["localhost","127.0.0.1","[::1]"];function Pr(e){if(!jo.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||!["ws:","wss:"].includes(e.protocol))throw Error("\u4E2D\u7EE7\u5FC5\u987B\u662F WS/WSS \u5730\u5740\uFF0C\u4E0D\u542B\u8D26\u53F7\u3001\u67E5\u8BE2\u53C2\u6570\u6216 #");if(e.protocol==="ws:"&&!Po.includes(e.hostname))throw Error("\u8FDC\u7A0B\u4E2D\u7EE7\u5FC5\u987B\u4F7F\u7528 WSS")}function Vo(e){let t=new URL(e);if(Pr(t),t.pathname!=="/")throw Error("\u4E2D\u7EE7\u5FC5\u987B\u662F\u7CBE\u786E\u7684 WS/WSS \u6E90");return t.origin}var Vr=/^(bc|nostr|mqtt)\+/i,Go=/^(?:\/[A-Za-z0-9._~-]+)*\/?$/,ft=e=>Vr.exec(e)?.[1].toLowerCase()??"hr",Bt=e=>e.replace(Vr,""),Qe=Bt;function tr(e){if(typeof e!="string")throw Error("\u4E2D\u7EE7\u5730\u5740\u65E0\u6548");let t=ft(e.trim());if(t==="hr")return Vo(e.trim());let r=new URL(Bt(e.trim()));Pr(r);let n=r.pathname;if(!Go.test(n)||n.includes("//"))throw Error("\u4E2D\u7EE7\u8DEF\u5F84\u542B\u4E0D\u5B89\u5168\u5B57\u7B26");if(t==="bc"){if(n==="/"&&(n="/bc/"),!n.endsWith("/"))throw Error("bc \u4E2D\u7EE7\u8DEF\u5F84\u987B\u4EE5 / \u7ED3\u5C3E")}else n==="/"&&(n="");return`${t}+${r.origin}${n}`}function Dt(e){let t=Array.isArray(e)?e:typeof e=="string"?e.trim().split(/[\s,]+/):[],r=[...new Set(t.map(tr))],n=r.some(o=>ft(o)!=="hr");if(n&&r.some(o=>ft(o)==="hr"))throw Error("\u7ECF\u5178\u4E2D\u7EE7\uFF08wss://\uFF09\u4E0D\u80FD\u4E0E\u53BB\u4E2D\u5FC3\u5316\u4E2D\u7EE7\uFF08bc+ / nostr+ / mqtt+\uFF09\u6DF7\u7528");if(!r.length||r.length>(n?5:3))throw Error(n?"\u8BF7\u63D0\u4F9B 1 \u5230 5 \u4E2A\u4E0D\u540C\u7684\u53BB\u4E2D\u5FC3\u5316\u4E2D\u7EE7":"\u8BF7\u63D0\u4F9B 1 \u5230 3 \u4E2A\u4E0D\u540C\u4E2D\u7EE7");return r}var we=e=>e.some(t=>ft(t)!=="hr")?3:2,er={name:"PBKDF2",hash:"SHA-256",iterations:31e4},Gr=e=>At.encode(`private-talk/html-room/v1/wrap|${e}`);async function zr(e,t){if(typeof e!="string"||!e.length||At.encode(e).length>1024)throw Error("\u53E3\u4EE4\u957F\u5EA6\u65E0\u6548");let r=await crypto.subtle.importKey("raw",At.encode(e),"PBKDF2",!1,["deriveKey"]);return crypto.subtle.deriveKey({...er,salt:t},r,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}function lt(e){if(!e||![2,3].includes(e.version)||!/^[0-9a-f]{64}$/.test(e.roomId))throw Error("\u623F\u95F4\u7248\u672C\u6216\u5730\u5740\u9519\u8BEF");if(!Array.isArray(e.relays)||JSON.stringify(Dt(e.relays))!==JSON.stringify(e.relays))throw Error("\u4E2D\u7EE7\u5217\u8868\u683C\u5F0F\u9519\u8BEF");if(we(e.relays)!==e.version)throw Error("\u623F\u95F4\u7248\u672C\u4E0E\u4E2D\u7EE7\u7C7B\u578B\u4E0D\u7B26");let t=["version","relays","roomId","encrypted"];if(e.encrypted===!0){let r=[...t,"salt","iv","ciphertext","kdf"];if(Object.keys(e).some(n=>!r.includes(n))||!e.kdf||Object.keys(e.kdf).length!==3||Object.entries(er).some(([n,o])=>e.kdf[n]!==o))throw Error("\u4E0D\u652F\u6301\u7684 KDF \u914D\u7F6E");yt(e.salt,16),yt(e.iv,12),yt(e.ciphertext,48)}else{if(e.encrypted!==!1||Object.keys(e).some(r=>![...t,"master"].includes(r)))throw Error("\u914D\u7F6E\u5B57\u6BB5\u9519\u8BEF");yt(e.master,32)}return e}async function Zr(e,t){let r=Dt(e),n=tt(32),o=await jr(n),s={version:we(r),relays:r,roomId:o.roomId,encrypted:!!t};if(!t)return{...s,master:z(n)};let i=tt(16),c=tt(12),a=await zr(t,i),f=await crypto.subtle.encrypt({name:"AES-GCM",iv:c,additionalData:Gr(s.roomId)},a,n);return{...s,salt:z(i),iv:z(c),ciphertext:z(f),kdf:{...er}}}async function Xr(e,t){lt(e);let r=e.encrypted?new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:yt(e.iv,12),additionalData:Gr(e.roomId)},await zr(t,yt(e.salt,16)),yt(e.ciphertext,48))):yt(e.master,32),n=await jr(r);if(r.fill(0),n.roomId!==e.roomId)throw Error("\u623F\u95F4\u5730\u5740\u4E0E\u5BC6\u94A5\u4E0D\u5339\u914D");return n}async function Fr(e,t){let r=tt(12),n=await crypto.subtle.encrypt({name:"AES-GCM",iv:r,additionalData:At.encode(`private-talk/html-room/v1/keys|${e.roomId}`)},e.key,ye(At.encode(t)));return{iv:btoa(String.fromCharCode(...r)),ct:btoa(String.fromCharCode(...new Uint8Array(n)))}}async function Yr(e,t){let r=n=>Uint8Array.from(atob(n),o=>o.charCodeAt(0));return new TextDecoder().decode(me(new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:r(t.iv),additionalData:At.encode(`private-talk/html-room/v1/keys|${e.roomId}`)},e.key,r(t.ct)))))}var be=BigInt(4294967295),Wr=BigInt(32);function zo(e,t=!1){return t?{h:Number(e&be),l:Number(e>>Wr&be)}:{h:Number(e>>Wr&be)|0,l:Number(e&be)|0}}function Zo(e,t=!1){let r=e.length,n=new Uint32Array(r),o=new Uint32Array(r);for(let s=0;se?`"${e}" `:"";function mt(e,t=""){if(typeof e!="number")throw new TypeError(ve(t)+"expected number, got "+typeof e);if(!Number.isSafeInteger(e)||e<0)throw new RangeError(ve(t)+"expected integer >= 0, got "+e);return e}function Xo(e,t=""){if(typeof e!="boolean")throw new TypeError(ve(t)+"expected boolean, got type="+typeof e);return e}function pt(e,t,r=""){if(Jr(e)&&(t===void 0||e.length===t))return e;t!==void 0&&mt(t,"length");let n=Jr(e),o=t!==void 0?` of length ${t}`:"",s=n?`length=${e.length}`:`type=${typeof e}`,i=ve(r)+"expected Uint8Array"+o+", got "+s;throw n?new RangeError(i):new TypeError(i)}var Fo=(e,t)=>{if(e===null||typeof e!="object"||Array.isArray(e))throw new TypeError((t==="object"?"":`"${t}" `)+"expected object, got type="+typeof e)},Qr=(e,t)=>{Fo(e,t);let r=Object.getPrototypeOf(e);if(r!==Object.prototype&&r!==null)throw new TypeError(`"${t}" expected plain object`);if(Object.prototype.hasOwnProperty.call(e,"__proto__"))throw new TypeError(`"${t}.__proto__" is not allowed`)};function tn(e,t=!0){if(e.destroyed)throw new Error("hash was destroyed");if(t&&e.finished)throw new Error("digest() was already called")}function Yo(e,t){pt(e,void 0,"output");let r=t.outputLen;if(!(e.length>=r))throw new RangeError('"output" expected length >= '+r)}function ir(e){return new Uint32Array(e.buffer,e.byteOffset,Math.floor(e.byteLength/4))}function an(...e){for(let t=0;t>>8&65280|e>>>24&255}function Qo(e){for(let t=0;te:Qo;function fn(e,t,r="opts"){return Qr(e,"defaults"),t!==void 0&&Qr(t,r),Object.assign(Object.create(null),e,t)}function ln(e,t={}){if(typeof e!="function")throw new TypeError('"hashCons" expected function, got type='+typeof e);t=fn({},t,"info");let r=(o,s)=>e(s).update(o).digest(),n=e(void 0);return r.outputLen=n.outputLen,r.blockLen=n.blockLen,r.canXOF=n.canXOF,r.create=o=>e(o),Object.assign(r,t),Object.freeze(r)}function ts(e=32){mt(e,"bytesLength");let t=typeof globalThis=="object"?globalThis.crypto:null;if(typeof t?.getRandomValues!="function")throw new Error("crypto.getRandomValues must be defined");if(e>65536)throw new RangeError(`"bytesLength" expected <= 65536, got ${e}`);return t.getRandomValues(new Uint8Array(e))}var ke=e=>({oid:Uint8Array.from([6,9,96,134,72,1,101,3,4,2,e])}),es=BigInt(0),te=BigInt(1),rs=BigInt(2),ns=BigInt(7),os=BigInt(256),ss=BigInt(113),hn=[],un=[],dn=[];for(let e=0,t=te,r=1,n=0;e<24;e++){[r,n]=[n,(2*r+3*n)%5],hn.push(2*(5*n+r)),un.push((e+1)*(e+2)/2%64);let o=es;for(let s=0;s<7;s++)t=(t<>ns)*ss)%os,t&rs&&(o^=te<<(te<e<>>32-r,fs=(e,t,r)=>t<>>32-r,ls=(e,t,r)=>t<>>64-r,hs=(e,t,r)=>e<>>64-r,en=(e,t,r)=>r>32?ls(e,t,r):as(e,t,r),rn=(e,t,r)=>r>32?hs(e,t,r):fs(e,t,r),qt=new Uint32Array(10);function us(e,t=24){if(!(e instanceof Uint32Array))throw new TypeError('"s" expected Uint32Array(50), got type='+typeof e);if(e.length!==50)throw new RangeError('"s" expected Uint32Array(50), got length='+e.length);if(mt(t,"rounds"),t<1||t>24)throw new Error('"rounds" expected integer 1..24');for(let r=24-t;r<24;r++){for(let s=0;s<10;s++)qt[s]=e[s]^e[s+10]^e[s+20]^e[s+30]^e[s+40];for(let s=0;s<10;s+=2){let i=(s+8)%10,c=(s+2)%10,a=qt[c],f=qt[c+1],d=en(a,f,1)^qt[i],u=rn(a,f,1)^qt[i+1];for(let l=0;l<50;l+=10)e[s+l]^=d,e[s+l+1]^=u}let n=e[2],o=e[3];for(let s=0;s<24;s++){let i=un[s],c=en(n,o,i),a=rn(n,o,i),f=hn[s];n=e[f],o=e[f+1],e[f]=c,e[f+1]=a}for(let s=0;s<50;s+=10){let i=e[s],c=e[s+1],a=e[s+2],f=e[s+3];e[s]^=~e[s+2]&e[s+4],e[s+1]^=~e[s+3]&e[s+5],e[s+2]^=~e[s+4]&e[s+6],e[s+3]^=~e[s+5]&e[s+7],e[s+4]^=~e[s+6]&e[s+8],e[s+5]^=~e[s+7]&e[s+9],e[s+6]^=~e[s+8]&i,e[s+7]^=~e[s+9]&c,e[s+8]^=~i&a,e[s+9]^=~c&f}e[0]^=is[r],e[1]^=cs[r]}an(qt)}var yn=class mn{constructor(t,r,n,o=!1,s=24){st(this,"state");st(this,"pos",0);st(this,"posOut",0);st(this,"finished",!1);st(this,"state32");st(this,"destroyed",!1);st(this,"blockLen");st(this,"suffix");st(this,"outputLen");st(this,"canXOF");st(this,"enableXOF",!1);st(this,"rounds");if(mt(t,"blockLen"),mt(r,"suffix"),mt(s,"rounds"),Xo(o,"enableXOF"),this.blockLen=t,this.suffix=r,this.outputLen=n,this.enableXOF=o,this.canXOF=o,this.rounds=s,mt(n,"outputLen"),!(0=r?ir(t):void 0;for(let f=0;f=r){for(let u=0,l=f/4;u=n&&this.keccak();let i=Math.min(n-this.posOut,s-o);t.set(r.subarray(this.posOut,this.posOut+i),o),this.posOut+=i,o+=i}return t}xofInto(t){if(!this.enableXOF)throw new Error("XOF is not enabled");return this.writeInto(t)}xof(t){return mt(t),this.xofInto(new Uint8Array(t))}digestInto(t){if(Yo(t,this),this.finished)throw new Error("digest() was already called");this.writeInto(t.length===this.outputLen?t:t.subarray(0,this.outputLen)),this.destroy()}digest(){let t=new Uint8Array(this.outputLen);return this.digestInto(t),t}destroy(){this.destroyed=!0,an(this.state)}_cloneInto(t){let{blockLen:r,suffix:n,outputLen:o,rounds:s,enableXOF:i}=this;return t||(t=new mn(r,n,o,i,s)),t.blockLen=r,t.state32.set(this.state32),t.pos=this.pos,t.posOut=this.posOut,t.finished=this.finished,t.rounds=s,t.suffix=n,t.outputLen=o,t.enableXOF=i,t.canXOF=this.canXOF,t.destroyed=this.destroyed,t}},wn=(e,t,r,n={})=>ln(()=>new yn(t,e,r),n),ds=wn(6,136,32,ke(8)),ps=wn(6,72,64,ke(10)),bn=(e,t,r,n={})=>ln((o={})=>(o=fn({},o),new yn(t,e,o.dkLen===void 0?r:o.dkLen,!0)),n),ys=bn(31,168,16,ke(11)),gn=bn(31,136,32,ke(12));function rr(e,t="object"){if(e===null||typeof e!="object"||Array.isArray(e))throw new TypeError(t==="object"?"expected valid options object":`"${t}" expected object, got type=${typeof e}`);return e}function ms(e,t={},r={},n="object"){rr(e,n),rr(t,"fields"),rr(r,"optFields");function o(i,c,a){let f=n==="object"?`param "${String(i)}"`:`"${n}.${String(i)}"`,d=e[i];if(!Object.prototype.hasOwnProperty.call(e,i)&&(a?d!==void 0:c!=="function"))throw new TypeError(`${f} is invalid: expected own property`);if(a&&d===void 0)return;let u=typeof d;if(u!==c||d===null)throw new TypeError(`${f} is invalid: expected ${c}, got ${u}`)}let s=(i,c)=>Object.entries(i).forEach(([a,f])=>o(a,f,c));s(t,!1),s(r,!0)}function ne(e,t="n"){if(typeof e!="number")throw new TypeError(`wrong u32 integer "${t}": expected number, got type=${typeof e}`);if(!Number.isSafeInteger(e)||e<0||e>4294967295)throw new RangeError(`wrong u32 integer "${t}": expected 0..4294967295, got ${e}`);return e}function xn(e){return ne(e,"x"),(e&e-1)===0&&e!==0}function vn(e,t){if(ne(e),typeof t!="number")throw new TypeError('"bits" expected number, got type='+typeof t);if(!Number.isSafeInteger(t)||t<0||t>32)throw new Error(`expected integer 0 <= bits <= 32, got ${t}`);let r=0;for(let n=0;n>>=1)r=r<<1|e&1;return r>>>0}function En(e){return ne(e),31-Math.clz32(e)}function nn(e){if(!e||typeof e!="object"||typeof e.length!="number")throw new TypeError('"values" expected array-like, got type='+typeof e);let t=e.length;if(!xn(t))throw new Error("expected positive power-of-two length, got "+t);let r=En(t);for(let n=0;n{ms(t,{N:"number",roots:"object",dit:"boolean"},{invertButterflies:"boolean",skipStages:"number",brp:"boolean"},"coreOpts");let{N:r,roots:n,dit:o,invertButterflies:s=!1,skipStages:i=0,brp:c=!0}=t;ne(r,"coreOpts.N");let a=En(r);if(!xn(r))throw new Error("FFT: Polynomial size should be power of two");ne(i,"coreOpts.skipStages");let f=a===0?0:a-1;if(i>f)throw new Error(`FFT: wrong skipStages: expected 0 <= skipStages <= ${f}`);if(n.length!==r)throw new Error(`FFT: wrong roots length: expected ${r}, got ${n.length}`);let d=o!==s;return u=>{if(u.length!==r)throw new Error("FFT: wrong Polynomial length");o&&c&&nn(u);for(let l=0,h=1;l>1,B=r>>y;for(let $=0;${}){if(!Array.isArray(e))throw new TypeError(`"${t}" expected array, got type=${typeof e}`);for(let n=0;ntypeof o=="number"?o:o.bytesLen,n=t.reduce((o,s)=>o+r(s),0);return{bytesLen:n,encode:o=>{let s=new Uint8Array(n);for(let i=0,c=0;i{pt(o,n,e);let s=[];for(let i of t){let c=r(i),a=o.subarray(0,c);s.push(typeof i=="number"?a:i.decode(a)),o=o.subarray(c)}return s}}}function or(e,t){let r=e,n=t*r.bytesLen;return{bytesLen:n,encode:o=>{let s=ws(o,"u");if(s.length!==t)throw new RangeError(`vecCoder.encode: wrong length=${s.length}. Expected: ${t}`);let i=new Uint8Array(n);for(let c=0,a=0;c{pt(o,n);let s=[];for(let i=0;i32)throw new RangeError('"bits" expected <= 32, got '+e);return e===32?4294967295:~(-1<>>0}var bs=e=>{let{newPoly:t,N:r,Q:n,F:o,ROOT_OF_UNITY:s,brvBits:i,isKyber:c}=e,a=(g,v=n)=>{let S=g%v|0;return(S>=0?S|0:v+S|0)|0},f=(g,v=n)=>{let S=a(g,v)|0;return(S>v>>1?S-v|0:S)|0};function d(){let g=t(r);for(let v=0;v{throw new Error("not implemented")},h=c?{add:(g,v)=>{let S=g+v|0;return S>=n?S-n|0:S},sub:(g,v)=>{let S=g-v|0;return S<0?S+n|0:S},mul:(g,v)=>a((g|0)*(v|0))|0,inv:l}:{add:(g,v)=>a((g|0)+(v|0))|0,sub:(g,v)=>a((g|0)-(v|0))|0,mul:(g,v)=>a((g|0)*(v|0))|0,inv:l},y={N:r,roots:u,invertButterflies:!0,skipStages:c?1:0,brp:!1},m=on(h,{dit:!1,...y}),k=on(h,{dit:!0,...y}),B={encode:g=>m(g),decode:g=>{k(g);for(let v=0;vB.encode(g),decode:g=>B.decode(g)},bitsCoder:(g,v)=>{for(let I=0,_=0;I32&&cn(_),_%=8;let S=cn(g),R=g*(r/8);return{bytesLen:R,encode:I=>{let _=I,x=new Uint8Array(R);for(let M=0,U=0,L=0,b=0;M<_.length;M++)for(U|=(v.encode(_[M])&S)<=8;L-=8,U>>=8)x[b++]=U&255;return x},decode:I=>{let _=t(r);for(let x=0,M=0,U=0,L=0;x=g;U-=g,M>>=g)_[L++]=v.decode(M&S);return _}}}}},gs=e=>(t,r)=>{r||(r=e.blockLen);let n=new Uint8Array(t.length+2);n.set(t);let o=t.length,s=new Uint8Array(r),i=e.create({}),c=0,a=0;return{stats:()=>({calls:c,xofs:a}),get:(f,d)=>(n[o+0]=f,n[o+1]=d,i.destroy(),i=e.create({}).update(n),c++,()=>(a++,i.xofInto(s))),clean:()=>{i.destroy(),Y(s,n)}}},xs=gs(ys);var J=256,W=3329,vs=3303,Es=17,nt=bs({N:J,Q:W,F:vs,ROOT_OF_UNITY:Es,newPoly:e=>new Uint16Array(e),brvBits:7,isKyber:!0}),ks=Object.freeze({512:Object.freeze({N:J,Q:W,K:2,ETA1:3,ETA2:2,du:10,dv:4,RBGstrength:128}),768:Object.freeze({N:J,Q:W,K:3,ETA1:2,ETA2:2,du:10,dv:4,RBGstrength:192}),1024:Object.freeze({N:J,Q:W,K:4,ETA1:2,ETA2:2,du:11,dv:5,RBGstrength:256})}),Ss=e=>{if(e>=12)return{encode:r=>r,decode:r=>r>=W?r-W:r};let t=2**(e-1);return{encode:r=>((r<r*W+t>>>e}},As=e=>nt.bitsCoder(e,e===12?{encode:t=>t,decode:t=>t>=W?t-W:t}:{encode:t=>t,decode:t=>t}),ee=e=>e===12?As(12):nt.bitsCoder(e,Ss(e));function Ut(e,t){let r=e,n=t;for(let o=0;o=W?s-W:s}}function Bs(e,t){let r=e,n=t;for(let o=0;o>1)];o&1&&(s=-s);let{c0:i,c1:c}=Ts(r[2*o+0],r[2*o+1],n[2*o+0],n[2*o+1],s);r[2*o+0]=i,r[2*o+1]=c}return r}function sr(e){let t=e,r=new Uint16Array(J);for(let n=0;n>0|o[s+1]<<8)&4095,c=(o[s+1]>>4|o[s+2]<<4)&4095;i{let r=new Uint16Array(J),n=ir(e);Ee(n);let o=0;for(let s=0,i=0,c=0,a=0;s>=1,o+=1,o===t?(a=c,c=0):o===2*t&&(r[i++]=nt.mod(a-c),c=0,o=0)}if(Ee(n),o)throw new Error(`sampleCBD: leftover bits: ${o}`);return r};function re(e,t,r,n){return Is(e(n*J/4,t,r),n)}var Os=e=>{let t=e,{K:r,PRF:n,XOF:o,HASH512:s,ETA1:i,ETA2:c,du:a,dv:f}=t,d=ee(1),u=ee(f),l=ee(a),h=xe("publicKey",or(ee(12),r),32),y=or(ee(12),r),m=xe("ciphertext",or(l,r),u),k=xe("seed",32,32),B=($,g,v,S)=>{let R=[];for(let U=0;U{dt($,32,"seed");let g=new Uint8Array(33);g.set($),g[32]=r;let v=s(g),[S,R]=k.decode(v),I=[],_=[];for(let U=0;U{let[S,R]=h.decode($),I=o(R),_=B(S,(x,M)=>sr(I.get(x,M)),g,v);return I.clean(),_},prepare:$=>{let[g,v]=h.decode($),S=o(v),R=[];for(let I=0;IB(g.map(x=>x.slice()),(x,M)=>R[x*r+M].slice(),I,_),clean:()=>Y(g,R)}},decrypt:($,g)=>{let[v,S]=m.decode($),R=y.decode(g),I=new Uint16Array(J);for(let x=0;x{let m=h.subarray(0,384*t.K),k=i.encode(i.decode(sn(m))),B=jt(k,m);if(Y(k),!B)throw new Error(`ML-KEM.${y}: wrong publicKey modulus`)},l=Object.freeze({...c,seed:64,msg:f,msgRand:f,secretKey:a.bytesLen});return Object.freeze({info:Object.freeze({type:"ml-kem"}),lengths:l,keygen:h=>{let y=h===void 0,m=y?nr(d):h,k,B;try{dt(m,d,"seed");let $=r.keygen(m.subarray(0,32)),g=$.publicKey;k=$.secretKey,B=n(g);let v=a.encode([k,g,B,m.subarray(32)]);return{publicKey:g,secretKey:v}}finally{k!==void 0&&Y(k),B!==void 0&&Y(B),y&&Y(m)}},getPublicKey:h=>{let[y,m,k,B]=a.decode(h);return Uint8Array.from(m)},encapsulate:(h,y)=>{let m=y===void 0,k=m?nr(f):y,B;try{return dt(h,c.publicKey,"publicKey"),dt(k,f,"message"),u(h,"encapsulate"),B=o.create().update(k).update(n(h)).digest(),{cipherText:r.encrypt(h,k,B.subarray(32,64)),sharedSecret:B.subarray(0,32)}}finally{B!==void 0&&Y(B.subarray(32)),m&&Y(k)}},decapsulate:(h,y)=>{dt(y,a.bytesLen,"secretKey"),dt(h,c.cipherText,"cipherText");let m=a.bytesLen-96,k=m+32,B=n(y.subarray(m/2,k));if(!jt(B,y.subarray(k,k+32)))throw new Error("invalid secretKey: hash check failed");let[$,g,v,S]=a.decode(y),R=r.decrypt(h,$),I=o.create().update(R).update(v).digest(),_=I.subarray(0,32),x=r.encrypt(g,R,I.subarray(32,64)),M=jt(h,x),U=s.create({dkLen:32}).update(S).update(h).digest();return Y(R,x,I.subarray(32),M?U:_),M?_:U},prepare:h=>{dt(h,c.publicKey,"publicKey"),u(h,"prepare");let y=sn(h),m=n(y),k=r.prepare(y);return Object.freeze({publicKey:y,encapsulate:B=>{let $=B===void 0,g=$?nr(f):B,v;try{return dt(g,f,"message"),v=o.create().update(g).update(m).digest(),{cipherText:k.encrypt(g,v.subarray(32,64)),sharedSecret:v.subarray(0,32)}}finally{v!==void 0&&Y(v.subarray(32)),$&&Y(g)}},decapsulate:(B,$)=>{dt($,a.bytesLen,"secretKey"),dt(B,c.cipherText,"cipherText");let[g,v,S,R]=a.decode($);if(!jt(v,y)||!jt(S,m))throw new Error("ML-KEM.decapsulate: secretKey does not match prepared publicKey");let I=r.decrypt(B,g),_=o.create().update(I).update(m).digest(),x=_.subarray(0,32),M=k.encrypt(I,_.subarray(32,64)),U=jt(B,M),L=s.create({dkLen:32}).update(R).update(B).digest();return Y(I,M,_.subarray(32),U?L:x),U?x:L},clean:k.clean})}})}function _s(e,t,r){return gn.create({dkLen:e}).update(t).update(new Uint8Array([r])).digest()}var Us={HASH256:ds,HASH512:ps,KDF:gn,XOF:xs,PRF:_s},Ls=e=>Hs({...Us,...e}),Se=Ls(ks[768]);var at=new TextEncoder,cr=new TextDecoder,_n={name:"ECDH",namedCurve:"P-256"},kn={name:"ECDSA",namedCurve:"P-256"},Ae={name:"ECDSA",hash:"SHA-256"},Cs=2e3,Rs=100,$s=600*1e3,Sn=60*1e3,Ks=20*1e3,Ns=4,Ms=4e3,An=1580,Ds=1452,qs=/^(\d{1,9})\.(\d{1,9})$/,Be=/^[A-Za-z0-9+/]+={0,2}$/,js=/,"s":"([A-Za-z0-9+/]{86}==)"\}$/,bt=e=>btoa(String.fromCharCode(...new Uint8Array(e))),wt=e=>Uint8Array.from(atob(e),t=>t.charCodeAt(0)),Bn=e=>Array.from(new Uint8Array(e),t=>t.toString(16).padStart(2,"0")).join("");async function Tn(e,t){let r=await crypto.subtle.importKey("raw",e,{name:"HMAC",hash:"SHA-256"},!1,["sign"]);return new Uint8Array(await crypto.subtle.sign("HMAC",r,new Uint8Array([t])))}async function ar(e){return{mk:await Tn(e,1),next:await Tn(e,2)}}async function In(e,t){return crypto.subtle.importKey("raw",e,"AES-GCM",!1,t)}async function Te(e,t,r){let n=crypto.getRandomValues(new Uint8Array(12)),o=await crypto.subtle.encrypt({name:"AES-GCM",iv:n,additionalData:at.encode(r)},e,t);return{iv:bt(n),ct:bt(o)}}async function fr(e,{iv:t,ct:r},n){return new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:wt(t),additionalData:at.encode(n)},e,wt(r)))}var Ps=(e,t)=>{let r=new Uint8Array(e.length+t.length);return r.set(e),r.set(t,e.length),r};async function On(){let e=await crypto.subtle.generateKey(_n,!1,["deriveBits"]);return{pair:e,epk:bt(await crypto.subtle.exportKey("raw",e.publicKey))}}var Hn=e=>`pt31a|${e.from}|${e.id}|${e.t}|${e.epk}|${e.hello?1:0}|${JSON.stringify(e.w)}`,Ie=class e{static async create(t,r,n){let o=new e;return o.key=t,o.bind=r,o.me=n,o.signing=await crypto.subtle.generateKey(kn,!1,["sign","verify"]),o.spk=bt(await crypto.subtle.exportKey("raw",o.signing.publicKey)),o.kem=Se.keygen(),o.kpk=bt(o.kem.publicKey),o.ecdh=await On(),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(t=this.mine.epoch+1,r=Date.now()){this.mine&&r-this.ecdhSince>=Ks&&(this.oldEcdh=[{ecdh:this.ecdh,until:r+Sn},...this.oldEcdh].slice(0,Ns),this.ecdh=await On(),this.ecdhSince=r),this.oldEcdh=this.oldEcdh.filter(n=>n.until>r),this.mine={epoch:t,ck:crypto.getRandomValues(new Uint8Array(32)),c:0,since:r,handed:new Map};for(let n of this.peers.values())n.sent=-1,n.gotAt=null}due(t=Date.now()){return this.mine.c>=Rs||t-this.mine.since>=$s}forget(t){this.peers.delete(t)}async pairKey(t,r,n=!1){let o=n?`${t.epk}|${r.kcIn??r.kc}`:t.epk;if(r.wraps.has(o))return r.wraps.get(o);let s=await crypto.subtle.importKey("raw",wt(r.epk),_n,!1,[]),i=new Uint8Array(await crypto.subtle.deriveBits({name:"ECDH",public:s},t.pair.privateKey,256)),c=await crypto.subtle.importKey("raw",n?Ps(i,r.ss):i,"HKDF",!1,["deriveKey"]),a=at.encode(`private-talk/${n?"v3.2":"v3"}/pair|${[t.epk,r.epk].sort().join("|")}`),f=await crypto.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:this.bind,info:a},c,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"]);return r.wraps.set(o,f),f}pqFor(t,r){if(r.v!==32||this.mer!==this.me&&this.peers.has(r)&&this.peers.get(r).sent!==this.mine.epoch)}async announce(t,r,n=Date.now()){return(await this.announceAll(t,r,n,1/0))[0]}async announceAll(t,r,n=Date.now(),o=Ms){let s=this.mine;for(let[y,m]of s.handed)n-m.at>Sn&&s.handed.delete(y);let i=[...this.peers].some(([y,m])=>y{let y=c&&u?{q:1,k:this.kpk,...f}:{q:1,...f},m={kind:"keys",v:32,from:this.me,id:Bn(crypto.getRandomValues(new Uint8Array(8))),t:n,epk:this.epk,spk:this.spk,hello:!!r&&u,w:y};m.sig=bt(await crypto.subtle.sign(Ae,this.signing.privateKey,at.encode(Hn(m)))),a.push(JSON.stringify(m)),f={},d=0,u=!1},h=()=>430+(c&&u?An+8:0);for(let y of t){let m=this.peers.get(y);if(!m)continue;let{pq:k,can:B}=this.pqFor(y,m);if(!B)continue;let $=m.gotAt!=null&&s.handed.has(m.gotAt),g=$?m.gotAt:s.c,v=$?s.handed.get(g).ck:s.ck;$||(m.gotAt=g,s.handed.has(g)||s.handed.set(g,{ck:v,at:n}));let S=at.encode(JSON.stringify({e:s.epoch,ck:bt(v),c:g})),R=k?{...await Te(await this.pairKey(this.ecdh,m,!0),S,`pt32w|${this.me}|${y}|${m.epk}`),q:1,...m.kc&&{kc:m.kc}}:await Te(await this.pairKey(this.ecdh,m),S,`pt3w|${this.me}|${y}|${m.epk}`),I=JSON.stringify(R).length+8;Object.keys(f).length&&h()+d+I>o&&await l(),f[y]=R,d+=I,m.sent=this.mine.epoch}return(u||Object.keys(f).length)&&await l(),a}async receive(t,r){if(!t||t.kind!=="keys"||t.from!==r||r===this.me||typeof t.epk!="string"||!Be.test(t.epk)||t.epk.length>120||!t.w||typeof t.w!="object")return null;let n=typeof t.spk=="string",o=null;if(n){if(!Be.test(t.spk)||t.spk.length>120||typeof t.sig!="string")throw new Error("bad announcement");let c=this.peers.get(r);if(o=c?.spk===t.spk?c.verify:await crypto.subtle.importKey("raw",wt(t.spk),kn,!1,["verify"]),!await crypto.subtle.verify(Ae,o,wt(t.sig),at.encode(Hn(t))))throw new Error("bad signature")}let s=this.peers.get(r);s&&(s.spk??null)!==(n?t.spk:null)&&(s.spk&&this.changed.push(r),s=null),s?s.epk!==t.epk&&(n?(s.epk=t.epk,s.wraps=new Map):(s={epk:t.epk,spk:null,verify:null,wraps:new Map,recv:null,prev:null,sent:-1,gotAt:null,v:30},this.peers.set(r,s))):(s={epk:t.epk,spk:n?t.spk:null,verify:o,wraps:new Map,recv:null,prev:null,sent:-1,gotAt:null,v:n?31:30,kpk:null,ss:null,kc:null,kcIn:null,pqIn:!1},this.peers.set(r,s)),n&&t.w.q===1&&s.v!==32&&(s.v=32,s.sent=-1),n&&typeof t.w.k=="string"&&t.w.k.length===An&&Be.test(t.w.k)&&t.w.k!==s.kpk&&(Object.assign(s,{kpk:t.w.k,ss:null,kc:null,kcIn:null,v:32}),s.wraps=new Map);let i=t.w[this.me];if(i&&typeof i.iv=="string"&&typeof i.ct=="string"){let c=i.q===1;c&&n&&(s.v=32),c&&ru.ecdh)],f=null;if(!c||s.ss)for(let u of a)try{f=JSON.parse(cr.decode(await fr(await this.pairKey(u,s,c),i,`${c?"pt32w":"pt3w"}|${r}|${this.me}|${u.epk}`)));break}catch{}f&&c&&this.me=0&&d.length===32&&(!s.recv||f.e>s.recv.epoch)?(s.prev=s.recv,s.recv={epoch:f.e,ck:d,c:f.c},s.pqIn=c):f&&c&&s.recv&&f.e===s.recv.epoch&&(s.pqIn=!0)}return t.hello&&(s.sent=-1),s}takeChanged(){let t=this.changed;return this.changed=[],t}ready(t,r=!1){return t.every(n=>n===this.me||this.peers.get(n)?.sent===this.mine.epoch&&(!r||this.peers.get(n).v>=31))}quantumSafe(t){return t.every(r=>r===this.me||this.peers.get(r)?.v===32&&this.peers.get(r).ss&&this.peers.get(r).pqIn)}older(t){return t.filter(r=>r!==this.me&&this.peers.get(r)?.v===30)}async pairSecret(t,r){let n=this.peers.get(t);if(!n?.ss)throw new Error("no pair secret");let o=await crypto.subtle.importKey("raw",n.ss,"HKDF",!1,["deriveKey"]),s=at.encode(`private-talk/v3.2/for|${r}|${Math.min(t,this.me)}|${Math.max(t,this.me)}`);return crypto.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:this.bind,info:s},o,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}async sealFor(t,r,n){return Te(await this.pairSecret(t,n),at.encode(r),`pt32f|${this.me}|${t}|${n}`)}async openFrom(t,r,n){return cr.decode(await fr(await this.pairSecret(t,n),r,`pt32f|${t}|${this.me}|${n}`))}async seal(t){let r=this.mine,n=`${r.epoch}.${r.c}`,{mk:o,next:s}=await ar(r.ck);r.ck=s,r.c++;let i=bt(await crypto.subtle.sign(Ae,this.signing.privateKey,at.encode(`pt31m|${this.me}|${n}|${t}`))),c=`${t.slice(0,-1)},"s":"${i}"}`;return{...await Te(await In(o,["encrypt"]),ye(at.encode(c)),`pt3|${this.me}|${n}`),h:n}}async open(t,r){let n=qs.exec(t.h??""),o=this.peers.get(r);if(!n||!o?.recv)throw new Error("no key");let s=Number(n[1]),i=Number(n[2]),c=s===o.recv.epoch?"recv":s===o.prev?.epoch?"prev":null,a=o[c];if(!c||iCs)throw new Error("out of step");let f=a.ck;for(let m=a.c;m=31){if(!y)throw new Error("unsigned");if(l=`${l.slice(0,y.index)}}`,!await crypto.subtle.verify(Ae,o.verify,wt(y[1]),at.encode(`pt31m|${r}|${t.h}|${l}`)))throw new Error("bad signature");h=!0}return o[c]={epoch:s,ck:u,c:i+1},{text:l,missed:i-a.c,signed:h}}async safetyCode(t){let r=i=>i===this.me?this.spk:this.peers.get(i)?.spk??this.peers.get(i)?.epk??"?",n=t.map(i=>`${i}:${r(i)}`).sort().join("|"),o=new Uint8Array(await crypto.subtle.digest("SHA-256",at.encode(`private-talk/v3.1/safety|${Bn(this.bind)}|${n}`))),s=0n;for(let i of o.subarray(0,12))s=s<<8n|BigInt(i);return(s%10n**20n).toString().padStart(20,"0").replace(/(\d{5})(?=\d)/g,"$1 ")}};function He(e){let t=e.trim();return!t||Array.from(t).length>500||new TextEncoder().encode(t).byteLength>2048?null:t}var Oe=600*1e3,Un=/^[0-9a-f]{16}$/;function Ln(e,t,r=Date.now(),n,o,s){let i=Array.from(crypto.getRandomValues(new Uint8Array(8)),a=>a.toString(16).padStart(2,"0")).join(""),c=Number.isInteger(s)&&s>=0&&s<=5;return JSON.stringify({from:e,id:i,t:r,text:t,...n&&{g:n},...o&&{f:o},...c&&{q:s}})}function Cn(e,t,r,n=Date.now()){let o;try{o=JSON.parse(e)}catch{return null}if(!o||typeof o!="object"||typeof o.text!="string"||typeof o.id!="string"||!Un.test(o.id)||!Number.isInteger(o.from)||o.from<1||o.from>25||o.from!==t.from||!Number.isFinite(o.t)||!Number.isFinite(t.at)||Math.abs(n-o.t)>Oe||Math.abs(t.at-o.t)>Oe)return null;let s=Number.isInteger(o.q)&&o.q>=0&&o.q<=5;if(s){if(o.text!==""&&He(o.text)!==o.text)return null}else if(He(o.text)!==o.text)return null;for(let[f,d]of r)Math.abs(n-d)>2*Oe&&r.delete(f);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,c=o.f&&typeof o.f=="object"&&!Array.isArray(o.f)?o.f:void 0,a=s?o.q:void 0;return{text:o.text,id:o.id,g:i,f:c,q:a}}function lr(e,t,r,n=Date.now(),o="keys"){let s;try{s=JSON.parse(e)}catch{return null}return!s||typeof s!="object"||s.kind!==o||typeof s.id!="string"||!Un.test(s.id)||!Number.isInteger(s.from)||s.from!==t.from||!Number.isFinite(s.t)||Math.abs(n-s.t)>Oe||r.has(s.id)?null:(r.set(s.id,s.t),s)}var Vs=()=>new Map,_e=class{constructor(t,r,{storage:n=Vs(),emit:o=()=>{}}={}){Object.assign(this,{d:t,config:r,store:n,emit:o,deviceId:z(tt(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(t){return this.serial=this.serial.then(()=>{if(!this.stopped)return t()}).catch(()=>this.emit("notice","\u6536\u5230\u65E0\u6CD5\u9A8C\u8BC1\u7684\u6570\u636E\uFF0C\u5DF2\u5FFD\u7565\u3002")),this.serial}send(t){!this.stopped&&this.ws?.readyState===1&&this.ws.send(JSON.stringify(t))}async announce(t=!1){if(!this.group)return;let r=this.group.owed(this.present);if(!(!t&&!r.length))for(let n of await this.group.announceAll(r,t))this.send({type:"key",...await Fr(this.d,n)})}ready(){return!this.stopped&&this.group&&this.present.length>1&&this.group.ready(this.present,!0)&&this.group.quantumSafe(this.present)}async incoming(t){if(this.stopped)return;if(t.type==="pong"){this.lastPong=Date.now();return}if(t.type==="wait"){this.nextSend=Date.now()+Math.min(3e4,Math.max(1e3,t.ms||6e3));return}if(t.type==="hello"){this.resume=t.resume,(!this.group||this.group.me!==t.you)&&(this.group=await Ie.create(this.d.key,this.d.bind,t.you),this.own.clear(),this.received.clear(),this.receipts=[],this.present=[]),await this.announce(!0);return}if(!this.group)return;if(t.type==="members"){if(!Array.isArray(t.members)||t.members.some(i=>!Number.isInteger(i)||i<1||i>25))return;let s=this.present.filter(i=>!t.members.includes(i));for(let i of s){this.group.forget(i),this.config.version===3&&this.pins.delete(i);for(let c of this.received.keys())c.startsWith(`${i}|`)&&this.received.delete(c)}this.present=t.members,s.length&&await this.group.rekey(),await this.announce(!0),this.emit("presence",this.present.length),await this.update();return}if(t.from===this.group.me){let s=this.own.get(t.h);t.type==="msg"&&s&&s.frame.iv===t.iv&&s.frame.ct===t.ct&&(s.echo=!0,this.emit("delivery",{id:s.id,text:"\u5DF2\u63D0\u4EA4\u4E2D\u7EE7"}));return}if(!this.present.includes(t.from))return;if(t.type==="key"){let s=lr(await Yr(this.d,t),t,this.seen);if(!s||s.v!==32||!s.spk)return;let i=this.pins.get(t.from),c=await this.group.receive(s,t.from);if(!c)return;if(i&&i!==c.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(t.from,c.spk),c?.lost&&(c.sent=-1),await this.announce(),await this.update();return}if(!["msg","seen"].includes(t.type)||!t.h)return;let r=`${t.from}|${t.h}`,n=this.received.get(r);if(n){t.type==="msg"&&n.iv===t.iv&&n.ct===t.ct&&this.receipts.length<100&&this.receipts.push([t.from,t.h]);return}let o=await this.group.open(t,t.from);if(!(!o.signed||this.group.peers.get(t.from)?.v!==32))if(t.type==="seen"){let s=lr(o.text,t,this.seen,Date.now(),"seen");if(s&&Array.isArray(s.l)&&s.l.length<=25)for(let i of s.l){if(!Array.isArray(i)||i[0]!==this.group.me)continue;let c=this.own.get(i[1]);c&&c.targets.includes(t.from)&&(c.confirmed.add(t.from),this.emit("delivery",{id:c.id,text:`\u5BA2\u6237\u7AEF\u786E\u8BA4 ${c.confirmed.size}/${c.targets.length}`}))}}else{let s=Cn(o.text,t,this.seen);if(!s||s.g||s.f)return;for(this.received.set(r,{iv:t.iv,ct:t.ct});this.received.size>200;)this.received.delete(this.received.keys().next().value);this.emit("message",{id:s.id,from:t.from,text:s.text}),this.receipts.length<100&&this.receipts.push([t.from,t.h])}}async update(){this.ready()&&this.emit("ready",await this.group.safetyCode(this.present)),await this.flush()}queue(t){if(this.stopped||(t=He(t),!t||this.queued.length>=20))return!1;let r=z(tt(8));return this.queued.push({id:r,text:t}),this.emit("message",{id:r,from:"\u6211",text:t}),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 t=JSON.stringify({kind:"seen",id:z(tt(8)),from:this.group.me,t:Date.now(),l:this.receipts.splice(0,25)});this.send({type:"seen",...await this.group.seal(t)})}if(this.queued.length&&Date.now()>=(this.nextSend??0)){let t=this.queued.shift(),r={type:"msg",...await this.group.seal(Ln(this.group.me,t.text))};for(this.own.set(r.h,{...t,frame:r,targets:this.present.filter(n=>n!==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[t,r]of this.seen)Date.now()-r>12e5&&this.seen.delete(t);for(;this.seen.size>4096;)this.seen.delete(this.seen.keys().next().value)}}async retryPending(){for(let t of this.own.values())!t.echo&&Date.now()-t.at>8e3&&(t.retries??0)<3&&(this.send(t.frame),t.at=Date.now(),t.retries=(t.retries??0)+1),!t.echo&&(t.retries??0)>=3&&this.emit("delivery",{id:t.id,text:"\u4E2D\u7EE7\u63D0\u4EA4\u672A\u786E\u8BA4\uFF0C\u8BF7\u6838\u5B9E\u540E\u91CD\u53D1"})}resetRelay(){for(let t of this.own.values())(t.confirmed.sizenew WebSocket(d,u)}){Object.assign(this,{relays:t,roomId:r,protocols:n,onactive:o,onmessage:s,onnotice:i,onfatal:c,ondisconnect:a,createSocket:f}),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 t=this.active;if(t?.ready){if(Date.now()-t.lastPong>65e3){this.fail(t);return}if(Date.now()-t.lastPing>=25e3){t.lastPing=Date.now();try{t.ws.send(JSON.stringify({type:"ping"}))}catch{this.fail(t)}}}},1e3)}connect(t,r=!1){if(this.stopped)return;let n={index:t,probe:r,frames:[],ready:!1,retired:!1};r?this.probe=n:this.pending=n;try{n.ws=this.createSocket(`${this.relays[t]}/hr/${this.roomId}`,this.protocols(t))}catch{this.fail(n);return}n.timeout=setTimeout(()=>this.fail(n),1e4),n.ws.onerror=()=>{!n.retired&&!n.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")},n.ws.onclose=o=>this.fail(n,o.code),n.ws.onmessage=o=>{if(n.retired||this.stopped||typeof o.data!="string"||o.data.length>7e3)return;let s;try{s=JSON.parse(o.data)}catch{return}if(!(!s||typeof s!="object")){if(n.ready){if(this.active!==n)return;s.type==="pong"&&(n.lastPong=Date.now()),this.onmessage(s);return}if(n.frames.length>=50){this.fail(n);return}n.frames.push(s),s.type==="hello"&&!n.activating&&Number.isInteger(s.you)&&s.you>=1&&s.you<=25&&typeof s.resume=="string"&&/^[0-9a-f]{32}$/.test(s.resume)&&(n.activating=!0,this.activate(n).catch(()=>this.fail(n)))}}}async activate(t){let r=this.active,n=this.lastIndex!==-1&&this.lastIndex!==t.index;if(this.active=t,this.probe===t&&(this.probe=null),this.pending===t&&(this.pending=null),r&&r!==t&&this.retire(r),await this.onactive(t.ws,t.index,n),!(t.retired||this.stopped||this.active!==t)){clearTimeout(t.timeout),this.lastIndex=t.index,this.backoff=1e3,t.ready=!0,t.lastPong=t.lastPing=Date.now();for(let o of t.frames)this.onmessage(o);t.frames=[]}}retire(t){if(!(!t||t.retired)){t.retired=!0,clearTimeout(t.timeout);try{t.ws?.close(4e3)}catch{}}}fail(t,r){if(!(t.retired||this.stopped)){if(this.retire(t),this.probe===t){this.probe=null;return}if(this.pending===t&&(this.pending=null),this.active===t&&(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((t.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)}};var Lt=typeof globalThis=="object"&&"crypto"in globalThis?globalThis.crypto:void 0;function Ct(e){return e instanceof Uint8Array||ArrayBuffer.isView(e)&&e.constructor.name==="Uint8Array"}function oe(e){if(!Number.isSafeInteger(e)||e<0)throw new Error("positive integer expected, got "+e)}function ht(e,...t){if(!Ct(e))throw new Error("Uint8Array expected");if(t.length>0&&!t.includes(e.length))throw new Error("Uint8Array expected of length "+t+", got length="+e.length)}function Le(e){if(typeof e!="function"||typeof e.create!="function")throw new Error("Hash should be wrapped by utils.createHasher");oe(e.outputLen),oe(e.blockLen)}function Vt(e,t=!0){if(e.destroyed)throw new Error("Hash instance has been destroyed");if(t&&e.finished)throw new Error("Hash#digest() has already been called")}function $n(e,t){ht(e);let r=t.outputLen;if(e.length>>t}var Kn=typeof Uint8Array.from([]).toHex=="function"&&typeof Uint8Array.fromHex=="function",Gs=Array.from({length:256},(e,t)=>t.toString(16).padStart(2,"0"));function xt(e){if(ht(e),Kn)return e.toHex();let t="";for(let r=0;r=gt._0&&e<=gt._9)return e-gt._0;if(e>=gt.A&&e<=gt.F)return e-(gt.A-10);if(e>=gt.a&&e<=gt.f)return e-(gt.a-10)}function $t(e){if(typeof e!="string")throw new Error("hex string expected, got "+typeof e);if(Kn)return Uint8Array.fromHex(e);let t=e.length,r=t/2;if(t%2)throw new Error("hex string expected, got unpadded hex of length "+t);let n=new Uint8Array(r);for(let o=0,s=0;oe().update(se(n)).digest(),r=e();return t.outputLen=r.outputLen,t.blockLen=r.blockLen,t.create=()=>e(),t}function Kt(e=32){if(Lt&&typeof Lt.getRandomValues=="function")return Lt.getRandomValues(new Uint8Array(e));if(Lt&&typeof Lt.randomBytes=="function")return Uint8Array.from(Lt.randomBytes(e));throw new Error("crypto.getRandomValues must be defined")}function zs(e,t,r,n){if(typeof e.setBigUint64=="function")return e.setBigUint64(t,r,n);let o=BigInt(32),s=BigInt(4294967295),i=Number(r>>o&s),c=Number(r&s),a=n?4:0,f=n?0:4;e.setUint32(t+a,i,n),e.setUint32(t+f,c,n)}function Mn(e,t,r){return e&t^~e&r}function Dn(e,t,r){return e&t^e&r^t&r}var $e=class extends Pt{constructor(t,r,n,o){super(),this.finished=!1,this.length=0,this.pos=0,this.destroyed=!1,this.blockLen=t,this.outputLen=r,this.padOffset=n,this.isLE=o,this.buffer=new Uint8Array(t),this.view=Ce(this.buffer)}update(t){Vt(this),t=se(t),ht(t);let{view:r,buffer:n,blockLen:o}=this,s=t.length;for(let i=0;io-i&&(this.process(n,0),i=0);for(let u=i;ud.length)throw new Error("_sha2: outputLen bigger than state");for(let u=0;u>>3,m=ut(h,17)^ut(h,19)^h>>>10;Tt[u]=m+Tt[u-7]+y+Tt[u-16]|0}let{A:n,B:o,C:s,D:i,E:c,F:a,G:f,H:d}=this;for(let u=0;u<64;u++){let l=ut(c,6)^ut(c,11)^ut(c,25),h=d+l+Mn(c,a,f)+Zs[u]+Tt[u]|0,m=(ut(n,2)^ut(n,13)^ut(n,22))+Dn(n,o,s)|0;d=f,f=a,a=c,c=i+h|0,i=s,s=o,o=n,n=h+m|0}n=n+this.A|0,o=o+this.B|0,s=s+this.C|0,i=i+this.D|0,c=c+this.E|0,a=a+this.F|0,f=f+this.G|0,d=d+this.H|0,this.set(n,o,s,i,c,a,f,d)}roundClean(){Rt(Tt)}destroy(){this.set(0,0,0,0,0,0,0,0),Rt(this.buffer)}};var Gt=Nn(()=>new Ke);var Ne=class extends Pt{constructor(t,r){super(),this.finished=!1,this.destroyed=!1,Le(t);let n=se(r);if(this.iHash=t.create(),typeof this.iHash.update!="function")throw new Error("Expected instance of class which extends utils.Hash");this.blockLen=this.iHash.blockLen,this.outputLen=this.iHash.outputLen;let o=this.blockLen,s=new Uint8Array(o);s.set(n.length>o?t.create().update(n).digest():n);for(let i=0;inew Ne(e,t).update(r).digest();hr.create=(e,t)=>new Ne(e,t);var pr=BigInt(0),dr=BigInt(1);function ie(e,t=""){if(typeof e!="boolean"){let r=t&&`"${t}"`;throw new Error(r+"expected boolean, got type="+typeof e)}return e}function It(e,t,r=""){let n=Ct(e),o=e?.length,s=t!==void 0;if(!n||s&&o!==t){let i=r&&`"${r}" `,c=s?` of length ${t}`:"",a=n?`length=${o}`:`type=${typeof e}`;throw new Error(i+"expected Uint8Array"+c+", got "+a)}return e}function ce(e){let t=e.toString(16);return t.length&1?"0"+t:t}function qn(e){if(typeof e!="string")throw new Error("hex string expected, got "+typeof e);return e===""?pr:BigInt("0x"+e)}function Et(e){return qn(xt(e))}function yr(e){return ht(e),qn(xt(Uint8Array.from(e).reverse()))}function zt(e,t){return $t(e.toString(16).padStart(t*2,"0"))}function mr(e,t){return zt(e,t).reverse()}function G(e,t,r){let n;if(typeof t=="string")try{n=$t(t)}catch(s){throw new Error(e+" must be hex string or Uint8Array, cause: "+s)}else if(Ct(t))n=Uint8Array.from(t);else throw new Error(e+" must be hex string or Uint8Array");let o=n.length;if(typeof r=="number"&&o!==r)throw new Error(e+" of length "+r+" expected, got "+o);return n}var ur=e=>typeof e=="bigint"&&pr<=e;function ae(e,t,r){return ur(e)&&ur(t)&&ur(r)&&t<=e&&epr;e>>=dr,t+=1);return t}var Ot=e=>(dr<new Uint8Array(h),o=h=>Uint8Array.of(h),s=n(e),i=n(e),c=0,a=()=>{s.fill(1),i.fill(0),c=0},f=(...h)=>r(i,s,...h),d=(h=n(0))=>{i=f(o(0),h),s=f(),h.length!==0&&(i=f(o(1),h),s=f())},u=()=>{if(c++>=1e3)throw new Error("drbg: tried 1000 values");let h=0,y=[];for(;h{a(),d(h);let m;for(;!(m=y(u()));)d();return a(),m}}function fe(e,t,r={}){if(!e||typeof e!="object")throw new Error("expected valid options object");function n(o,s,i){let c=e[o];if(i&&c===void 0)return;let a=typeof c;if(a!==s||c===null)throw new Error(`param "${o}" is invalid: expected ${s}, got ${a}`)}Object.entries(t).forEach(([o,s])=>n(o,s,!1)),Object.entries(r).forEach(([o,s])=>n(o,s,!0))}function wr(e){let t=new WeakMap;return(r,...n)=>{let o=t.get(r);if(o!==void 0)return o;let s=e(r,...n);return t.set(r,s),s}}var et=BigInt(0),Q=BigInt(1),Nt=BigInt(2),zn=BigInt(3),Zn=BigInt(4),Xn=BigInt(5),Xs=BigInt(7),Fn=BigInt(8),Fs=BigInt(9),Yn=BigInt(16);function it(e,t){let r=e%t;return r>=et?r:t+r}function ct(e,t,r){let n=e;for(;t-- >et;)n*=n,n%=r;return n}function Vn(e,t){if(e===et)throw new Error("invert: expected non-zero number");if(t<=et)throw new Error("invert: expected positive modulus, got "+t);let r=it(e,t),n=t,o=et,s=Q,i=Q,c=et;for(;r!==et;){let f=n/r,d=n%r,u=o-i*f,l=s-c*f;n=r,r=d,o=i,s=c,i=u,c=l}if(n!==Q)throw new Error("invert: does not exist");return it(o,t)}function br(e,t,r){if(!e.eql(e.sqr(t),r))throw new Error("Cannot find square root")}function Wn(e,t){let r=(e.ORDER+Q)/Zn,n=e.pow(t,r);return br(e,n,t),n}function Ys(e,t){let r=(e.ORDER-Xn)/Fn,n=e.mul(t,Nt),o=e.pow(n,r),s=e.mul(t,o),i=e.mul(e.mul(s,Nt),o),c=e.mul(s,e.sub(i,e.ONE));return br(e,c,t),c}function Ws(e){let t=Ht(e),r=Jn(e),n=r(t,t.neg(t.ONE)),o=r(t,n),s=r(t,t.neg(n)),i=(e+Xs)/Yn;return(c,a)=>{let f=c.pow(a,i),d=c.mul(f,n),u=c.mul(f,o),l=c.mul(f,s),h=c.eql(c.sqr(d),a),y=c.eql(c.sqr(u),a);f=c.cmov(f,d,h),d=c.cmov(l,u,y);let m=c.eql(c.sqr(d),a),k=c.cmov(f,d,m);return br(c,k,a),k}}function Jn(e){if(e1e3)throw new Error("Cannot find square root: probably non-prime P");if(r===1)return Wn;let s=o.pow(n,t),i=(t+Q)/Nt;return function(a,f){if(a.is0(f))return f;if(Gn(a,f)!==1)throw new Error("Cannot find square root");let d=r,u=a.mul(a.ONE,s),l=a.pow(f,t),h=a.pow(f,i);for(;!a.eql(l,a.ONE);){if(a.is0(l))return a.ZERO;let y=1,m=a.sqr(l);for(;!a.eql(m,a.ONE);)if(y++,m=a.sqr(m),y===d)throw new Error("Cannot find square root");let k=Q<(n[o]="function",n),t);return fe(e,r),e}function ti(e,t,r){if(ret;)r&Q&&(n=e.mul(n,o)),o=e.sqr(o),r>>=Q;return n}function De(e,t,r=!1){let n=new Array(t.length).fill(r?e.ZERO:void 0),o=t.reduce((i,c,a)=>e.is0(c)?i:(n[a]=i,e.mul(i,c)),e.ONE),s=e.inv(o);return t.reduceRight((i,c,a)=>e.is0(c)?i:(n[a]=e.mul(i,n[a]),e.mul(i,c)),s),n}function Gn(e,t){let r=(e.ORDER-Q)/Nt,n=e.pow(t,r),o=e.eql(n,e.ONE),s=e.eql(n,e.ZERO),i=e.eql(n,e.neg(e.ONE));if(!o&&!s&&!i)throw new Error("invalid Legendre symbol result");return o?1:s?0:-1}function qe(e,t){t!==void 0&&oe(t);let r=t!==void 0?t:e.toString(2).length,n=Math.ceil(r/8);return{nBitLength:r,nByteLength:n}}function Ht(e,t,r=!1,n={}){if(e<=et)throw new Error("invalid field: expected ORDER > 0, got "+e);let o,s,i=!1,c;if(typeof t=="object"&&t!=null){if(n.sqrt||r)throw new Error("cannot specify opts in two arguments");let l=t;l.BITS&&(o=l.BITS),l.sqrt&&(s=l.sqrt),typeof l.isLE=="boolean"&&(r=l.isLE),typeof l.modFromBytes=="boolean"&&(i=l.modFromBytes),c=l.allowedLengths}else typeof t=="number"&&(o=t),n.sqrt&&(s=n.sqrt);let{nBitLength:a,nByteLength:f}=qe(e,o);if(f>2048)throw new Error("invalid field: expected ORDER of <= 2048 bytes");let d,u=Object.freeze({ORDER:e,isLE:r,BITS:a,BYTES:f,MASK:Ot(a),ZERO:et,ONE:Q,allowedLengths:c,create:l=>it(l,e),isValid:l=>{if(typeof l!="bigint")throw new Error("invalid field element: expected bigint, got "+typeof l);return et<=l&&ll===et,isValidNot0:l=>!u.is0(l)&&u.isValid(l),isOdd:l=>(l&Q)===Q,neg:l=>it(-l,e),eql:(l,h)=>l===h,sqr:l=>it(l*l,e),add:(l,h)=>it(l+h,e),sub:(l,h)=>it(l-h,e),mul:(l,h)=>it(l*h,e),pow:(l,h)=>ti(u,l,h),div:(l,h)=>it(l*Vn(h,e),e),sqrN:l=>l*l,addN:(l,h)=>l+h,subN:(l,h)=>l-h,mulN:(l,h)=>l*h,inv:l=>Vn(l,e),sqrt:s||(l=>(d||(d=Js(e)),d(u,l))),toBytes:l=>r?mr(l,f):zt(l,f),fromBytes:(l,h=!0)=>{if(c){if(!c.includes(l.length)||l.length>f)throw new Error("Field.fromBytes: expected "+c+" bytes, got "+l.length);let m=new Uint8Array(f);m.set(l,r?0:m.length-l.length),l=m}if(l.length!==f)throw new Error("Field.fromBytes: expected "+f+" bytes, got "+l.length);let y=r?yr(l):Et(l);if(i&&(y=it(y,e)),!h&&!u.isValid(y))throw new Error("invalid field element: outside of range 0..ORDER");return y},invertBatch:l=>De(u,l),cmov:(l,h,y)=>y?h:l});return Object.freeze(u)}function Qn(e){if(typeof e!="bigint")throw new Error("field order must be bigint");let t=e.toString(2).length;return Math.ceil(t/8)}function xr(e){let t=Qn(e);return t+Math.ceil(t/2)}function je(e,t,r=!1){let n=e.length,o=Qn(t),s=xr(t);if(n<16||n1024)throw new Error("expected "+s+"-1024 bytes of input, got "+n);let i=r?yr(e):Et(e),c=it(i,t-Q)+Q;return r?mr(c,o):zt(c,o)}var Zt=BigInt(0),Mt=BigInt(1);function le(e,t){let r=t.negate();return e?r:t}function Ve(e,t){let r=De(e.Fp,t.map(n=>n.Z));return t.map((n,o)=>e.fromAffine(n.toAffine(r[o])))}function no(e,t){if(!Number.isSafeInteger(e)||e<=0||e>t)throw new Error("invalid window size, expected [1.."+t+"], got W="+e)}function vr(e,t){no(e,t);let r=Math.ceil(t/e)+1,n=2**(e-1),o=2**e,s=Ot(e),i=BigInt(e);return{windows:r,windowSize:n,mask:s,maxNumber:o,shiftBy:i}}function to(e,t,r){let{windowSize:n,mask:o,maxNumber:s,shiftBy:i}=r,c=Number(e&o),a=e>>i;c>n&&(c-=s,a+=Mt);let f=t*n,d=f+Math.abs(c)-1,u=c===0,l=c<0,h=t%2!==0;return{nextN:a,offset:d,isZero:u,isNeg:l,isNegF:h,offsetF:f}}function ei(e,t){if(!Array.isArray(e))throw new Error("array expected");e.forEach((r,n)=>{if(!(r instanceof t))throw new Error("invalid point at index "+n)})}function ri(e,t){if(!Array.isArray(e))throw new Error("array of scalars expected");e.forEach((r,n)=>{if(!t.isValid(r))throw new Error("invalid scalar at index "+n)})}var Er=new WeakMap,oo=new WeakMap;function kr(e){return oo.get(e)||1}function eo(e){if(e!==Zt)throw new Error("invalid wNAF")}var Pe=class{constructor(t,r){this.BASE=t.BASE,this.ZERO=t.ZERO,this.Fn=t.Fn,this.bits=r}_unsafeLadder(t,r,n=this.ZERO){let o=t;for(;r>Zt;)r&Mt&&(n=n.add(o)),o=o.double(),r>>=Mt;return n}precomputeWindow(t,r){let{windows:n,windowSize:o}=vr(r,this.bits),s=[],i=t,c=i;for(let a=0;aZt||n>Zt;)r&Mt&&(s=s.add(o)),n&Mt&&(i=i.add(o)),o=o.double(),r>>=Mt,n>>=Mt;return{p1:s,p2:i}}function io(e,t,r,n){ei(r,e),ri(n,t);let o=r.length,s=n.length;if(o!==s)throw new Error("arrays of points and scalars must have equal length");let i=e.ZERO,c=Me(BigInt(o)),a=1;c>12?a=c-3:c>4?a=c-2:c>0&&(a=2);let f=Ot(a),d=new Array(Number(f)+1).fill(i),u=Math.floor((t.BITS-1)/a)*a,l=i;for(let h=u;h>=0;h-=a){d.fill(i);for(let m=0;m>BigInt(h)&f);d[B]=d[B].add(r[m])}let y=i;for(let m=d.length-1,k=i;m>0;m--)k=k.add(d[m]),y=y.add(k);if(l=l.add(y),h!==0)for(let m=0;mZt))throw new Error(`CURVE.${a} must be positive bigint`)}let o=ro(t.p,r.Fp,n),s=ro(t.n,r.Fn,n),c=["Gx","Gy","a",e==="weierstrass"?"b":"d"];for(let a of c)if(!o.isValid(t[a]))throw new Error(`CURVE.${a} must be valid field element of CURVE.Fp`);return t=Object.freeze(Object.assign({},t)),{CURVE:t,Fp:o,Fn:s}}var ao=(e,t)=>(e+(e>=0?t:-t)/fo)/t;function ni(e,t,r){let[[n,o],[s,i]]=t,c=ao(i*e,r),a=ao(-o*e,r),f=e-c*n-a*s,d=-c*o-a*i,u=f=h||d=h)throw new Error("splitScalar (endomorphism): failed, k="+e);return{k1neg:u,k1:f,k2neg:l,k2:d}}function Ar(e){if(!["compact","recovered","der"].includes(e))throw new Error('Signature format must be "compact", "recovered", or "der"');return e}function Sr(e,t){let r={};for(let n of Object.keys(t))r[n]=e[n]===void 0?t[n]:e[n];return ie(r.lowS,"lowS"),ie(r.prehash,"prehash"),r.format!==void 0&&Ar(r.format),r}var Br=class extends Error{constructor(t=""){super(t)}},kt={Err:Br,_tlv:{encode:(e,t)=>{let{Err:r}=kt;if(e<0||e>256)throw new r("tlv.encode: wrong tag");if(t.length&1)throw new r("tlv.encode: unpadded data");let n=t.length/2,o=ce(n);if(o.length/2&128)throw new r("tlv.encode: long form length too big");let s=n>127?ce(o.length/2|128):"";return ce(e)+s+o+t},decode(e,t){let{Err:r}=kt,n=0;if(e<0||e>256)throw new r("tlv.encode: wrong tag");if(t.length<2||t[n++]!==e)throw new r("tlv.decode: wrong tlv");let o=t[n++],s=!!(o&128),i=0;if(!s)i=o;else{let a=o&127;if(!a)throw new r("tlv.decode(long): indefinite length not supported");if(a>4)throw new r("tlv.decode(long): byte length is too big");let f=t.subarray(n,n+a);if(f.length!==a)throw new r("tlv.decode: length bytes not complete");if(f[0]===0)throw new r("tlv.decode(long): zero leftmost byte");for(let d of f)i=i<<8|d;if(n+=a,i<128)throw new r("tlv.decode(long): not minimal encoding")}let c=t.subarray(n,n+i);if(c.length!==i)throw new r("tlv.decode: wrong value length");return{v:c,l:t.subarray(n+i)}}},_int:{encode(e){let{Err:t}=kt;if(e{let{X:w,Y:p,Z:E}=L;if(n.eql(E,n.ONE))return{x:w,y:p};let A=L.is0();b==null&&(b=A?n.ONE:n.inv(E));let H=n.mul(w,b),O=n.mul(p,b),T=n.mul(E,b);if(A)return{x:n.ZERO,y:n.ZERO};if(!n.eql(T,n.ONE))throw new Error("invZ was invalid");return{x:H,y:O}}),I=wr(L=>{if(L.is0()){if(t.allowInfinityPoint&&!n.is0(L.Y))return;throw new Error("bad point: ZERO")}let{x:b,y:w}=L.toAffine();if(!n.isValid(b)||!n.isValid(w))throw new Error("bad point: x or y not field elements");if(!k(b,w))throw new Error("bad point: equation left != right");if(!L.isTorsionFree())throw new Error("bad point: not in prime-order subgroup");return!0});function _(L,b,w,p,E){return w=new x(n.mul(w.X,L),w.Y,w.Z),b=le(p,b),w=le(E,w),b.add(w)}class x{constructor(b,w,p){this.X=g("x",b),this.Y=g("y",w,!0),this.Z=g("z",p),Object.freeze(this)}static CURVE(){return s}static fromAffine(b){let{x:w,y:p}=b||{};if(!b||!n.isValid(w)||!n.isValid(p))throw new Error("invalid affine point");if(b instanceof x)throw new Error("projective point not allowed");return n.is0(w)&&n.is0(p)?x.ZERO:new x(w,p,n.ONE)}static fromBytes(b){let w=x.fromAffine(y(It(b,void 0,"point")));return w.assertValidity(),w}static fromHex(b){return x.fromBytes(G("pointHex",b))}get x(){return this.toAffine().x}get y(){return this.toAffine().y}precompute(b=8,w=!0){return U.createCache(this,b),w||this.multiply(Ge),this}assertValidity(){I(this)}hasEvenY(){let{y:b}=this.toAffine();if(!n.isOdd)throw new Error("Field doesn't support isOdd");return!n.isOdd(b)}equals(b){v(b);let{X:w,Y:p,Z:E}=this,{X:A,Y:H,Z:O}=b,T=n.eql(n.mul(w,O),n.mul(A,E)),C=n.eql(n.mul(p,O),n.mul(H,E));return T&&C}negate(){return new x(this.X,n.neg(this.Y),this.Z)}double(){let{a:b,b:w}=s,p=n.mul(w,Ge),{X:E,Y:A,Z:H}=this,O=n.ZERO,T=n.ZERO,C=n.ZERO,K=n.mul(E,E),Z=n.mul(A,A),j=n.mul(H,H),D=n.mul(E,A);return D=n.add(D,D),C=n.mul(E,H),C=n.add(C,C),O=n.mul(b,C),T=n.mul(p,j),T=n.add(O,T),O=n.sub(Z,T),T=n.add(Z,T),T=n.mul(O,T),O=n.mul(D,O),C=n.mul(p,C),j=n.mul(b,j),D=n.sub(K,j),D=n.mul(b,D),D=n.add(D,C),C=n.add(K,K),K=n.add(C,K),K=n.add(K,j),K=n.mul(K,D),T=n.add(T,K),j=n.mul(A,H),j=n.add(j,j),K=n.mul(j,D),O=n.sub(O,K),C=n.mul(j,Z),C=n.add(C,C),C=n.add(C,C),new x(O,T,C)}add(b){v(b);let{X:w,Y:p,Z:E}=this,{X:A,Y:H,Z:O}=b,T=n.ZERO,C=n.ZERO,K=n.ZERO,Z=s.a,j=n.mul(s.b,Ge),D=n.mul(w,A),P=n.mul(p,H),X=n.mul(E,O),rt=n.add(w,p),V=n.add(A,H);rt=n.mul(rt,V),V=n.add(D,P),rt=n.sub(rt,V),V=n.add(w,E);let F=n.add(A,O);return V=n.mul(V,F),F=n.add(D,X),V=n.sub(V,F),F=n.add(p,E),T=n.add(H,O),F=n.mul(F,T),T=n.add(P,X),F=n.sub(F,T),K=n.mul(Z,V),T=n.mul(j,X),K=n.add(T,K),T=n.sub(P,K),K=n.add(P,K),C=n.mul(T,K),P=n.add(D,D),P=n.add(P,D),X=n.mul(Z,X),V=n.mul(j,V),P=n.add(P,X),X=n.sub(D,X),X=n.mul(Z,X),V=n.add(V,X),D=n.mul(P,V),C=n.add(C,D),D=n.mul(F,V),T=n.mul(rt,T),T=n.sub(T,D),D=n.mul(rt,P),K=n.mul(F,K),K=n.add(K,D),new x(T,C,K)}subtract(b){return this.add(b.negate())}is0(){return this.equals(x.ZERO)}multiply(b){let{endo:w}=t;if(!o.isValidNot0(b))throw new Error("invalid scalar: out of range");let p,E,A=H=>U.cached(this,H,O=>Ve(x,O));if(w){let{k1neg:H,k1:O,k2neg:T,k2:C}=S(b),{p:K,f:Z}=A(O),{p:j,f:D}=A(C);E=Z.add(D),p=_(w.beta,K,j,H,T)}else{let{p:H,f:O}=A(b);p=H,E=O}return Ve(x,[p,E])[0]}multiplyUnsafe(b){let{endo:w}=t,p=this;if(!o.isValid(b))throw new Error("invalid scalar: out of range");if(b===St||p.is0())return x.ZERO;if(b===Xt)return p;if(U.hasCache(this))return this.multiply(b);if(w){let{k1neg:E,k1:A,k2neg:H,k2:O}=S(b),{p1:T,p2:C}=so(x,p,A,O);return _(w.beta,T,C,E,H)}else return U.unsafe(p,b)}multiplyAndAddUnsafe(b,w,p){let E=this.multiplyUnsafe(w).add(b.multiplyUnsafe(p));return E.is0()?void 0:E}toAffine(b){return R(this,b)}isTorsionFree(){let{isTorsionFree:b}=t;return i===Xt?!0:b?b(x,this):U.unsafe(this,c).is0()}clearCofactor(){let{clearCofactor:b}=t;return i===Xt?this:b?b(x,this):this.multiplyUnsafe(i)}isSmallOrder(){return this.multiplyUnsafe(i).is0()}toBytes(b=!0){return ie(b,"isCompressed"),this.assertValidity(),h(x,this,b)}toHex(b=!0){return xt(this.toBytes(b))}toString(){return``}get px(){return this.X}get py(){return this.X}get pz(){return this.Z}toRawBytes(b=!0){return this.toBytes(b)}_setWindowSize(b){this.precompute(b)}static normalizeZ(b){return Ve(x,b)}static msm(b,w){return io(x,o,b,w)}static fromPrivateKey(b){return x.BASE.multiply(_t(o,b))}}x.BASE=new x(s.Gx,s.Gy,n.ONE),x.ZERO=new x(n.ZERO,n.ONE,n.ZERO),x.Fp=n,x.Fn=o;let M=o.BITS,U=new Pe(x,t.endo?Math.ceil(M/2):M);return x.BASE.precompute(8),x}function lo(e){return Uint8Array.of(e?2:3)}function ho(e,t){return{secretKey:t.BYTES,publicKey:1+e.BYTES,publicKeyUncompressed:1+2*e.BYTES,publicKeyHasPrefix:!0,signature:2*t.BYTES}}function ii(e,t={}){let{Fn:r}=e,n=t.randomBytes||Kt,o=Object.assign(ho(e.Fp,r),{seed:xr(r.ORDER)});function s(h){try{return!!_t(r,h)}catch{return!1}}function i(h,y){let{publicKey:m,publicKeyUncompressed:k}=o;try{let B=h.length;return y===!0&&B!==m||y===!1&&B!==k?!1:!!e.fromBytes(h)}catch{return!1}}function c(h=n(o.seed)){return je(It(h,o.seed,"seed"),r.ORDER)}function a(h,y=!0){return e.BASE.multiply(_t(r,h)).toBytes(y)}function f(h){let y=c(h);return{secretKey:y,publicKey:a(y)}}function d(h){if(typeof h=="bigint")return!1;if(h instanceof e)return!0;let{secretKey:y,publicKey:m,publicKeyUncompressed:k}=o;if(r.allowedLengths||y===m)return;let B=G("key",h).length;return B===m||B===k}function u(h,y,m=!0){if(d(h)===!0)throw new Error("first arg must be private key");if(d(y)===!1)throw new Error("second arg must be public key");let k=_t(r,h);return e.fromHex(y).multiply(k).toBytes(m)}return Object.freeze({getPublicKey:a,getSharedSecret:u,keygen:f,Point:e,utils:{isValidSecretKey:s,isValidPublicKey:i,randomSecretKey:c,isValidPrivateKey:s,randomPrivateKey:c,normPrivateKeyToScalar:h=>_t(r,h),precompute(h=8,y=e.BASE){return y.precompute(h,!1)}},lengths:o})}function ci(e,t,r={}){Le(t),fe(r,{},{hmac:"function",lowS:"boolean",randomBytes:"function",bits2int:"function",bits2int_modN:"function"});let n=r.randomBytes||Kt,o=r.hmac||((w,...p)=>hr(t,w,ot(...p))),{Fp:s,Fn:i}=e,{ORDER:c,BITS:a}=i,{keygen:f,getPublicKey:d,getSharedSecret:u,utils:l,lengths:h}=ii(e,r),y={prehash:!1,lowS:typeof r.lowS=="boolean"?r.lowS:!1,format:void 0,extraEntropy:!1},m="compact";function k(w){let p=c>>Xt;return w>p}function B(w,p){if(!i.isValidNot0(p))throw new Error(`invalid signature ${w}: out of range 1..Point.Fn.ORDER`);return p}function $(w,p){Ar(p);let E=h.signature,A=p==="compact"?E:p==="recovered"?E+1:void 0;return It(w,A,`${p} signature`)}class g{constructor(p,E,A){this.r=B("r",p),this.s=B("s",E),A!=null&&(this.recovery=A),Object.freeze(this)}static fromBytes(p,E=m){$(p,E);let A;if(E==="der"){let{r:C,s:K}=kt.toSig(It(p));return new g(C,K)}E==="recovered"&&(A=p[0],E="compact",p=p.subarray(1));let H=i.BYTES,O=p.subarray(0,H),T=p.subarray(H,H*2);return new g(i.fromBytes(O),i.fromBytes(T),A)}static fromHex(p,E){return this.fromBytes($t(p),E)}addRecoveryBit(p){return new g(this.r,this.s,p)}recoverPublicKey(p){let E=s.ORDER,{r:A,s:H,recovery:O}=this;if(O==null||![0,1,2,3].includes(O))throw new Error("recovery id invalid");if(c*fo1)throw new Error("recovery id is ambiguous for h>1 curve");let C=O===2||O===3?A+c:A;if(!s.isValid(C))throw new Error("recovery id 2 or 3 invalid");let K=s.toBytes(C),Z=e.fromBytes(ot(lo((O&1)===0),K)),j=i.inv(C),D=S(G("msgHash",p)),P=i.create(-D*j),X=i.create(H*j),rt=e.BASE.multiplyUnsafe(P).add(Z.multiplyUnsafe(X));if(rt.is0())throw new Error("point at infinify");return rt.assertValidity(),rt}hasHighS(){return k(this.s)}toBytes(p=m){if(Ar(p),p==="der")return $t(kt.hexFromSig(this));let E=i.toBytes(this.r),A=i.toBytes(this.s);if(p==="recovered"){if(this.recovery==null)throw new Error("recovery bit must be present");return ot(Uint8Array.of(this.recovery),E,A)}return ot(E,A)}toHex(p){return xt(this.toBytes(p))}assertValidity(){}static fromCompact(p){return g.fromBytes(G("sig",p),"compact")}static fromDER(p){return g.fromBytes(G("sig",p),"der")}normalizeS(){return this.hasHighS()?new g(this.r,i.neg(this.s),this.recovery):this}toDERRawBytes(){return this.toBytes("der")}toDERHex(){return xt(this.toBytes("der"))}toCompactRawBytes(){return this.toBytes("compact")}toCompactHex(){return xt(this.toBytes("compact"))}}let v=r.bits2int||function(p){if(p.length>8192)throw new Error("input is too large");let E=Et(p),A=p.length*8-a;return A>0?E>>BigInt(A):E},S=r.bits2int_modN||function(p){return i.create(v(p))},R=Ot(a);function I(w){return jn("num < 2^"+a,w,St,R),i.toBytes(w)}function _(w,p){return It(w,void 0,"message"),p?It(t(w),void 0,"prehashed message"):w}function x(w,p,E){if(["recovered","canonical"].some(P=>P in E))throw new Error("sign() legacy options not supported");let{lowS:A,prehash:H,extraEntropy:O}=Sr(E,y);w=_(w,H);let T=S(w),C=_t(i,p),K=[I(C),I(T)];if(O!=null&&O!==!1){let P=O===!0?n(h.secretKey):O;K.push(G("extraEntropy",P))}let Z=ot(...K),j=T;function D(P){let X=v(P);if(!i.isValidNot0(X))return;let rt=i.inv(X),V=e.BASE.multiply(X).toAffine(),F=i.create(V.x);if(F===St)return;let pe=i.create(rt*i.create(j+F*C));if(pe===St)return;let Dr=(V.x===F?0:2)|Number(V.y&Xt),qr=pe;return A&&k(pe)&&(qr=i.neg(pe),Dr^=1),new g(F,qr,Dr)}return{seed:Z,k2sig:D}}function M(w,p,E={}){w=G("message",w);let{seed:A,k2sig:H}=x(w,p,E);return Pn(t.outputLen,i.BYTES,o)(A,H)}function U(w){let p,E=typeof w=="string"||Ct(w),A=!E&&w!==null&&typeof w=="object"&&typeof w.r=="bigint"&&typeof w.s=="bigint";if(!E&&!A)throw new Error("invalid signature, expected Uint8Array, hex string or Signature instance");if(A)p=new g(w.r,w.s);else if(E){try{p=g.fromBytes(G("sig",w),"der")}catch(H){if(!(H instanceof kt.Err))throw H}if(!p)try{p=g.fromBytes(G("sig",w),"compact")}catch{return!1}}return p||!1}function L(w,p,E,A={}){let{lowS:H,prehash:O,format:T}=Sr(A,y);if(E=G("publicKey",E),p=_(G("message",p),O),"strict"in A)throw new Error("options.strict was renamed to lowS");let C=T===void 0?U(w):g.fromBytes(G("sig",w),T);if(C===!1)return!1;try{let K=e.fromBytes(E);if(H&&C.hasHighS())return!1;let{r:Z,s:j}=C,D=S(p),P=i.inv(j),X=i.create(D*P),rt=i.create(Z*P),V=e.BASE.multiplyUnsafe(X).add(K.multiplyUnsafe(rt));return V.is0()?!1:i.create(V.x)===Z}catch{return!1}}function b(w,p,E={}){let{prehash:A}=Sr(E,y);return p=_(p,A),g.fromBytes(w,"recovered").recoverPublicKey(p).toBytes()}return Object.freeze({keygen:f,getPublicKey:d,getSharedSecret:u,utils:l,lengths:h,Point:e,sign:M,verify:L,recoverPublicKey:b,Signature:g,hash:t})}function ai(e){let t={a:e.a,b:e.b,p:e.Fp.ORDER,n:e.n,h:e.h,Gx:e.Gx,Gy:e.Gy},r=e.Fp,n=e.allowedPrivateKeyLengths?Array.from(new Set(e.allowedPrivateKeyLengths.map(i=>Math.ceil(i/2)))):void 0,o=Ht(t.n,{BITS:e.nBitLength,allowedLengths:n,modFromBytes:e.wrapPrivateKey}),s={Fp:r,Fn:o,allowInfinityPoint:e.allowInfinityPoint,endo:e.endo,isTorsionFree:e.isTorsionFree,clearCofactor:e.clearCofactor,fromBytes:e.fromBytes,toBytes:e.toBytes};return{CURVE:t,curveOpts:s}}function fi(e){let{CURVE:t,curveOpts:r}=ai(e),n={hmac:e.hmac,randomBytes:e.randomBytes,lowS:e.lowS,bits2int:e.bits2int,bits2int_modN:e.bits2int_modN};return{CURVE:t,curveOpts:r,hash:e.hash,ecdsaOpts:n}}function li(e,t){let r=t.Point;return Object.assign({},t,{ProjectivePoint:r,CURVE:Object.assign({},e,qe(r.Fn.ORDER,r.Fn.BITS))})}function uo(e){let{CURVE:t,curveOpts:r,hash:n,ecdsaOpts:o}=fi(e),s=si(t,r),i=ci(s,n,o);return li(e,i)}function po(e,t){let r=n=>uo({...e,hash:n});return{...r(t),create:r}}var Ft={p:BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"),n:BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"),h:BigInt(1),a:BigInt(0),b:BigInt(7),Gx:BigInt("0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"),Gy:BigInt("0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8")},hi={beta:BigInt("0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee"),basises:[[BigInt("0x3086d221a7d46bcde86c90e49284eb15"),-BigInt("0xe4437ed6010e88286f547fa90abfe4c3")],[BigInt("0x114ca50f7a8e2f3f657c1108d9d44cfd8"),BigInt("0x3086d221a7d46bcde86c90e49284eb15")]]},ui=BigInt(0),yo=BigInt(1),Tr=BigInt(2);function di(e){let t=Ft.p,r=BigInt(3),n=BigInt(6),o=BigInt(11),s=BigInt(22),i=BigInt(23),c=BigInt(44),a=BigInt(88),f=e*e*e%t,d=f*f*e%t,u=ct(d,r,t)*d%t,l=ct(u,r,t)*d%t,h=ct(l,Tr,t)*f%t,y=ct(h,o,t)*h%t,m=ct(y,s,t)*y%t,k=ct(m,c,t)*m%t,B=ct(k,a,t)*k%t,$=ct(B,c,t)*m%t,g=ct($,r,t)*d%t,v=ct(g,i,t)*y%t,S=ct(v,n,t)*f%t,R=ct(S,Tr,t);if(!ze.eql(ze.sqr(R),e))throw new Error("Cannot find square root");return R}var ze=Ht(Ft.p,{sqrt:di}),bo=po({...Ft,Fp:ze,lowS:!0,endo:hi},Gt),mo={};function Ze(e,...t){let r=mo[e];if(r===void 0){let n=Gt(Re(e));r=ot(n,n),mo[e]=r}return Gt(ot(r,...t))}var Or=e=>e.toBytes(!0).slice(1),Yt=bo.Point,Hr=e=>e%Tr===ui;function Ir(e){let{Fn:t,BASE:r}=Yt,n=_t(t,e),o=r.multiply(n);return{scalar:Hr(o.y)?n:t.neg(n),bytes:Or(o)}}function go(e){let t=ze;if(!t.isValidNot0(e))throw new Error("invalid x: Fail if x \u2265 p");let r=t.create(e*e),n=t.create(r*e+BigInt(7)),o=t.sqrt(n);Hr(o)||(o=t.neg(o));let s=Yt.fromAffine({x:e,y:o});return s.assertValidity(),s}var he=Et;function xo(...e){return Yt.Fn.create(he(Ze("BIP0340/challenge",...e)))}function wo(e){return Ir(e).bytes}function pi(e,t,r=Kt(32)){let{Fn:n}=Yt,o=G("message",e),{bytes:s,scalar:i}=Ir(t),c=G("auxRand",r,32),a=n.toBytes(i^he(Ze("BIP0340/aux",c))),f=Ze("BIP0340/nonce",a,s,o),{bytes:d,scalar:u}=Ir(f),l=xo(d,s,o),h=new Uint8Array(64);if(h.set(d,0),h.set(n.toBytes(n.create(u+l*i)),32),!vo(h,o,s))throw new Error("sign: Invalid signature produced");return h}function vo(e,t,r){let{Fn:n,BASE:o}=Yt,s=G("signature",e,64),i=G("message",t),c=G("publicKey",r,32);try{let a=go(he(c)),f=he(s.subarray(0,32));if(!ae(f,yo,Ft.p))return!1;let d=he(s.subarray(32,64));if(!ae(d,yo,Ft.n))return!1;let u=xo(n.toBytes(f),Or(a),i),l=o.multiplyUnsafe(d).add(a.multiplyUnsafe(n.neg(u))),{x:h,y}=l.toAffine();return!(l.is0()||!Hr(y)||h!==f)}catch{return!1}}var Xe=(()=>{let r=(o=Kt(48))=>je(o,Ft.n);bo.utils.randomSecretKey;function n(o){let s=r(o);return{secretKey:s,publicKey:wo(s)}}return{keygen:n,getPublicKey:wo,sign:pi,verify:vo,Point:Yt,utils:{randomSecretKey:r,randomPrivateKey:r,taggedHash:Ze,lift_x:go,pointToBytes:Or,numberToBytesBE:zt,bytesToNumberBE:Et,mod:it},lengths:{secretKey:32,publicKey:32,publicKeyHasPrefix:!1,signature:64,seed:48}}})();var Eo=Gt;var yi=4e3,ko=14e3,mi=3e3,So=16e3,Ao=6e5,Bo=25,Fe=20111,de=new TextEncoder,_r=new TextDecoder,wi=e=>{let t="";for(let r=0;rUint8Array.from(atob(e),t=>t.charCodeAt(0)),To=e=>de.encode(`private-talk/html-room/v3/mesh|${e}`),Io=["key","msg","seen"];async function gi(e,t){let r=tt(12),n=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:r,additionalData:To(e.topic)},e.meshKey,de.encode(JSON.stringify(t)))),o=new Uint8Array(12+n.length);return o.set(r),o.set(n,12),wi(o)}async function xi(e,t,r=Date.now()){if(typeof t!="string"||t.length<40||t.length>So||!/^[A-Za-z0-9+/]+={0,2}$/.test(t))return null;let n;try{let o=bi(t);n=JSON.parse(_r.decode(await crypto.subtle.decrypt({name:"AES-GCM",iv:o.subarray(0,12),additionalData:To(e.topic)},e.meshKey,o.subarray(12))))}catch{return null}return!n||typeof n!="object"||!/^[0-9a-f]{16}$/.test(n.d??"")||!Number.isInteger(n.n)||n.n<0||n.n>Bo||typeof n.t!="number"||Math.abs(r-n.t)>Ao||!["hb","f","bye"].includes(n.k)||n.c!==void 0&&(typeof n.c!="number"||!Number.isFinite(n.c))||n.k==="f"&&(!n.f||typeof n.f!="object"||!Io.includes(n.f.type)||typeof n.f.iv!="string"||typeof n.f.ct!="string")?null:n}var ue=class{constructor(t,r){Object.assign(this,{mesh:t,entry:r,kind:ft(r),url:Bt(r),open:!1,backoff:1e3})}protocols(){}target(){return this.url}connect(){if(this.mesh.stopped)return;let t;try{t=this.ws=this.mesh.createSocket(this.target(),this.protocols())}catch{this.fail();return}t.binaryType="arraybuffer",this.timeout=setTimeout(()=>this.fail(t),1e4),t.onopen=()=>{t===this.ws&&this.opened()},t.onmessage=r=>{t===this.ws&&!this.mesh.stopped&&this.data(r.data)},t.onclose=t.onerror=()=>this.fail(t)}ready(){clearTimeout(this.timeout),this.open=!0,this.backoff=1e3,this.mesh.linkChanged()}fail(t=this.ws){if(t!==this.ws||this.mesh.stopped)return;clearTimeout(this.timeout),clearInterval(this.keepalive),this.ws=null;let r=this.open;this.open=!1;try{t?.close()}catch{}r&&this.mesh.linkChanged(),clearTimeout(this.retry),this.retry=setTimeout(()=>this.connect(),this.backoff),this.backoff=Math.min(this.backoff*2,3e4)}stop(){clearTimeout(this.timeout),clearTimeout(this.retry),clearInterval(this.keepalive);let t=this.ws;this.ws=null,this.open=!1;try{t?.close(1e3)}catch{}}notice(t){this.noticed||(this.noticed=!0,this.mesh.onnotice(`${this.url}\uFF1A${String(t).slice(0,120)}`))}},Ur=class extends ue{target(){return this.url+this.mesh.d.topic}opened(){this.ready()}data(t){typeof t=="string"&&this.mesh.receive(t)}send(t){this.ws.send(t)}},Lr=class extends ue{opened(){this.sub="hr"+z(tt(6)),this.ws.send(JSON.stringify(["REQ",this.sub,{kinds:[Fe],"#t":[this.mesh.d.topic],since:Math.floor(Date.now()/1e3)-30}])),this.ready()}data(t){if(typeof t!="string"||t.length>So+2e3)return;let r;try{r=JSON.parse(t)}catch{return}Array.isArray(r)&&(r[0]==="EVENT"&&r[1]===this.sub&&r[2]?.kind===Fe&&typeof r[2].content=="string"?this.mesh.receive(r[2].content):r[0]==="OK"&&r[2]===!1?this.notice(`Nostr \u4E2D\u7EE7\u62D2\u7EDD\u6D88\u606F\uFF08${r[3]??""}\uFF09`):r[0]==="AUTH"?this.notice("Nostr \u4E2D\u7EE7\u8981\u6C42\u767B\u5F55\uFF08NIP-42\uFF09\uFF0C\u6682\u4E0D\u652F\u6301\uFF0C\u8BF7\u6362\u7528\u5176\u4ED6\u4E2D\u7EE7"):r[0]==="CLOSED"&&r[1]===this.sub&&(this.notice(`Nostr \u4E2D\u7EE7\u5173\u95ED\u8BA2\u9605\uFF08${r[2]??""}\uFF09`),this.fail()))}send(t){this.ws.send(JSON.stringify(["EVENT",vi(this.mesh.nostrKey,this.mesh.d.topic,t)]))}};function vi(e,t,r,n=Date.now()){let o=z(Xe.getPublicKey(e)),s=Math.floor(n/1e3),i=[["t",t]],c=Eo(de.encode(JSON.stringify([0,o,s,Fe,i,r])));return{id:z(c),pubkey:o,created_at:s,kind:Fe,tags:i,content:r,sig:z(Xe.sign(c,e))}}var Ei=e=>{let t=[];do{let r=e%128;e=Math.floor(e/128),e&&(r|=128),t.push(r)}while(e);return t},Ye=e=>{let t=de.encode(e);return[t.length>>8,t.length&255,...t]},Rr=(e,t)=>Uint8Array.from([e,...Ei(t.length),...t]),ki=(e,t=60)=>Rr(16,[...Ye("MQTT"),4,2,t>>8,t&255,...Ye(e)]),Si=(e,t=1)=>Rr(130,[t>>8,t&255,...Ye(e),0]),Ai=(e,t)=>Rr(48,[...Ye(e),...de.encode(t)]);function Bi(e){let t=[],r=0;for(;r=e.length)return{packets:t,rest:e.subarray(r)};if(i=e[s++],n+=(i&127)*o,o*=128,o>128**4)throw Error("bad length")}while(i&128);if(s+n>e.length)return{packets:t,rest:e.subarray(r)};t.push({type:e[r]>>4,flags:e[r]&15,body:e.subarray(s,s+n)}),r=s+n}return{packets:t,rest:e.subarray(r)}}var Cr=class extends ue{protocols(){return["mqtt"]}topic(){return`htmlroom/${this.mesh.d.topic}`}opened(){this.buf=new Uint8Array(0),this.ws.send(ki("hr"+z(tt(8))))}data(t){if(!(t instanceof ArrayBuffer))return;let r=new Uint8Array(this.buf.length+t.byteLength);if(r.set(this.buf),r.set(new Uint8Array(t),this.buf.length),r.length>65536){this.fail();return}let n;try{n=Bi(r)}catch{this.fail();return}this.buf=n.rest.slice();for(let o of n.packets)if(o.type===2){if(o.body[1]!==0){this.notice(`MQTT \u4EE3\u7406\u62D2\u7EDD\u8FDE\u63A5\uFF08\u8FD4\u56DE\u7801 ${o.body[1]}\uFF09`),this.fail();return}this.ws.send(Si(this.topic()))}else if(o.type===9){if(o.body[2]===128){this.notice("MQTT \u4EE3\u7406\u62D2\u7EDD\u8BA2\u9605"),this.fail();return}this.ready(),this.keepalive=setInterval(()=>{try{this.ws?.send(Uint8Array.of(192,0))}catch{}},3e4)}else if(o.type===3){let s=o.body[0]<<8|o.body[1],i=2+s+(o.flags>>1&3?2:0);_r.decode(o.body.subarray(2,2+s))===this.topic()&&this.mesh.receive(_r.decode(o.body.subarray(i)))}}send(t){this.ws.send(Ai(this.topic(),t))}},Ti={bc:Ur,nostr:Lr,mqtt:Cr},We=class{constructor(t,{derived:r,onhello:n,onmembers:o,onframe:s,onstatus:i=()=>{},onnotice:c=()=>{},onfatal:a=()=>{},createSocket:f=(l,h)=>h?new WebSocket(l,h):new WebSocket(l),now:d=()=>Date.now(),claimDelay:u=mi+Math.random()*1e3}){Object.assign(this,{d:r,onhello:n,onmembers:o,onframe:s,onstatus:i,onnotice:c,onfatal:a,createSocket:f,now:d,claimDelay:u}),this.node=z(tt(8)),this.me=0,this.peers=new Map,this.known=new Set,this.seenIvs=new Map,this.owners=new Map,this.lastHb=0,this.nostrKey=Xe.utils.randomPrivateKey(),this.links=t.map(h=>new Ti[ft(h)](this,h));let l=this;this.socket={get readyState(){return l.me&&l.openCount()?1:0},send:h=>l.sendSession(h),close(){}}}start(){for(let t of this.links)t.connect();this.claimTimer=setTimeout(()=>this.claim(),this.claimDelay),this.timer=setInterval(()=>this.sweep(),1e3),this.linkChanged()}stop(){this.stopped||(this.me&&this.broadcast({k:"bye"}).catch(()=>{}),this.stopped=!0,clearTimeout(this.claimTimer),clearTimeout(this.answer),clearInterval(this.timer),setTimeout(()=>this.links.forEach(t=>t.stop()),50))}openCount(){return this.links.filter(t=>t.open).length}linkChanged(){let t=this.openCount();t&&!this.me&&!this.stopped&&this.broadcast({k:"hb"}).catch(()=>{}),this.onstatus(t?`\u53BB\u4E2D\u5FC3\u5316\u4E2D\u7EE7\uFF1A\u5DF2\u8FDE\u63A5 ${t}/${this.links.length}\uFF08\u6D88\u606F\u540C\u65F6\u53D1\u5F80\u5168\u90E8\u4E2D\u7EE7\uFF09`:"\u53BB\u4E2D\u5FC3\u5316\u4E2D\u7EE7\uFF1A\u5168\u90E8\u65AD\u5F00\uFF0C\u6B63\u5728\u91CD\u8BD5")}async broadcast(t){let r=this.links.filter(s=>s.open);if(!r.length)return 0;let n=await gi(this.d,{...t,d:this.node,n:this.me,t:this.now(),...this.me&&{c:this.claimedAt}});this.remember(n.slice(0,16));let o=0;for(let s of r)try{s.send(n),o++}catch{s.fail()}return o}remember(t){for(this.seenIvs.set(t,this.now());this.seenIvs.size>4096;)this.seenIvs.delete(this.seenIvs.keys().next().value)}sendSession(t){let r;try{r=JSON.parse(t)}catch{return}if(!Io.includes(r?.type)||!this.me)return;let n=this.me;this.broadcast({k:"f",f:r}).then(o=>{o&&r.type==="msg"&&n===this.me&&this.onframe(r,n)}).catch(()=>{})}async receive(t){if(this.stopped||typeof t!="string")return;let r=t.slice(0,16);if(this.seenIvs.has(r))return;this.remember(r);let n=await xi(this.d,t,this.now());!n||n.d===this.node||this.stopped||(!this.known.has(n.d)&&n.k!=="bye"&&(this.known.add(n.d),this.answerSoon()),n.k==="bye"?this.peers.delete(n.d):n.n>0&&this.peers.set(n.d,{n:n.n,c:n.c??n.t,seen:this.now()}),this.reconcile(),n.k==="f"&&n.n>0&&this.owners.get(n.n)===n.d&&this.onframe({...n.f,at:this.now()},n.n))}answerSoon(){!this.me||this.answer||(this.answer=setTimeout(()=>{this.answer=null,this.lastHb=0,this.sweep()},50+Math.random()*250))}claim(){if(this.stopped)return;let t=new Set([...this.live()].map(([,n])=>n.n)),r=1;for(;t.has(r);)r++;if(r>Bo){this.stop(),this.onfatal("\u623F\u95F4\u5DF2\u6EE1\uFF0825 \u4EBA\uFF09\u3002");return}this.me=r,this.claimedAt=this.now(),this.lastHb=0,this.onhello(r),this.sweep()}*live(){let t=this.now();for(let r of this.peers)t-r[1].seen<=ko&&(yield r)}reconcile(){let t=new Map,r=new Map,n=(i,c,a)=>{let f=r.get(i);(!f||athis.owners.has(i)&&this.owners.get(i)!==c).map(([i])=>i);t.size===this.owners.size&&[...t].every(([i,c])=>this.owners.get(i)===c)||(o.length&&this.onmembers([...this.owners.keys()].filter(i=>!o.includes(i)).sort((i,c)=>i-c)),this.owners=t,this.me&&this.onmembers([...t.keys()].sort((i,c)=>i-c)))}sweep(){if(this.stopped)return;let t=this.now();for(let[r,n]of this.peers)t-n.seen>ko*4&&this.peers.delete(r);for(let[r,n]of this.seenIvs)if(t-n>Ao*2)this.seenIvs.delete(r);else break;this.reconcile(),this.me&&t-this.lastHb>=yi&&this.openCount()&&(this.lastHb=t,this.broadcast({k:"hb"}).catch(()=>{}))}};function Oo(e){let t=f=>document.getElementById(f),r=f=>{t("status").textContent=f};t("preview-help").hidden=!0;let n=0,o=!1,s=0,i,c=new Map;function a(f,d){if((f==="notice"||f==="fatal")&&r(d),f==="relay-change"&&(t("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"),f==="presence"&&(t("presence").textContent=`\u5728\u7EBF ${d} \u4EBA\uFF08${e.version===3?"\u6210\u5458\u5FC3\u8DF3\u4F30\u8BA1":"\u4E2D\u7EE7\u62A5\u544A"}\uFF09`),f==="ready"&&(r("\u5DF2\u5EFA\u7ACB\u7B7E\u540D\u6DF7\u5408\u52A0\u5BC6\u8FDE\u63A5"),t("safety").textContent=`\u5B89\u5168\u7801\uFF1A${d}\uFF0C\u8BF7\u901A\u8FC7\u72EC\u7ACB\u6E20\u9053\u6838\u5BF9\u3002`),f==="message"){let u=document.createElement("li");u.textContent=`${d.from}\uFF1A${d.text}`;let l=document.createElement("small");u.append(l),t("messages").append(u),c.set(d.id,l),c.size>200&&(c.delete(c.keys().next().value),t("messages").firstChild.remove())}f==="delivery"&&c.has(d.id)&&(c.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"),t("enter").disabled=!0;else if(e.version)try{lt(e),r(e.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"),t("password").disabled=!1,t("enter").disabled=!1}catch{r("\u914D\u7F6E\u65E0\u6548\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u6587\u4EF6\u3002"),t("enter").disabled=!0}else r("\u65E0\u79D8\u5BC6\u6A21\u677F\uFF1A\u8BF7\u4F7F\u7528\u751F\u6210\u5668\u521B\u5EFA\u623F\u95F4\u6587\u4EF6\u3002"),t("enter").disabled=!0;t("unlock").addEventListener("submit",async f=>{if(f.preventDefault(),!(o||Date.now()i.run(()=>(i.ws=u.socket,r("\u5DF2\u52A0\u5165\u53BB\u4E2D\u5FC3\u5316\u623F\u95F4\uFF0C\u6B63\u5728\u9A8C\u8BC1\u6210\u5458\u5BC6\u94A5\u2026"),i.incoming({type:"hello",you:l,resume:""}))),onmembers:l=>i.run(()=>i.incoming({type:"members",members:l})),onframe:(l,h)=>i.run(()=>i.incoming({...l,from:h})),onstatus:l=>{t("relay-status").textContent=l},onnotice:r,onfatal:l=>{i.halt(),r(l||"\u4F1A\u8BDD\u7ED3\u675F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u623F\u95F4\u6587\u4EF6\u3002")}});i.connection=u,u.start(),r("\u6B63\u5728\u8FDE\u63A5\u53BB\u4E2D\u5FC3\u5316\u4E2D\u7EE7\u5E76\u786E\u8BA4\u6210\u5458\u7F16\u53F7\u2026")}else{let u=new Ue(e.relays,{roomId:e.roomId,protocols:l=>["pt2",`a${z(i.d.access)}`,...i.resume&&u.lastIndex===l?[`r${i.resume}`]:[]],onactive:(l,h,y)=>i.run(()=>{y&&i.resetRelay(),i.ws=l,t("relay-status").textContent=`\u5F53\u524D\u4E2D\u7EE7\uFF1A${e.relays[h]}\uFF08\u4F18\u5148\u7EA7 ${h+1}/${e.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:l=>{let h=i.ws;i.run(()=>{if(h===i.ws)return i.incoming(l)})},ondisconnect:()=>{i.ws=null,t("relay-status").textContent="\u5F53\u524D\u4E2D\u7EE7\uFF1A\u5DF2\u65AD\u5F00\uFF0C\u6B63\u5728\u6309\u4F18\u5148\u7EA7\u91CD\u8BD5",a("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{n++,s=Date.now()+Math.min(3e4,1e3*2**Math.min(n,5)),t("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(()=>{t("enter").disabled=!1},Math.max(0,s-Date.now()))}}}),t("chat").addEventListener("submit",f=>{f.preventDefault(),i?.queue(t("text").value)?t("text").value="":r("\u6D88\u606F\u4E3A\u7A7A\u3001\u8FC7\u957F\u6216\u961F\u5217\u5DF2\u6EE1\u3002")}),addEventListener("pagehide",()=>i?.halt())}function Ii(e){if(e.version===2)return e.relays[0].replace(/^ws/,"http");if(e.version===3){let t=e.relays.map(r=>ft(r)==="bc"?new URL(Bt(r)).origin:null);if(t.every(r=>r&&r===t[0]))return t[0].replace(/^ws/,"http")}return""}var Ho=e=>JSON.stringify(e).replace(/[<>/\u2028\u2029]/g,t=>`\\u${t.charCodeAt(0).toString(16).padStart(4,"0")}`),Oi=e=>btoa(String.fromCharCode(...new TextEncoder().encode(e))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");function Nr(e){return lt(e),"#hr1="+Oi(JSON.stringify(e))}function Kr(e){let t=e.version?Ii(e):"",r=t?`${t}/hr.html${Nr(e)}`:"";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

                ${r?`

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

                私密聊天房间

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

                ${previewHelp(config)}
                ${RETARGET}

                  \n`; + const html=`加密聊天房间

                  加密聊天房间

                  正在启动……如果一直停在这里,请看下面的说明。

                  ${previewHelp(config)}
                  ${RETARGET}

                    HTML Room 开源项目:${repoLink}

                    \n`; return { html, csp }; } diff --git a/html-room/transport.js b/html-room/transport.js index 5c1dc80..d2627f1 100644 --- a/html-room/transport.js +++ b/html-room/transport.js @@ -13,7 +13,7 @@ export function fragmentFor(config){validate(config);return '#hr1='+encode(JSON. // Static HTML: still usable when a file preview disables JavaScript or CSP blocks boot. export function previewHelp(config){ const base=config.version?webEntry(config):'', link=base ? `${base}/hr.html${fragmentFor(config)}` : ''; - return `

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

                    ${link?`

                    在浏览器中打开同一房间(若无法跳转,请复制链接到 Safari 地址栏)。

                    链接包含加入房间的能力,请谨慎分发。网页代码可读取链接中的房间配置,其安全性不等同于验证过的离线文件;中继须提供 /hr.html 网页入口。

                    `:''}
                    `; + return `

                    页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                    ${link?`

                    用浏览器打开这个房间(点了没反应,就复制这个链接,粘贴到浏览器地址栏)。

                    注意:这个链接和文件一样是房间钥匙,别随便转发。

                    `:''}
                    `; } export function configFromHash(hash){ if(typeof hash!=='string'||!/^#hr1=[A-Za-z0-9_-]+$/.test(hash)||hash.length>8192)throw Error('房间链接格式或长度无效'); diff --git a/html-room/web.js b/html-room/web.js index c96752a..724a48d 100644 --- a/html-room/web.js +++ b/html-room/web.js @@ -20,20 +20,20 @@ async function filledTemplate(config){ function fail(text){el('mk-error').textContent=text;} function openFragment(fragment){ if(location.hash)history.replaceState(null,'',location.pathname+location.search); - if(!el('room-panel').hidden){el('status').textContent='请在新窗口打开其他房间链接,当前会话不会自动切换。';return;} + if(!el('room-panel').hidden){el('status').textContent='你已经在一个房间里了。要进别的房间,请在新窗口打开链接。';return;} el('maker').hidden=true;el('room-panel').hidden=false; try{ const config=configFromHash(fragment); if(!linkable(config.relays))throw Error('网页入口仅允许预设中继,请用房间文件连接其他中继。'); boot(config); - }catch{el('status').textContent='房间链接无效,或中继与本站不匹配。请重新索取链接或使用验证过的房间文件。';el('enter').disabled=true;} + }catch{el('status').textContent='房间链接打不开:链接不完整,或者它用的中继本站不支持。请让对方重发链接,或直接用房间文件。';el('enter').disabled=true;} } addEventListener('hashchange',()=>{if(location.hash)openFragment(location.hash);}); if(fragment)openFragment(fragment); else{ el('mk-relay').value=allowedRelays.join(', '); el('mk-gen').addEventListener('click',()=>{el('mk-pass').value=hex(random(32));}); - el('mk-mesh').addEventListener('click',()=>{el('mk-relay').value=allowedRelays.map(r=>`bc+${r}/bc/`).join(' ');fail('已填入本站的去中心化中继(bc+)。可继续添加 nostr+wss://… 或 mqtt+wss://… 作为同时在线的备份中继。');}); + el('mk-mesh').addEventListener('click',()=>{el('mk-relay').value=allowedRelays.map(r=>`bc+${r}/bc/`).join(' ');fail('已换成本站的去中心化中继。还可以在后面加上 nostr+wss://… 或 mqtt+wss://… 当备份。');}); el('maker').addEventListener('submit',async event=>{ event.preventDefault();el('mk-make').disabled=true;fail('');el('mk-out').hidden=true; try{ @@ -48,8 +48,8 @@ else{ el('mk-pass').value=''; if(linkable(relays)){el('mk-link').value=location.origin+location.pathname+fragmentFor(config);el('mk-link-row').hidden=false;} else{el('mk-link').value='';el('mk-link-row').hidden=true;} - fail(password?'文件已包装。请通过独立渠道传递口令;仍须防范离线猜测。':'此文件包含完整房间密钥,请勿通过未加密的社交软件直接发送。'); - }catch(e){fail(/[\u4e00-\u9fff]/.test(e?.message??'')?`生成失败:${e.message}`:'生成失败。请检查中继地址、环境和模板完整性。');} + fail(password?'✅ 房间建好了。口令请换一种方式告诉朋友(比如打电话),别和文件一起发。':'✅ 房间建好了。文件和链接就是钥匙,请只发给要聊天的人。'); + }catch(e){fail(/[\u4e00-\u9fff]/.test(e?.message??'')?`创建失败:${e.message}`:'创建失败,请检查中继地址后再试。');el('mk-advanced').open=true;} finally{el('mk-make').disabled=false;} }); // Existing room file -> same room on other relays. Read locally, never uploaded; @@ -58,17 +58,17 @@ else{ const say=t=>{el('cv-error').textContent=t;};el('cv-download').hidden=true;el('cv-make').disabled=true;say(''); try{ const file=el('cv-file').files?.[0];if(!file)throw Error('请先选择房间文件'); - if(file.size>400000)throw Error('文件过大,不是房间文件'); - let old;try{old=configFromRoomHtml(await file.text());}catch{throw Error('无法读取房间配置;旧版(单中继 version 1)文件需重新创建房间');} + if(file.size>400000)throw Error('这个文件太大了,不是房间文件'); + let old;try{old=configFromRoomHtml(await file.text());}catch{throw Error('读不出房间信息。如果是很早的旧版文件,请重新创建房间');} const config=withRelays(old,el('cv-relay').value); const html=await filledTemplate(config); if(convertUrl)URL.revokeObjectURL(convertUrl); convertUrl=URL.createObjectURL(new Blob([html],{type:'text/html'})); el('cv-download').href=convertUrl;el('cv-download').hidden=false; - say(`已生成同一房间的新文件,中继:${config.relays.join('、')}。${config.encrypted?'口令不变。':'此文件包含完整房间密钥,请勿通过未加密渠道发送。'}所有成员须改用新文件并重新核对安全码。`); - }catch(e){say(/[\u4e00-\u9fff]/.test(e?.message??'')?e.message:'生成失败:请检查中继地址(1–3 个 wss:// 源)。');} + say(`新文件已生成(中继:${config.relays.join('、')})。${config.encrypted?'口令不变。':''}请把它发给所有人,大家都用新文件打开,再核对一次安全码。`); + }catch(e){say(/[\u4e00-\u9fff]/.test(e?.message??'')?e.message:'生成失败:请检查中继地址。');} finally{el('cv-make').disabled=false;} }); - el('mk-copy').addEventListener('click',()=>navigator.clipboard?.writeText(el('mk-link').value).then(()=>fail('已复制。链接包含加入能力,请谨慎分发。'),()=>fail('无法自动复制,请手动复制链接。'))); + el('mk-copy').addEventListener('click',()=>navigator.clipboard?.writeText(el('mk-link').value).then(()=>fail('已复制。链接就是钥匙,请只发给要聊天的人。'),()=>fail('复制失败,请手动选中链接复制。'))); } addEventListener('pagehide',()=>{if(downloadUrl)URL.revokeObjectURL(downloadUrl);if(convertUrl)URL.revokeObjectURL(convertUrl);}); diff --git a/scripts/build-html-room.mjs b/scripts/build-html-room.mjs index 12cde7c..aa036b5 100644 --- a/scripts/build-html-room.mjs +++ b/scripts/build-html-room.mjs @@ -4,7 +4,7 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { makeConfig, relayEntry, relayList, validate } from '../html-room/crypto.js'; import { escapeConfig } from '../html-room/transport.js'; -import { roomHtml, withRelays, configFromRoomHtml } from '../html-room/template.js'; +import { roomHtml, withRelays, configFromRoomHtml, repoLink } from '../html-room/template.js'; export { escapeConfig }; const root=fileURLToPath(new URL('../',import.meta.url)); const hash=s=>createHash('sha256').update(s).digest('base64'); @@ -26,9 +26,9 @@ 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 房间

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

                    经典中继 wss://… 填 1–3 个,按优先级切换。去中心化中继填 1–5 个,消息同时发往全部:bc+wss://…(本项目或任意转发服务器)、nostr+wss://…(Nostr 中继)、mqtt+wss://…/mqtt(MQTT 代理)。两类不能混用。

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

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

                    `; + const maker=`

                    创建加密聊天室

                    点一下就能建好一个房间,把文件或链接发给朋友,打开就能聊。全程在你的浏览器里完成,服务器看不到聊天内容。

                    中继服务器(一般不用改)

                    中继负责帮大家传消息,看不到内容。想换成自己的或多个服务器:经典 wss://… 最多 3 个,坏了会自动换下一个;去中心化 bc+wss://…、nostr+wss://…、mqtt+wss://…/mqtt 最多 5 个,同时使用。两类不能混用。

                    已经有房间文件?换一个中继服务器

                    选择房间文件,填上新的中继地址,会生成同一个房间的新文件,口令不变。文件只在你的浏览器里读取,不会上传。

                    HTML Room 开源项目:${repoLink}

                    `; const body=t.html.match(/([\s\S]*?)${maker}\n`; + const page=`HTML Room · 加密聊天室${maker}\n`; const files={'hr-template.html':t.html,'hr.js':built.outputFiles[0].text,'hr.css':style+'\n','hr.html':page}; for(const [name,text] of Object.entries(files)){ const path=outputDir+name; diff --git a/test/fixtures/hr-relay-suite.mjs b/test/fixtures/hr-relay-suite.mjs index 00f9703..3994834 100644 --- a/test/fixtures/hr-relay-suite.mjs +++ b/test/fixtures/hr-relay-suite.mjs @@ -121,7 +121,7 @@ test('/hr/ drops table, ref and file frames without closing, and passes key, see assert.ok(b.frames.some((f) => f.type === 'seen')); assert.ok(!b.frames.some((f) => f.type === 'file')); assert.ok(!b.frames.some((f) => f.type === 'ref' || f.type === 'cards')); - // The sender gets its own message back: the page's "已提交中继". + // The sender gets its own message back: the page's "已发出". assert.ok(await a.frame('msg', (f) => f.from === 1)); assert.equal(a.closed, null); a.ws.close(4000); b.ws.close(4000); diff --git a/test/html-room.test.mjs b/test/html-room.test.mjs index 40bc119..7b35e6e 100644 --- a/test/html-room.test.mjs +++ b/test/html-room.test.mjs @@ -58,7 +58,7 @@ test('HTML Room v3 signed chat and receipt, replay and forged receipt rejection' a.queue('secret chat');await a.serial;const msg=frames.find(([,m])=>m.type==='msg')[1];await drain(); assert.equal(events[1].filter(([k])=>k==='message').length,1); await b.incoming(msg);assert.equal(events[1].filter(([k])=>k==='message').length,1); - await b.flush();await drain();assert.ok(events[0].some(([k,v])=>k==='delivery'&&v.text==='客户端确认 1/1')); + await b.flush();await drain();assert.ok(events[0].some(([k,v])=>k==='delivery'&&v.text==='已送达 1/1')); const h=[...a.own.keys()][0];const own=a.own.get(h);own.confirmed.clear(); const fake={type:'seen',from:2,h:'0.999',iv:msg.iv,ct:msg.ct};await assert.rejects(a.incoming(fake));assert.equal(own.confirmed.size,0); }); From 9fcfe451cc453c71f8f570cd5bc103a99ceb7caa Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 16:56:57 -0400 Subject: [PATCH 04/13] feat: let room-file holders type any relay before entering Files carry an optional custom: true (on by default in the creation page and CLI room; untick or --strict for exact relays only). Such files allow connect-src wss: plus loopback ws, and show a relay box on the entry screen; typed relays apply to that visit only and pick classic or decentralized mode, with the same key, password and roomId. Web links never show the box. Converter and CLI retarget keep or change the setting. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PCUwNSXQdqfDSugmFo4qmM --- README.md | 2 + browser-tests/custom-relay.mjs | 78 +++++++++++++++++++++++++++ browser-tests/pages.mjs | 2 +- dist/hr-template.html | 4 +- dist/hr.css | 2 +- dist/hr.html | 2 +- dist/hr.js | 2 +- docs/DEVELOPER.md | 6 +++ docs/html-room/protocol.md | 4 ++ docs/html-room/release-integrity.json | 14 ++--- docs/html-room/security-model.md | 6 ++- html-room/client.js | 24 +++++---- html-room/crypto.js | 32 ++++++++--- html-room/style.css | 2 +- html-room/template.js | 15 +++--- html-room/transport.js | 4 +- html-room/web.js | 6 +-- package.json | 2 +- scripts/build-html-room.mjs | 14 +++-- test/mesh.test.mjs | 25 +++++++++ 20 files changed, 198 insertions(+), 48 deletions(-) create mode 100644 browser-tests/custom-relay.mjs diff --git a/README.md b/README.md index a1134be..5307bb1 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ 创建房间时可按优先级填写 **1–3 个经典中继**(以逗号或空格分隔),或改用下文的[去中心化中继](#去中心化房间不依赖任何一台服务器)。首选不可用时会尝试备用,并每 60 秒探测首选是否恢复。页面显示当前中继和在线人数;连到不同中继的成员暂时无法互通,切换后请重新核对安全码。所有成员须使用同一份房间配置。 +**在房间文件里直接填自己的中继:** 新创建的房间文件默认允许自定义中继。打开文件后,进房间前可以把「中继服务器」改成任意 `wss://` 地址(也可以填去中心化中继),只对这次打开有效、不改动文件。大家要连同一个中继才能互相看到消息;去中心化中继只要有一个相同即可。创建时在「中继服务器(一般不用改)」里取消勾选「允许在房间文件里自定义中继」,文件就只能连列出的中继,更严格。 + **想改用自己搭的中继?** 打开房间文件,展开「换一个中继服务器」,填入自己的 `wss://` 地址即可生成同一房间的新文件(密钥、口令不变,无需口令);也可在生成器页面的「已经有房间文件?换一个中继服务器」中转换旧文件。把新文件发给所有成员后,大家就在你的中继上聊天,并重新核对安全码。详见 [更换中继](docs/DEVELOPER.md#更换中继迁移到自建中继)。 多中继配置使用新版房间格式,旧文件/链接需重新生成。网页房间链接只支持站点预先允许的中继,其他组合请使用下载的房间文件。配置静态网页的多个中继见 [开发说明](docs/DEVELOPER.md#多中继房间)。 diff --git a/browser-tests/custom-relay.mjs b/browser-tests/custom-relay.mjs new file mode 100644 index 0000000..66d13ba --- /dev/null +++ b/browser-tests/custom-relay.mjs @@ -0,0 +1,78 @@ +// A room file made for a relay that is gone: members type their own relay into +// the file and chat. Strict files and web links show no relay box. +import { chromium } from 'playwright'; +import { mkdtemp, writeFile, rm } 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 { fragmentFor } from '../html-room/transport.js'; +import { render } from '../scripts/build-html-room.mjs'; +import { nodeRelay, nostrRelay } from '../test/fixtures/mesh-relays.mjs'; + +const dir = await mkdtemp(tmpdir() + '/hr-custom-'); +const own = await nodeRelay(), nostr = await nostrRelay(); +let browser; +try { + const dead = 'wss://relay-that-is-gone.invalid'; + const config = await makeConfig(dead, 'pw', { custom: true }); + const path = dir + '/room.html'; await writeFile(path, (await render(config, config.relays)).html); + browser = await chromium.launch({ headless: true }); + const errors = [], sockets = []; + const open = async (relays) => { + const page = await browser.newPage(); + page.on('pageerror', e => errors.push(e.message)); page.on('websocket', s => sockets.push(s.url())); + await page.goto(pathToFileURL(path).href); + assert.equal(await page.locator('#custom-relay').isVisible(), true); + assert.equal(await page.locator('#relay-input').inputValue(), dead); + await page.locator('#password').fill('pw'); + if (relays) await page.locator('#relay-input').fill(relays); + await page.locator('#enter').click(); + return page; + }; + // A bad address is reported and does not count as a wrong password. + const probe = await open('http://nope.example'); + await probe.locator('#status').filter({ hasText: '中继地址不对' }).waitFor(); + assert.equal(await probe.locator('#enter').isEnabled(), true); + await probe.close(); + + // Classic: both type this project's relay. + const a = await open(own.url), b = await open(own.url); + for (const p of [a, b]) { + await p.locator('#relay-status').filter({ hasText: own.url }).waitFor({ timeout: 15000 }); + await p.locator('#status').filter({ hasText: '已加密连接' }).waitFor({ timeout: 20000 }); + } + await a.locator('#text').fill('文件里自己填的中继'); await a.locator('#send').click(); + await b.locator('#messages li').filter({ hasText: '文件里自己填的中继' }).waitFor({ timeout: 15000 }); + assert.ok(!sockets.some(u => u.includes('relay-that-is-gone')), 'the file\'s own relay was never tried'); + + // Decentralized: different relay lists that share one relay still meet. + const c = await open(`bc+${own.url}/bc/ nostr+${nostr.url}`), d = await open(`nostr+${nostr.url}`); + for (const p of [c, d]) await p.locator('#status').filter({ hasText: '已加密连接' }).waitFor({ timeout: 30000 }); + await d.locator('#text').fill('只共用一个 Nostr 中继'); await d.locator('#send').click(); + await c.locator('#messages li').filter({ hasText: '只共用一个 Nostr 中继' }).waitFor({ timeout: 20000 }); + + // Strict files and web links have no relay box. + const strict = await makeConfig(own.url, undefined), strictPath = dir + '/strict.html'; + await writeFile(strictPath, (await render(strict, strict.relays)).html); + const s = await browser.newPage(); await s.goto(pathToFileURL(strictPath).href); + await s.locator('#status').filter({ hasText: '进入房间' }).waitFor(); + assert.equal(await s.locator('#custom-relay').isVisible(), false); + const webConfig = await makeConfig(own.url, undefined, { custom: true }); + const w = await browser.newPage(); await w.goto(`http://127.0.0.1:${own.port}/hr.html${fragmentFor(webConfig)}`); + await w.locator('#status').filter({ hasText: '进入房间' }).waitFor(); + assert.equal(await w.locator('#custom-relay').isVisible(), false); + + // The creation page makes custom-relay files by default, strict when unticked. + const maker = await browser.newPage(); await maker.goto(`http://127.0.0.1:${own.port}/`); + const made = async () => { await maker.locator('#mk-make').click(); await maker.locator('#mk-out').waitFor({ state: 'visible' }); + const dl = maker.waitForEvent('download'); await maker.locator('#mk-download').click(); const f = dir + '/made.html'; await (await dl).saveAs(f); + return (await import('node:fs/promises')).readFile(f, 'utf8'); }; + assert.match(await made(), /connect-src wss: /); + await maker.locator('#mk-advanced summary').click(); await maker.locator('#mk-custom').uncheck(); + assert.doesNotMatch(await made(), /connect-src wss: /); + assert.deepEqual(errors, []); + console.log('PASS: custom relays typed into the file (classic and decentralized), bad address handling, strict files and web links without the box, maker default'); +} finally { + await browser?.close(); await Promise.all([own.stop(), nostr.stop()]); await rm(dir, { recursive: true, force: true }); +} diff --git a/browser-tests/pages.mjs b/browser-tests/pages.mjs index 6246035..b614111 100644 --- a/browser-tests/pages.mjs +++ b/browser-tests/pages.mjs @@ -29,7 +29,7 @@ try { const html=await readFile(await download.path(),'utf8'); const config=JSON.parse(html.match(/type="application\/json">(.*?)<\/script>/s)[1]); assert.deepEqual(config.relays,['wss://chat.nya.je']); - assert.ok(html.includes('connect-src wss://chat.nya.je')); + assert.ok(/connect-src wss: [^;]*wss:\/\/chat\.nya\.je;/.test(html), 'custom-relay file still lists its relay'); await page.goto(link);await page.waitForFunction(()=>location.hash===''); await page.locator('#enter').waitFor({state:'visible'}); assert.equal(await page.locator('#enter').isEnabled(),true); diff --git a/dist/hr-template.html b/dist/hr-template.html index ad7c8a6..3903fce 100644 --- a/dist/hr-template.html +++ b/dist/hr-template.html @@ -1,2 +1,2 @@ -加密聊天房间

                    加密聊天房间

                    正在启动……如果一直停在这里,请看下面的说明。

                    页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                      +加密聊天房间

                      加密聊天房间

                      正在启动……如果一直停在这里,请看下面的说明。

                      页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                        diff --git a/dist/hr.css b/dist/hr.css index 8fdaba9..47a77f1 100644 --- a/dist/hr.css +++ b/dist/hr.css @@ -1 +1 @@ -:root{color-scheme:dark;font-family:system-ui,sans-serif;background:#121820;color:#edf2f7}body{max-width:760px;margin:auto;padding:20px}h1{font-size:1.5rem}p{line-height:1.6}aside{border:1px solid #dcac58;padding:12px;border-radius:10px;color:#ffe0a3}input,textarea,button{font:inherit;padding:12px;border:1px solid #65778c;border-radius:8px;background:#202d3b;color:inherit;box-sizing:border-box}input,textarea{width:100%;margin:8px 0}button{cursor:pointer}button:disabled{opacity:.5}ol{padding:0;list-style:none}li{padding:12px;margin:8px 0;background:#202d3b;border-radius:8px;white-space:pre-wrap;overflow-wrap:anywhere}small{display:block;color:#b9c8d8;margin-top:6px}[hidden]{display:none}#status{color:#a7d9ff}textarea{min-height:90px}.hint{color:#b9c8d8;font-size:.92rem}code{background:#202d3b;padding:1px 4px;border-radius:4px;overflow-wrap:anywhere}footer{margin-top:32px;border-top:1px solid #34465a}a{color:#a7d9ff}details{margin:14px 0}summary{cursor:pointer}#mk-make,#enter,#send{background:#2f6fb0;border-color:#4a8bd0;font-weight:600;padding:12px 22px}button{margin:4px 0} +:root{color-scheme:dark;font-family:system-ui,sans-serif;background:#121820;color:#edf2f7}body{max-width:760px;margin:auto;padding:20px}h1{font-size:1.5rem}p{line-height:1.6}aside{border:1px solid #dcac58;padding:12px;border-radius:10px;color:#ffe0a3}input,textarea,button{font:inherit;padding:12px;border:1px solid #65778c;border-radius:8px;background:#202d3b;color:inherit;box-sizing:border-box}input,textarea{width:100%;margin:8px 0}button{cursor:pointer}button:disabled{opacity:.5}ol{padding:0;list-style:none}li{padding:12px;margin:8px 0;background:#202d3b;border-radius:8px;white-space:pre-wrap;overflow-wrap:anywhere}small{display:block;color:#b9c8d8;margin-top:6px}[hidden]{display:none}#status{color:#a7d9ff}textarea{min-height:90px}.hint{color:#b9c8d8;font-size:.92rem}code{background:#202d3b;padding:1px 4px;border-radius:4px;overflow-wrap:anywhere}footer{margin-top:32px;border-top:1px solid #34465a}a{color:#a7d9ff}details{margin:14px 0}summary{cursor:pointer}#mk-make,#enter,#send{background:#2f6fb0;border-color:#4a8bd0;font-weight:600;padding:12px 22px}button{margin:4px 0}.check{display:flex;gap:8px;align-items:center;margin:10px 0}.check input{width:auto;margin:0} diff --git a/dist/hr.html b/dist/hr.html index d359a0a..4aafb03 100644 --- a/dist/hr.html +++ b/dist/hr.html @@ -1 +1 @@ -HTML Room · 加密聊天室

                        创建加密聊天室

                        点一下就能建好一个房间,把文件或链接发给朋友,打开就能聊。全程在你的浏览器里完成,服务器看不到聊天内容。

                        中继服务器(一般不用改)

                        中继负责帮大家传消息,看不到内容。想换成自己的或多个服务器:经典 wss://… 最多 3 个,坏了会自动换下一个;去中心化 bc+wss://…、nostr+wss://…、mqtt+wss://…/mqtt 最多 5 个,同时使用。两类不能混用。

                        已经有房间文件?换一个中继服务器

                        选择房间文件,填上新的中继地址,会生成同一个房间的新文件,口令不变。文件只在你的浏览器里读取,不会上传。

                        HTML Room 开源项目:github.com/Freecode100Year/htmlroom

                        +HTML Room · 加密聊天室

                        创建加密聊天室

                        点一下就能建好一个房间,把文件或链接发给朋友,打开就能聊。全程在你的浏览器里完成,服务器看不到聊天内容。

                        中继服务器(一般不用改)

                        中继负责帮大家传消息,看不到内容。想换成自己的或多个服务器:经典 wss://… 最多 3 个,坏了会自动换下一个;去中心化 bc+wss://…、nostr+wss://…、mqtt+wss://…/mqtt 最多 5 个,同时使用。两类不能混用。

                        打开时,拿到文件的人进房前可以改填任意 wss:// 中继。关掉更严格:文件只能连上面这些中继。

                        已经有房间文件?换一个中继服务器

                        选择房间文件,填上新的中继地址,会生成同一个房间的新文件,口令不变。文件只在你的浏览器里读取,不会上传。

                        HTML Room 开源项目:github.com/Freecode100Year/htmlroom

                        diff --git a/dist/hr.js b/dist/hr.js index 9a9fa2f..a046d4b 100644 --- a/dist/hr.js +++ b/dist/hr.js @@ -1 +1 @@ -(()=>{var No=Object.defineProperty;var Mo=(e,t,r)=>t in e?No(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r;var st=(e,t,r)=>Mo(e,typeof t!="symbol"?t+"":t,r);var q=[];var Li=new TextEncoder;var qo=[256,512,1024,2048,4096];function ye(e){let t=qo.find(n=>n>e.length)??e.length+1,r=new Uint8Array(t);return r.set(e),r[e.length]=128,r}function me(e){let t=e.length-1;for(;t>=0&&e[t]===0;)t--;return t>=0&&e[t]===128?e.subarray(0,t):e}var St=new TextEncoder,z=e=>Array.from(new Uint8Array(e),t=>t.toString(16).padStart(2,"0")).join("");function yt(e,t){if(typeof e!="string"||e.length!==t*2||!/^[0-9a-f]+$/.test(e))throw Error("\u914D\u7F6E\u5B57\u8282\u683C\u5F0F\u9519\u8BEF");return Uint8Array.from(e.match(/../g),r=>parseInt(r,16))}var tt=e=>crypto.getRandomValues(new Uint8Array(e));async function jr(e){let t=typeof e=="string"?yt(e,32):e;if(!(t instanceof Uint8Array)||t.length!==32)throw Error("\u623F\u95F4\u5BC6\u94A5\u957F\u5EA6\u9519\u8BEF");let r=await crypto.subtle.importKey("raw",t,"HKDF",!1,["deriveBits"]),n=async u=>new Uint8Array(await crypto.subtle.deriveBits({name:"HKDF",hash:"SHA-256",salt:new Uint8Array(32),info:St.encode(`private-talk/html-room/v1/${u}`)},r,256)),o=await n("access"),s=await n("key"),i=await n("bind"),c=async u=>new Uint8Array(await crypto.subtle.deriveBits({name:"HKDF",hash:"SHA-256",salt:new Uint8Array(32),info:St.encode(`private-talk/html-room/v3/${u}`)},r,256)),a=await c("mesh"),f=z(await c("topic")).slice(0,32),d=await crypto.subtle.importKey("raw",a,"AES-GCM",!1,["encrypt","decrypt"]);return a.fill(0),{access:o,keyBytes:s,bind:i,topic:f,meshKey:d,roomId:z(await crypto.subtle.digest("SHA-256",o)),key:await crypto.subtle.importKey("raw",s,"AES-GCM",!1,["encrypt","decrypt"])}}var jo=/^(?:[a-z0-9.-]+|\[[0-9a-f:]+\])$/,Po=["localhost","127.0.0.1","[::1]"];function Pr(e){if(!jo.test(e.hostname))throw Error("\u4E2D\u7EE7\u5730\u5740\u91CC\u6709\u4E0D\u652F\u6301\u7684\u5B57\u7B26");if(e.username||e.password||e.search||e.hash||!["ws:","wss:"].includes(e.protocol))throw Error("\u4E2D\u7EE7\u5730\u5740\u8981\u4EE5 wss:// \u5F00\u5934\uFF0C\u4E0D\u80FD\u5E26\u8D26\u53F7\u3001? \u6216 #");if(e.protocol==="ws:"&&!Po.includes(e.hostname))throw Error("\u4E2D\u7EE7\u5730\u5740\u8981\u7528 wss://\uFF08\u52A0\u5BC6\u8FDE\u63A5\uFF09")}function Vo(e){let t=new URL(e);if(Pr(t),t.pathname!=="/")throw Error("\u7ECF\u5178\u4E2D\u7EE7\u53EA\u586B\u57DF\u540D\uFF0C\u4E0D\u8981\u5E26\u8DEF\u5F84\uFF0C\u4F8B\u5982 wss://relay.example.com");return t.origin}var Vr=/^(bc|nostr|mqtt)\+/i,Go=/^(?:\/[A-Za-z0-9._~-]+)*\/?$/,ft=e=>Vr.exec(e)?.[1].toLowerCase()??"hr",Bt=e=>e.replace(Vr,""),Qe=Bt;function tr(e){if(typeof e!="string")throw Error("\u4E2D\u7EE7\u5730\u5740\u65E0\u6548");let t=ft(e.trim());if(t==="hr")return Vo(e.trim());let r=new URL(Bt(e.trim()));Pr(r);let n=r.pathname;if(!Go.test(n)||n.includes("//"))throw Error("\u4E2D\u7EE7\u5730\u5740\u7684\u8DEF\u5F84\u91CC\u6709\u4E0D\u652F\u6301\u7684\u5B57\u7B26");if(t==="bc"){if(n==="/"&&(n="/bc/"),!n.endsWith("/"))throw Error("bc+ \u5730\u5740\u7684\u8DEF\u5F84\u8981\u4EE5 / \u7ED3\u5C3E")}else n==="/"&&(n="");return`${t}+${r.origin}${n}`}function Dt(e){let t=Array.isArray(e)?e:typeof e=="string"?e.trim().split(/[\s,]+/):[],r=[...new Set(t.map(tr))],n=r.some(o=>ft(o)!=="hr");if(n&&r.some(o=>ft(o)==="hr"))throw Error("\u7ECF\u5178\u4E2D\u7EE7\uFF08wss://\uFF09\u4E0D\u80FD\u4E0E\u53BB\u4E2D\u5FC3\u5316\u4E2D\u7EE7\uFF08bc+ / nostr+ / mqtt+\uFF09\u6DF7\u7528");if(!r.length||r.length>(n?5:3))throw Error(n?"\u53BB\u4E2D\u5FC3\u5316\u4E2D\u7EE7\u8BF7\u586B 1 \u5230 5 \u4E2A\u4E0D\u540C\u5730\u5740":"\u8BF7\u586B 1 \u5230 3 \u4E2A\u4E0D\u540C\u7684\u4E2D\u7EE7\u5730\u5740");return r}var we=e=>e.some(t=>ft(t)!=="hr")?3:2,er={name:"PBKDF2",hash:"SHA-256",iterations:31e4},Gr=e=>St.encode(`private-talk/html-room/v1/wrap|${e}`);async function zr(e,t){if(typeof e!="string"||!e.length||St.encode(e).length>1024)throw Error("\u53E3\u4EE4\u957F\u5EA6\u65E0\u6548");let r=await crypto.subtle.importKey("raw",St.encode(e),"PBKDF2",!1,["deriveKey"]);return crypto.subtle.deriveKey({...er,salt:t},r,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}function lt(e){if(!e||![2,3].includes(e.version)||!/^[0-9a-f]{64}$/.test(e.roomId))throw Error("\u623F\u95F4\u7248\u672C\u6216\u5730\u5740\u9519\u8BEF");if(!Array.isArray(e.relays)||JSON.stringify(Dt(e.relays))!==JSON.stringify(e.relays))throw Error("\u4E2D\u7EE7\u5217\u8868\u683C\u5F0F\u9519\u8BEF");if(we(e.relays)!==e.version)throw Error("\u623F\u95F4\u7248\u672C\u4E0E\u4E2D\u7EE7\u7C7B\u578B\u4E0D\u7B26");let t=["version","relays","roomId","encrypted"];if(e.encrypted===!0){let r=[...t,"salt","iv","ciphertext","kdf"];if(Object.keys(e).some(n=>!r.includes(n))||!e.kdf||Object.keys(e.kdf).length!==3||Object.entries(er).some(([n,o])=>e.kdf[n]!==o))throw Error("\u4E0D\u652F\u6301\u7684 KDF \u914D\u7F6E");yt(e.salt,16),yt(e.iv,12),yt(e.ciphertext,48)}else{if(e.encrypted!==!1||Object.keys(e).some(r=>![...t,"master"].includes(r)))throw Error("\u914D\u7F6E\u5B57\u6BB5\u9519\u8BEF");yt(e.master,32)}return e}async function Zr(e,t){let r=Dt(e),n=tt(32),o=await jr(n),s={version:we(r),relays:r,roomId:o.roomId,encrypted:!!t};if(!t)return{...s,master:z(n)};let i=tt(16),c=tt(12),a=await zr(t,i),f=await crypto.subtle.encrypt({name:"AES-GCM",iv:c,additionalData:Gr(s.roomId)},a,n);return{...s,salt:z(i),iv:z(c),ciphertext:z(f),kdf:{...er}}}async function Xr(e,t){lt(e);let r=e.encrypted?new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:yt(e.iv,12),additionalData:Gr(e.roomId)},await zr(t,yt(e.salt,16)),yt(e.ciphertext,48))):yt(e.master,32),n=await jr(r);if(r.fill(0),n.roomId!==e.roomId)throw Error("\u623F\u95F4\u5730\u5740\u4E0E\u5BC6\u94A5\u4E0D\u5339\u914D");return n}async function Fr(e,t){let r=tt(12),n=await crypto.subtle.encrypt({name:"AES-GCM",iv:r,additionalData:St.encode(`private-talk/html-room/v1/keys|${e.roomId}`)},e.key,ye(St.encode(t)));return{iv:btoa(String.fromCharCode(...r)),ct:btoa(String.fromCharCode(...new Uint8Array(n)))}}async function Yr(e,t){let r=n=>Uint8Array.from(atob(n),o=>o.charCodeAt(0));return new TextDecoder().decode(me(new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:r(t.iv),additionalData:St.encode(`private-talk/html-room/v1/keys|${e.roomId}`)},e.key,r(t.ct)))))}var be=BigInt(4294967295),Wr=BigInt(32);function zo(e,t=!1){return t?{h:Number(e&be),l:Number(e>>Wr&be)}:{h:Number(e>>Wr&be)|0,l:Number(e&be)|0}}function Zo(e,t=!1){let r=e.length,n=new Uint32Array(r),o=new Uint32Array(r);for(let s=0;se?`"${e}" `:"";function mt(e,t=""){if(typeof e!="number")throw new TypeError(ve(t)+"expected number, got "+typeof e);if(!Number.isSafeInteger(e)||e<0)throw new RangeError(ve(t)+"expected integer >= 0, got "+e);return e}function Xo(e,t=""){if(typeof e!="boolean")throw new TypeError(ve(t)+"expected boolean, got type="+typeof e);return e}function pt(e,t,r=""){if(Jr(e)&&(t===void 0||e.length===t))return e;t!==void 0&&mt(t,"length");let n=Jr(e),o=t!==void 0?` of length ${t}`:"",s=n?`length=${e.length}`:`type=${typeof e}`,i=ve(r)+"expected Uint8Array"+o+", got "+s;throw n?new RangeError(i):new TypeError(i)}var Fo=(e,t)=>{if(e===null||typeof e!="object"||Array.isArray(e))throw new TypeError((t==="object"?"":`"${t}" `)+"expected object, got type="+typeof e)},Qr=(e,t)=>{Fo(e,t);let r=Object.getPrototypeOf(e);if(r!==Object.prototype&&r!==null)throw new TypeError(`"${t}" expected plain object`);if(Object.prototype.hasOwnProperty.call(e,"__proto__"))throw new TypeError(`"${t}.__proto__" is not allowed`)};function tn(e,t=!0){if(e.destroyed)throw new Error("hash was destroyed");if(t&&e.finished)throw new Error("digest() was already called")}function Yo(e,t){pt(e,void 0,"output");let r=t.outputLen;if(!(e.length>=r))throw new RangeError('"output" expected length >= '+r)}function ir(e){return new Uint32Array(e.buffer,e.byteOffset,Math.floor(e.byteLength/4))}function an(...e){for(let t=0;t>>8&65280|e>>>24&255}function Qo(e){for(let t=0;te:Qo;function fn(e,t,r="opts"){return Qr(e,"defaults"),t!==void 0&&Qr(t,r),Object.assign(Object.create(null),e,t)}function ln(e,t={}){if(typeof e!="function")throw new TypeError('"hashCons" expected function, got type='+typeof e);t=fn({},t,"info");let r=(o,s)=>e(s).update(o).digest(),n=e(void 0);return r.outputLen=n.outputLen,r.blockLen=n.blockLen,r.canXOF=n.canXOF,r.create=o=>e(o),Object.assign(r,t),Object.freeze(r)}function ts(e=32){mt(e,"bytesLength");let t=typeof globalThis=="object"?globalThis.crypto:null;if(typeof t?.getRandomValues!="function")throw new Error("crypto.getRandomValues must be defined");if(e>65536)throw new RangeError(`"bytesLength" expected <= 65536, got ${e}`);return t.getRandomValues(new Uint8Array(e))}var ke=e=>({oid:Uint8Array.from([6,9,96,134,72,1,101,3,4,2,e])}),es=BigInt(0),te=BigInt(1),rs=BigInt(2),ns=BigInt(7),os=BigInt(256),ss=BigInt(113),hn=[],un=[],dn=[];for(let e=0,t=te,r=1,n=0;e<24;e++){[r,n]=[n,(2*r+3*n)%5],hn.push(2*(5*n+r)),un.push((e+1)*(e+2)/2%64);let o=es;for(let s=0;s<7;s++)t=(t<>ns)*ss)%os,t&rs&&(o^=te<<(te<e<>>32-r,fs=(e,t,r)=>t<>>32-r,ls=(e,t,r)=>t<>>64-r,hs=(e,t,r)=>e<>>64-r,en=(e,t,r)=>r>32?ls(e,t,r):as(e,t,r),rn=(e,t,r)=>r>32?hs(e,t,r):fs(e,t,r),qt=new Uint32Array(10);function us(e,t=24){if(!(e instanceof Uint32Array))throw new TypeError('"s" expected Uint32Array(50), got type='+typeof e);if(e.length!==50)throw new RangeError('"s" expected Uint32Array(50), got length='+e.length);if(mt(t,"rounds"),t<1||t>24)throw new Error('"rounds" expected integer 1..24');for(let r=24-t;r<24;r++){for(let s=0;s<10;s++)qt[s]=e[s]^e[s+10]^e[s+20]^e[s+30]^e[s+40];for(let s=0;s<10;s+=2){let i=(s+8)%10,c=(s+2)%10,a=qt[c],f=qt[c+1],d=en(a,f,1)^qt[i],u=rn(a,f,1)^qt[i+1];for(let l=0;l<50;l+=10)e[s+l]^=d,e[s+l+1]^=u}let n=e[2],o=e[3];for(let s=0;s<24;s++){let i=un[s],c=en(n,o,i),a=rn(n,o,i),f=hn[s];n=e[f],o=e[f+1],e[f]=c,e[f+1]=a}for(let s=0;s<50;s+=10){let i=e[s],c=e[s+1],a=e[s+2],f=e[s+3];e[s]^=~e[s+2]&e[s+4],e[s+1]^=~e[s+3]&e[s+5],e[s+2]^=~e[s+4]&e[s+6],e[s+3]^=~e[s+5]&e[s+7],e[s+4]^=~e[s+6]&e[s+8],e[s+5]^=~e[s+7]&e[s+9],e[s+6]^=~e[s+8]&i,e[s+7]^=~e[s+9]&c,e[s+8]^=~i&a,e[s+9]^=~c&f}e[0]^=is[r],e[1]^=cs[r]}an(qt)}var yn=class mn{constructor(t,r,n,o=!1,s=24){st(this,"state");st(this,"pos",0);st(this,"posOut",0);st(this,"finished",!1);st(this,"state32");st(this,"destroyed",!1);st(this,"blockLen");st(this,"suffix");st(this,"outputLen");st(this,"canXOF");st(this,"enableXOF",!1);st(this,"rounds");if(mt(t,"blockLen"),mt(r,"suffix"),mt(s,"rounds"),Xo(o,"enableXOF"),this.blockLen=t,this.suffix=r,this.outputLen=n,this.enableXOF=o,this.canXOF=o,this.rounds=s,mt(n,"outputLen"),!(0=r?ir(t):void 0;for(let f=0;f=r){for(let u=0,l=f/4;u=n&&this.keccak();let i=Math.min(n-this.posOut,s-o);t.set(r.subarray(this.posOut,this.posOut+i),o),this.posOut+=i,o+=i}return t}xofInto(t){if(!this.enableXOF)throw new Error("XOF is not enabled");return this.writeInto(t)}xof(t){return mt(t),this.xofInto(new Uint8Array(t))}digestInto(t){if(Yo(t,this),this.finished)throw new Error("digest() was already called");this.writeInto(t.length===this.outputLen?t:t.subarray(0,this.outputLen)),this.destroy()}digest(){let t=new Uint8Array(this.outputLen);return this.digestInto(t),t}destroy(){this.destroyed=!0,an(this.state)}_cloneInto(t){let{blockLen:r,suffix:n,outputLen:o,rounds:s,enableXOF:i}=this;return t||(t=new mn(r,n,o,i,s)),t.blockLen=r,t.state32.set(this.state32),t.pos=this.pos,t.posOut=this.posOut,t.finished=this.finished,t.rounds=s,t.suffix=n,t.outputLen=o,t.enableXOF=i,t.canXOF=this.canXOF,t.destroyed=this.destroyed,t}},wn=(e,t,r,n={})=>ln(()=>new yn(t,e,r),n),ds=wn(6,136,32,ke(8)),ps=wn(6,72,64,ke(10)),bn=(e,t,r,n={})=>ln((o={})=>(o=fn({},o),new yn(t,e,o.dkLen===void 0?r:o.dkLen,!0)),n),ys=bn(31,168,16,ke(11)),gn=bn(31,136,32,ke(12));function rr(e,t="object"){if(e===null||typeof e!="object"||Array.isArray(e))throw new TypeError(t==="object"?"expected valid options object":`"${t}" expected object, got type=${typeof e}`);return e}function ms(e,t={},r={},n="object"){rr(e,n),rr(t,"fields"),rr(r,"optFields");function o(i,c,a){let f=n==="object"?`param "${String(i)}"`:`"${n}.${String(i)}"`,d=e[i];if(!Object.prototype.hasOwnProperty.call(e,i)&&(a?d!==void 0:c!=="function"))throw new TypeError(`${f} is invalid: expected own property`);if(a&&d===void 0)return;let u=typeof d;if(u!==c||d===null)throw new TypeError(`${f} is invalid: expected ${c}, got ${u}`)}let s=(i,c)=>Object.entries(i).forEach(([a,f])=>o(a,f,c));s(t,!1),s(r,!0)}function ne(e,t="n"){if(typeof e!="number")throw new TypeError(`wrong u32 integer "${t}": expected number, got type=${typeof e}`);if(!Number.isSafeInteger(e)||e<0||e>4294967295)throw new RangeError(`wrong u32 integer "${t}": expected 0..4294967295, got ${e}`);return e}function xn(e){return ne(e,"x"),(e&e-1)===0&&e!==0}function vn(e,t){if(ne(e),typeof t!="number")throw new TypeError('"bits" expected number, got type='+typeof t);if(!Number.isSafeInteger(t)||t<0||t>32)throw new Error(`expected integer 0 <= bits <= 32, got ${t}`);let r=0;for(let n=0;n>>=1)r=r<<1|e&1;return r>>>0}function En(e){return ne(e),31-Math.clz32(e)}function nn(e){if(!e||typeof e!="object"||typeof e.length!="number")throw new TypeError('"values" expected array-like, got type='+typeof e);let t=e.length;if(!xn(t))throw new Error("expected positive power-of-two length, got "+t);let r=En(t);for(let n=0;n{ms(t,{N:"number",roots:"object",dit:"boolean"},{invertButterflies:"boolean",skipStages:"number",brp:"boolean"},"coreOpts");let{N:r,roots:n,dit:o,invertButterflies:s=!1,skipStages:i=0,brp:c=!0}=t;ne(r,"coreOpts.N");let a=En(r);if(!xn(r))throw new Error("FFT: Polynomial size should be power of two");ne(i,"coreOpts.skipStages");let f=a===0?0:a-1;if(i>f)throw new Error(`FFT: wrong skipStages: expected 0 <= skipStages <= ${f}`);if(n.length!==r)throw new Error(`FFT: wrong roots length: expected ${r}, got ${n.length}`);let d=o!==s;return u=>{if(u.length!==r)throw new Error("FFT: wrong Polynomial length");o&&c&&nn(u);for(let l=0,h=1;l>1,B=r>>y;for(let $=0;${}){if(!Array.isArray(e))throw new TypeError(`"${t}" expected array, got type=${typeof e}`);for(let n=0;ntypeof o=="number"?o:o.bytesLen,n=t.reduce((o,s)=>o+r(s),0);return{bytesLen:n,encode:o=>{let s=new Uint8Array(n);for(let i=0,c=0;i{pt(o,n,e);let s=[];for(let i of t){let c=r(i),a=o.subarray(0,c);s.push(typeof i=="number"?a:i.decode(a)),o=o.subarray(c)}return s}}}function or(e,t){let r=e,n=t*r.bytesLen;return{bytesLen:n,encode:o=>{let s=ws(o,"u");if(s.length!==t)throw new RangeError(`vecCoder.encode: wrong length=${s.length}. Expected: ${t}`);let i=new Uint8Array(n);for(let c=0,a=0;c{pt(o,n);let s=[];for(let i=0;i32)throw new RangeError('"bits" expected <= 32, got '+e);return e===32?4294967295:~(-1<>>0}var bs=e=>{let{newPoly:t,N:r,Q:n,F:o,ROOT_OF_UNITY:s,brvBits:i,isKyber:c}=e,a=(g,v=n)=>{let A=g%v|0;return(A>=0?A|0:v+A|0)|0},f=(g,v=n)=>{let A=a(g,v)|0;return(A>v>>1?A-v|0:A)|0};function d(){let g=t(r);for(let v=0;v{throw new Error("not implemented")},h=c?{add:(g,v)=>{let A=g+v|0;return A>=n?A-n|0:A},sub:(g,v)=>{let A=g-v|0;return A<0?A+n|0:A},mul:(g,v)=>a((g|0)*(v|0))|0,inv:l}:{add:(g,v)=>a((g|0)+(v|0))|0,sub:(g,v)=>a((g|0)-(v|0))|0,mul:(g,v)=>a((g|0)*(v|0))|0,inv:l},y={N:r,roots:u,invertButterflies:!0,skipStages:c?1:0,brp:!1},m=on(h,{dit:!1,...y}),k=on(h,{dit:!0,...y}),B={encode:g=>m(g),decode:g=>{k(g);for(let v=0;vB.encode(g),decode:g=>B.decode(g)},bitsCoder:(g,v)=>{for(let I=0,H=0;I32&&cn(H),H%=8;let A=cn(g),R=g*(r/8);return{bytesLen:R,encode:I=>{let H=I,x=new Uint8Array(R);for(let M=0,U=0,L=0,b=0;M=8;L-=8,U>>=8)x[b++]=U&255;return x},decode:I=>{let H=t(r);for(let x=0,M=0,U=0,L=0;x=g;U-=g,M>>=g)H[L++]=v.decode(M&A);return H}}}}},gs=e=>(t,r)=>{r||(r=e.blockLen);let n=new Uint8Array(t.length+2);n.set(t);let o=t.length,s=new Uint8Array(r),i=e.create({}),c=0,a=0;return{stats:()=>({calls:c,xofs:a}),get:(f,d)=>(n[o+0]=f,n[o+1]=d,i.destroy(),i=e.create({}).update(n),c++,()=>(a++,i.xofInto(s))),clean:()=>{i.destroy(),Y(s,n)}}},xs=gs(ys);var J=256,W=3329,vs=3303,Es=17,nt=bs({N:J,Q:W,F:vs,ROOT_OF_UNITY:Es,newPoly:e=>new Uint16Array(e),brvBits:7,isKyber:!0}),ks=Object.freeze({512:Object.freeze({N:J,Q:W,K:2,ETA1:3,ETA2:2,du:10,dv:4,RBGstrength:128}),768:Object.freeze({N:J,Q:W,K:3,ETA1:2,ETA2:2,du:10,dv:4,RBGstrength:192}),1024:Object.freeze({N:J,Q:W,K:4,ETA1:2,ETA2:2,du:11,dv:5,RBGstrength:256})}),As=e=>{if(e>=12)return{encode:r=>r,decode:r=>r>=W?r-W:r};let t=2**(e-1);return{encode:r=>((r<r*W+t>>>e}},Ss=e=>nt.bitsCoder(e,e===12?{encode:t=>t,decode:t=>t>=W?t-W:t}:{encode:t=>t,decode:t=>t}),ee=e=>e===12?Ss(12):nt.bitsCoder(e,As(e));function Ut(e,t){let r=e,n=t;for(let o=0;o=W?s-W:s}}function Bs(e,t){let r=e,n=t;for(let o=0;o>1)];o&1&&(s=-s);let{c0:i,c1:c}=Ts(r[2*o+0],r[2*o+1],n[2*o+0],n[2*o+1],s);r[2*o+0]=i,r[2*o+1]=c}return r}function sr(e){let t=e,r=new Uint16Array(J);for(let n=0;n>0|o[s+1]<<8)&4095,c=(o[s+1]>>4|o[s+2]<<4)&4095;i{let r=new Uint16Array(J),n=ir(e);Ee(n);let o=0;for(let s=0,i=0,c=0,a=0;s>=1,o+=1,o===t?(a=c,c=0):o===2*t&&(r[i++]=nt.mod(a-c),c=0,o=0)}if(Ee(n),o)throw new Error(`sampleCBD: leftover bits: ${o}`);return r};function re(e,t,r,n){return Is(e(n*J/4,t,r),n)}var Os=e=>{let t=e,{K:r,PRF:n,XOF:o,HASH512:s,ETA1:i,ETA2:c,du:a,dv:f}=t,d=ee(1),u=ee(f),l=ee(a),h=xe("publicKey",or(ee(12),r),32),y=or(ee(12),r),m=xe("ciphertext",or(l,r),u),k=xe("seed",32,32),B=($,g,v,A)=>{let R=[];for(let U=0;U{dt($,32,"seed");let g=new Uint8Array(33);g.set($),g[32]=r;let v=s(g),[A,R]=k.decode(v),I=[],H=[];for(let U=0;U{let[A,R]=h.decode($),I=o(R),H=B(A,(x,M)=>sr(I.get(x,M)),g,v);return I.clean(),H},prepare:$=>{let[g,v]=h.decode($),A=o(v),R=[];for(let I=0;IB(g.map(x=>x.slice()),(x,M)=>R[x*r+M].slice(),I,H),clean:()=>Y(g,R)}},decrypt:($,g)=>{let[v,A]=m.decode($),R=y.decode(g),I=new Uint16Array(J);for(let x=0;x{let m=h.subarray(0,384*t.K),k=i.encode(i.decode(sn(m))),B=jt(k,m);if(Y(k),!B)throw new Error(`ML-KEM.${y}: wrong publicKey modulus`)},l=Object.freeze({...c,seed:64,msg:f,msgRand:f,secretKey:a.bytesLen});return Object.freeze({info:Object.freeze({type:"ml-kem"}),lengths:l,keygen:h=>{let y=h===void 0,m=y?nr(d):h,k,B;try{dt(m,d,"seed");let $=r.keygen(m.subarray(0,32)),g=$.publicKey;k=$.secretKey,B=n(g);let v=a.encode([k,g,B,m.subarray(32)]);return{publicKey:g,secretKey:v}}finally{k!==void 0&&Y(k),B!==void 0&&Y(B),y&&Y(m)}},getPublicKey:h=>{let[y,m,k,B]=a.decode(h);return Uint8Array.from(m)},encapsulate:(h,y)=>{let m=y===void 0,k=m?nr(f):y,B;try{return dt(h,c.publicKey,"publicKey"),dt(k,f,"message"),u(h,"encapsulate"),B=o.create().update(k).update(n(h)).digest(),{cipherText:r.encrypt(h,k,B.subarray(32,64)),sharedSecret:B.subarray(0,32)}}finally{B!==void 0&&Y(B.subarray(32)),m&&Y(k)}},decapsulate:(h,y)=>{dt(y,a.bytesLen,"secretKey"),dt(h,c.cipherText,"cipherText");let m=a.bytesLen-96,k=m+32,B=n(y.subarray(m/2,k));if(!jt(B,y.subarray(k,k+32)))throw new Error("invalid secretKey: hash check failed");let[$,g,v,A]=a.decode(y),R=r.decrypt(h,$),I=o.create().update(R).update(v).digest(),H=I.subarray(0,32),x=r.encrypt(g,R,I.subarray(32,64)),M=jt(h,x),U=s.create({dkLen:32}).update(A).update(h).digest();return Y(R,x,I.subarray(32),M?U:H),M?H:U},prepare:h=>{dt(h,c.publicKey,"publicKey"),u(h,"prepare");let y=sn(h),m=n(y),k=r.prepare(y);return Object.freeze({publicKey:y,encapsulate:B=>{let $=B===void 0,g=$?nr(f):B,v;try{return dt(g,f,"message"),v=o.create().update(g).update(m).digest(),{cipherText:k.encrypt(g,v.subarray(32,64)),sharedSecret:v.subarray(0,32)}}finally{v!==void 0&&Y(v.subarray(32)),$&&Y(g)}},decapsulate:(B,$)=>{dt($,a.bytesLen,"secretKey"),dt(B,c.cipherText,"cipherText");let[g,v,A,R]=a.decode($);if(!jt(v,y)||!jt(A,m))throw new Error("ML-KEM.decapsulate: secretKey does not match prepared publicKey");let I=r.decrypt(B,g),H=o.create().update(I).update(m).digest(),x=H.subarray(0,32),M=k.encrypt(I,H.subarray(32,64)),U=jt(B,M),L=s.create({dkLen:32}).update(R).update(B).digest();return Y(I,M,H.subarray(32),U?L:x),U?x:L},clean:k.clean})}})}function Hs(e,t,r){return gn.create({dkLen:e}).update(t).update(new Uint8Array([r])).digest()}var Us={HASH256:ds,HASH512:ps,KDF:gn,XOF:xs,PRF:Hs},Ls=e=>_s({...Us,...e}),Ae=Ls(ks[768]);var at=new TextEncoder,cr=new TextDecoder,Hn={name:"ECDH",namedCurve:"P-256"},kn={name:"ECDSA",namedCurve:"P-256"},Se={name:"ECDSA",hash:"SHA-256"},Cs=2e3,Rs=100,$s=600*1e3,An=60*1e3,Ks=20*1e3,Ns=4,Ms=4e3,Sn=1580,Ds=1452,qs=/^(\d{1,9})\.(\d{1,9})$/,Be=/^[A-Za-z0-9+/]+={0,2}$/,js=/,"s":"([A-Za-z0-9+/]{86}==)"\}$/,bt=e=>btoa(String.fromCharCode(...new Uint8Array(e))),wt=e=>Uint8Array.from(atob(e),t=>t.charCodeAt(0)),Bn=e=>Array.from(new Uint8Array(e),t=>t.toString(16).padStart(2,"0")).join("");async function Tn(e,t){let r=await crypto.subtle.importKey("raw",e,{name:"HMAC",hash:"SHA-256"},!1,["sign"]);return new Uint8Array(await crypto.subtle.sign("HMAC",r,new Uint8Array([t])))}async function ar(e){return{mk:await Tn(e,1),next:await Tn(e,2)}}async function In(e,t){return crypto.subtle.importKey("raw",e,"AES-GCM",!1,t)}async function Te(e,t,r){let n=crypto.getRandomValues(new Uint8Array(12)),o=await crypto.subtle.encrypt({name:"AES-GCM",iv:n,additionalData:at.encode(r)},e,t);return{iv:bt(n),ct:bt(o)}}async function fr(e,{iv:t,ct:r},n){return new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:wt(t),additionalData:at.encode(n)},e,wt(r)))}var Ps=(e,t)=>{let r=new Uint8Array(e.length+t.length);return r.set(e),r.set(t,e.length),r};async function On(){let e=await crypto.subtle.generateKey(Hn,!1,["deriveBits"]);return{pair:e,epk:bt(await crypto.subtle.exportKey("raw",e.publicKey))}}var _n=e=>`pt31a|${e.from}|${e.id}|${e.t}|${e.epk}|${e.hello?1:0}|${JSON.stringify(e.w)}`,Ie=class e{static async create(t,r,n){let o=new e;return o.key=t,o.bind=r,o.me=n,o.signing=await crypto.subtle.generateKey(kn,!1,["sign","verify"]),o.spk=bt(await crypto.subtle.exportKey("raw",o.signing.publicKey)),o.kem=Ae.keygen(),o.kpk=bt(o.kem.publicKey),o.ecdh=await On(),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(t=this.mine.epoch+1,r=Date.now()){this.mine&&r-this.ecdhSince>=Ks&&(this.oldEcdh=[{ecdh:this.ecdh,until:r+An},...this.oldEcdh].slice(0,Ns),this.ecdh=await On(),this.ecdhSince=r),this.oldEcdh=this.oldEcdh.filter(n=>n.until>r),this.mine={epoch:t,ck:crypto.getRandomValues(new Uint8Array(32)),c:0,since:r,handed:new Map};for(let n of this.peers.values())n.sent=-1,n.gotAt=null}due(t=Date.now()){return this.mine.c>=Rs||t-this.mine.since>=$s}forget(t){this.peers.delete(t)}async pairKey(t,r,n=!1){let o=n?`${t.epk}|${r.kcIn??r.kc}`:t.epk;if(r.wraps.has(o))return r.wraps.get(o);let s=await crypto.subtle.importKey("raw",wt(r.epk),Hn,!1,[]),i=new Uint8Array(await crypto.subtle.deriveBits({name:"ECDH",public:s},t.pair.privateKey,256)),c=await crypto.subtle.importKey("raw",n?Ps(i,r.ss):i,"HKDF",!1,["deriveKey"]),a=at.encode(`private-talk/${n?"v3.2":"v3"}/pair|${[t.epk,r.epk].sort().join("|")}`),f=await crypto.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:this.bind,info:a},c,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"]);return r.wraps.set(o,f),f}pqFor(t,r){if(r.v!==32||this.mer!==this.me&&this.peers.has(r)&&this.peers.get(r).sent!==this.mine.epoch)}async announce(t,r,n=Date.now()){return(await this.announceAll(t,r,n,1/0))[0]}async announceAll(t,r,n=Date.now(),o=Ms){let s=this.mine;for(let[y,m]of s.handed)n-m.at>An&&s.handed.delete(y);let i=[...this.peers].some(([y,m])=>y{let y=c&&u?{q:1,k:this.kpk,...f}:{q:1,...f},m={kind:"keys",v:32,from:this.me,id:Bn(crypto.getRandomValues(new Uint8Array(8))),t:n,epk:this.epk,spk:this.spk,hello:!!r&&u,w:y};m.sig=bt(await crypto.subtle.sign(Se,this.signing.privateKey,at.encode(_n(m)))),a.push(JSON.stringify(m)),f={},d=0,u=!1},h=()=>430+(c&&u?Sn+8:0);for(let y of t){let m=this.peers.get(y);if(!m)continue;let{pq:k,can:B}=this.pqFor(y,m);if(!B)continue;let $=m.gotAt!=null&&s.handed.has(m.gotAt),g=$?m.gotAt:s.c,v=$?s.handed.get(g).ck:s.ck;$||(m.gotAt=g,s.handed.has(g)||s.handed.set(g,{ck:v,at:n}));let A=at.encode(JSON.stringify({e:s.epoch,ck:bt(v),c:g})),R=k?{...await Te(await this.pairKey(this.ecdh,m,!0),A,`pt32w|${this.me}|${y}|${m.epk}`),q:1,...m.kc&&{kc:m.kc}}:await Te(await this.pairKey(this.ecdh,m),A,`pt3w|${this.me}|${y}|${m.epk}`),I=JSON.stringify(R).length+8;Object.keys(f).length&&h()+d+I>o&&await l(),f[y]=R,d+=I,m.sent=this.mine.epoch}return(u||Object.keys(f).length)&&await l(),a}async receive(t,r){if(!t||t.kind!=="keys"||t.from!==r||r===this.me||typeof t.epk!="string"||!Be.test(t.epk)||t.epk.length>120||!t.w||typeof t.w!="object")return null;let n=typeof t.spk=="string",o=null;if(n){if(!Be.test(t.spk)||t.spk.length>120||typeof t.sig!="string")throw new Error("bad announcement");let c=this.peers.get(r);if(o=c?.spk===t.spk?c.verify:await crypto.subtle.importKey("raw",wt(t.spk),kn,!1,["verify"]),!await crypto.subtle.verify(Se,o,wt(t.sig),at.encode(_n(t))))throw new Error("bad signature")}let s=this.peers.get(r);s&&(s.spk??null)!==(n?t.spk:null)&&(s.spk&&this.changed.push(r),s=null),s?s.epk!==t.epk&&(n?(s.epk=t.epk,s.wraps=new Map):(s={epk:t.epk,spk:null,verify:null,wraps:new Map,recv:null,prev:null,sent:-1,gotAt:null,v:30},this.peers.set(r,s))):(s={epk:t.epk,spk:n?t.spk:null,verify:o,wraps:new Map,recv:null,prev:null,sent:-1,gotAt:null,v:n?31:30,kpk:null,ss:null,kc:null,kcIn:null,pqIn:!1},this.peers.set(r,s)),n&&t.w.q===1&&s.v!==32&&(s.v=32,s.sent=-1),n&&typeof t.w.k=="string"&&t.w.k.length===Sn&&Be.test(t.w.k)&&t.w.k!==s.kpk&&(Object.assign(s,{kpk:t.w.k,ss:null,kc:null,kcIn:null,v:32}),s.wraps=new Map);let i=t.w[this.me];if(i&&typeof i.iv=="string"&&typeof i.ct=="string"){let c=i.q===1;c&&n&&(s.v=32),c&&ru.ecdh)],f=null;if(!c||s.ss)for(let u of a)try{f=JSON.parse(cr.decode(await fr(await this.pairKey(u,s,c),i,`${c?"pt32w":"pt3w"}|${r}|${this.me}|${u.epk}`)));break}catch{}f&&c&&this.me=0&&d.length===32&&(!s.recv||f.e>s.recv.epoch)?(s.prev=s.recv,s.recv={epoch:f.e,ck:d,c:f.c},s.pqIn=c):f&&c&&s.recv&&f.e===s.recv.epoch&&(s.pqIn=!0)}return t.hello&&(s.sent=-1),s}takeChanged(){let t=this.changed;return this.changed=[],t}ready(t,r=!1){return t.every(n=>n===this.me||this.peers.get(n)?.sent===this.mine.epoch&&(!r||this.peers.get(n).v>=31))}quantumSafe(t){return t.every(r=>r===this.me||this.peers.get(r)?.v===32&&this.peers.get(r).ss&&this.peers.get(r).pqIn)}older(t){return t.filter(r=>r!==this.me&&this.peers.get(r)?.v===30)}async pairSecret(t,r){let n=this.peers.get(t);if(!n?.ss)throw new Error("no pair secret");let o=await crypto.subtle.importKey("raw",n.ss,"HKDF",!1,["deriveKey"]),s=at.encode(`private-talk/v3.2/for|${r}|${Math.min(t,this.me)}|${Math.max(t,this.me)}`);return crypto.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:this.bind,info:s},o,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}async sealFor(t,r,n){return Te(await this.pairSecret(t,n),at.encode(r),`pt32f|${this.me}|${t}|${n}`)}async openFrom(t,r,n){return cr.decode(await fr(await this.pairSecret(t,n),r,`pt32f|${t}|${this.me}|${n}`))}async seal(t){let r=this.mine,n=`${r.epoch}.${r.c}`,{mk:o,next:s}=await ar(r.ck);r.ck=s,r.c++;let i=bt(await crypto.subtle.sign(Se,this.signing.privateKey,at.encode(`pt31m|${this.me}|${n}|${t}`))),c=`${t.slice(0,-1)},"s":"${i}"}`;return{...await Te(await In(o,["encrypt"]),ye(at.encode(c)),`pt3|${this.me}|${n}`),h:n}}async open(t,r){let n=qs.exec(t.h??""),o=this.peers.get(r);if(!n||!o?.recv)throw new Error("no key");let s=Number(n[1]),i=Number(n[2]),c=s===o.recv.epoch?"recv":s===o.prev?.epoch?"prev":null,a=o[c];if(!c||iCs)throw new Error("out of step");let f=a.ck;for(let m=a.c;m=31){if(!y)throw new Error("unsigned");if(l=`${l.slice(0,y.index)}}`,!await crypto.subtle.verify(Se,o.verify,wt(y[1]),at.encode(`pt31m|${r}|${t.h}|${l}`)))throw new Error("bad signature");h=!0}return o[c]={epoch:s,ck:u,c:i+1},{text:l,missed:i-a.c,signed:h}}async safetyCode(t){let r=i=>i===this.me?this.spk:this.peers.get(i)?.spk??this.peers.get(i)?.epk??"?",n=t.map(i=>`${i}:${r(i)}`).sort().join("|"),o=new Uint8Array(await crypto.subtle.digest("SHA-256",at.encode(`private-talk/v3.1/safety|${Bn(this.bind)}|${n}`))),s=0n;for(let i of o.subarray(0,12))s=s<<8n|BigInt(i);return(s%10n**20n).toString().padStart(20,"0").replace(/(\d{5})(?=\d)/g,"$1 ")}};function _e(e){let t=e.trim();return!t||Array.from(t).length>500||new TextEncoder().encode(t).byteLength>2048?null:t}var Oe=600*1e3,Un=/^[0-9a-f]{16}$/;function Ln(e,t,r=Date.now(),n,o,s){let i=Array.from(crypto.getRandomValues(new Uint8Array(8)),a=>a.toString(16).padStart(2,"0")).join(""),c=Number.isInteger(s)&&s>=0&&s<=5;return JSON.stringify({from:e,id:i,t:r,text:t,...n&&{g:n},...o&&{f:o},...c&&{q:s}})}function Cn(e,t,r,n=Date.now()){let o;try{o=JSON.parse(e)}catch{return null}if(!o||typeof o!="object"||typeof o.text!="string"||typeof o.id!="string"||!Un.test(o.id)||!Number.isInteger(o.from)||o.from<1||o.from>25||o.from!==t.from||!Number.isFinite(o.t)||!Number.isFinite(t.at)||Math.abs(n-o.t)>Oe||Math.abs(t.at-o.t)>Oe)return null;let s=Number.isInteger(o.q)&&o.q>=0&&o.q<=5;if(s){if(o.text!==""&&_e(o.text)!==o.text)return null}else if(_e(o.text)!==o.text)return null;for(let[f,d]of r)Math.abs(n-d)>2*Oe&&r.delete(f);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,c=o.f&&typeof o.f=="object"&&!Array.isArray(o.f)?o.f:void 0,a=s?o.q:void 0;return{text:o.text,id:o.id,g:i,f:c,q:a}}function lr(e,t,r,n=Date.now(),o="keys"){let s;try{s=JSON.parse(e)}catch{return null}return!s||typeof s!="object"||s.kind!==o||typeof s.id!="string"||!Un.test(s.id)||!Number.isInteger(s.from)||s.from!==t.from||!Number.isFinite(s.t)||Math.abs(n-s.t)>Oe||r.has(s.id)?null:(r.set(s.id,s.t),s)}var Vs=()=>new Map,He=class{constructor(t,r,{storage:n=Vs(),emit:o=()=>{}}={}){Object.assign(this,{d:t,config:r,store:n,emit:o,deviceId:z(tt(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(t){return this.serial=this.serial.then(()=>{if(!this.stopped)return t()}).catch(()=>this.emit("notice","\u6536\u5230\u4E00\u6761\u65E0\u6CD5\u9A8C\u8BC1\u7684\u6570\u636E\uFF0C\u5DF2\u5FFD\u7565\u3002")),this.serial}send(t){!this.stopped&&this.ws?.readyState===1&&this.ws.send(JSON.stringify(t))}async announce(t=!1){if(!this.group)return;let r=this.group.owed(this.present);if(!(!t&&!r.length))for(let n of await this.group.announceAll(r,t))this.send({type:"key",...await Fr(this.d,n)})}ready(){return!this.stopped&&this.group&&this.present.length>1&&this.group.ready(this.present,!0)&&this.group.quantumSafe(this.present)}async incoming(t){if(this.stopped)return;if(t.type==="pong"){this.lastPong=Date.now();return}if(t.type==="wait"){this.nextSend=Date.now()+Math.min(3e4,Math.max(1e3,t.ms||6e3));return}if(t.type==="hello"){this.resume=t.resume,(!this.group||this.group.me!==t.you)&&(this.group=await Ie.create(this.d.key,this.d.bind,t.you),this.own.clear(),this.received.clear(),this.receipts=[],this.present=[]),await this.announce(!0);return}if(!this.group)return;if(t.type==="members"){if(!Array.isArray(t.members)||t.members.some(i=>!Number.isInteger(i)||i<1||i>25))return;let s=this.present.filter(i=>!t.members.includes(i));for(let i of s){this.group.forget(i),this.config.version===3&&this.pins.delete(i);for(let c of this.received.keys())c.startsWith(`${i}|`)&&this.received.delete(c)}this.present=t.members,s.length&&await this.group.rekey(),await this.announce(!0),this.emit("presence",this.present.length),await this.update();return}if(t.from===this.group.me){let s=this.own.get(t.h);t.type==="msg"&&s&&s.frame.iv===t.iv&&s.frame.ct===t.ct&&(s.echo=!0,this.emit("delivery",{id:s.id,text:"\u5DF2\u53D1\u51FA"}));return}if(!this.present.includes(t.from))return;if(t.type==="key"){let s=lr(await Yr(this.d,t),t,this.seen);if(!s||s.v!==32||!s.spk)return;let i=this.pins.get(t.from),c=await this.group.receive(s,t.from);if(!c)return;if(i&&i!==c.spk){this.halt(),this.emit("fatal","\u6709\u6210\u5458\u7684\u8EAB\u4EFD\u53D8\u4E86\uFF0C\u53EF\u80FD\u6709\u4EBA\u5192\u5145\u3002\u8BF7\u6253\u7535\u8BDD\u7B49\u65B9\u5F0F\u786E\u8BA4\u540E\uFF0C\u91CD\u65B0\u6253\u5F00\u623F\u95F4\u3002");return}this.pins.set(t.from,c.spk),c?.lost&&(c.sent=-1),await this.announce(),await this.update();return}if(!["msg","seen"].includes(t.type)||!t.h)return;let r=`${t.from}|${t.h}`,n=this.received.get(r);if(n){t.type==="msg"&&n.iv===t.iv&&n.ct===t.ct&&this.receipts.length<100&&this.receipts.push([t.from,t.h]);return}let o=await this.group.open(t,t.from);if(!(!o.signed||this.group.peers.get(t.from)?.v!==32))if(t.type==="seen"){let s=lr(o.text,t,this.seen,Date.now(),"seen");if(s&&Array.isArray(s.l)&&s.l.length<=25)for(let i of s.l){if(!Array.isArray(i)||i[0]!==this.group.me)continue;let c=this.own.get(i[1]);c&&c.targets.includes(t.from)&&(c.confirmed.add(t.from),this.emit("delivery",{id:c.id,text:`\u5DF2\u9001\u8FBE ${c.confirmed.size}/${c.targets.length}`}))}}else{let s=Cn(o.text,t,this.seen);if(!s||s.g||s.f)return;for(this.received.set(r,{iv:t.iv,ct:t.ct});this.received.size>200;)this.received.delete(this.received.keys().next().value);this.emit("message",{id:s.id,from:t.from,text:s.text}),this.receipts.length<100&&this.receipts.push([t.from,t.h])}}async update(){this.ready()&&this.emit("ready",await this.group.safetyCode(this.present)),await this.flush()}queue(t){if(this.stopped||(t=_e(t),!t||this.queued.length>=20))return!1;let r=z(tt(8));return this.queued.push({id:r,text:t}),this.emit("message",{id:r,from:"\u6211",text:t}),this.emit("delivery",{id:r,text:"\u53D1\u9001\u4E2D\u2026\u2026"}),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 t=JSON.stringify({kind:"seen",id:z(tt(8)),from:this.group.me,t:Date.now(),l:this.receipts.splice(0,25)});this.send({type:"seen",...await this.group.seal(t)})}if(this.queued.length&&Date.now()>=(this.nextSend??0)){let t=this.queued.shift(),r={type:"msg",...await this.group.seal(Ln(this.group.me,t.text))};for(this.own.set(r.h,{...t,frame:r,targets:this.present.filter(n=>n!==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[t,r]of this.seen)Date.now()-r>12e5&&this.seen.delete(t);for(;this.seen.size>4096;)this.seen.delete(this.seen.keys().next().value)}}async retryPending(){for(let t of this.own.values())!t.echo&&Date.now()-t.at>8e3&&(t.retries??0)<3&&(this.send(t.frame),t.at=Date.now(),t.retries=(t.retries??0)+1),!t.echo&&(t.retries??0)>=3&&this.emit("delivery",{id:t.id,text:"\u53EF\u80FD\u6CA1\u53D1\u51FA\u53BB\uFF0C\u8BF7\u786E\u8BA4\u540E\u91CD\u53D1"})}resetRelay(){for(let t of this.own.values())(t.confirmed.sizenew WebSocket(d,u)}){Object.assign(this,{relays:t,roomId:r,protocols:n,onactive:o,onmessage:s,onnotice:i,onfatal:c,ondisconnect:a,createSocket:f}),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 t=this.active;if(t?.ready){if(Date.now()-t.lastPong>65e3){this.fail(t);return}if(Date.now()-t.lastPing>=25e3){t.lastPing=Date.now();try{t.ws.send(JSON.stringify({type:"ping"}))}catch{this.fail(t)}}}},1e3)}connect(t,r=!1){if(this.stopped)return;let n={index:t,probe:r,frames:[],ready:!1,retired:!1};r?this.probe=n:this.pending=n;try{n.ws=this.createSocket(`${this.relays[t]}/hr/${this.roomId}`,this.protocols(t))}catch{this.fail(n);return}n.timeout=setTimeout(()=>this.fail(n),1e4),n.ws.onerror=()=>{!n.retired&&!n.probe&&this.onnotice("\u8FDE\u4E0D\u4E0A\u4E2D\u7EE7\uFF08\u53EF\u80FD\u662F\u7F51\u7EDC\u3001\u8BC1\u4E66\u6216\u6743\u9650\u95EE\u9898\uFF09\uFF0C\u6B63\u5728\u91CD\u8BD5\u2026\u2026")},n.ws.onclose=o=>this.fail(n,o.code),n.ws.onmessage=o=>{if(n.retired||this.stopped||typeof o.data!="string"||o.data.length>7e3)return;let s;try{s=JSON.parse(o.data)}catch{return}if(!(!s||typeof s!="object")){if(n.ready){if(this.active!==n)return;s.type==="pong"&&(n.lastPong=Date.now()),this.onmessage(s);return}if(n.frames.length>=50){this.fail(n);return}n.frames.push(s),s.type==="hello"&&!n.activating&&Number.isInteger(s.you)&&s.you>=1&&s.you<=25&&typeof s.resume=="string"&&/^[0-9a-f]{32}$/.test(s.resume)&&(n.activating=!0,this.activate(n).catch(()=>this.fail(n)))}}}async activate(t){let r=this.active,n=this.lastIndex!==-1&&this.lastIndex!==t.index;if(this.active=t,this.probe===t&&(this.probe=null),this.pending===t&&(this.pending=null),r&&r!==t&&this.retire(r),await this.onactive(t.ws,t.index,n),!(t.retired||this.stopped||this.active!==t)){clearTimeout(t.timeout),this.lastIndex=t.index,this.backoff=1e3,t.ready=!0,t.lastPong=t.lastPing=Date.now();for(let o of t.frames)this.onmessage(o);t.frames=[]}}retire(t){if(!(!t||t.retired)){t.retired=!0,clearTimeout(t.timeout);try{t.ws?.close(4e3)}catch{}}}fail(t,r){if(!(t.retired||this.stopped)){if(this.retire(t),this.probe===t){this.probe=null;return}if(this.pending===t&&(this.pending=null),this.active===t&&(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((t.index+1)%this.relays.length),this.backoff),this.backoff=Math.min(this.backoff*2,3e4),r!==1006&&this.onnotice("\u8FDE\u63A5\u65AD\u4E86\uFF0C\u6B63\u5728\u6362\u4E00\u4E2A\u4E2D\u7EE7\u91CD\u8BD5\u2026\u2026")}}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)}};var Lt=typeof globalThis=="object"&&"crypto"in globalThis?globalThis.crypto:void 0;function Ct(e){return e instanceof Uint8Array||ArrayBuffer.isView(e)&&e.constructor.name==="Uint8Array"}function oe(e){if(!Number.isSafeInteger(e)||e<0)throw new Error("positive integer expected, got "+e)}function ht(e,...t){if(!Ct(e))throw new Error("Uint8Array expected");if(t.length>0&&!t.includes(e.length))throw new Error("Uint8Array expected of length "+t+", got length="+e.length)}function Le(e){if(typeof e!="function"||typeof e.create!="function")throw new Error("Hash should be wrapped by utils.createHasher");oe(e.outputLen),oe(e.blockLen)}function Vt(e,t=!0){if(e.destroyed)throw new Error("Hash instance has been destroyed");if(t&&e.finished)throw new Error("Hash#digest() has already been called")}function $n(e,t){ht(e);let r=t.outputLen;if(e.length>>t}var Kn=typeof Uint8Array.from([]).toHex=="function"&&typeof Uint8Array.fromHex=="function",Gs=Array.from({length:256},(e,t)=>t.toString(16).padStart(2,"0"));function xt(e){if(ht(e),Kn)return e.toHex();let t="";for(let r=0;r=gt._0&&e<=gt._9)return e-gt._0;if(e>=gt.A&&e<=gt.F)return e-(gt.A-10);if(e>=gt.a&&e<=gt.f)return e-(gt.a-10)}function $t(e){if(typeof e!="string")throw new Error("hex string expected, got "+typeof e);if(Kn)return Uint8Array.fromHex(e);let t=e.length,r=t/2;if(t%2)throw new Error("hex string expected, got unpadded hex of length "+t);let n=new Uint8Array(r);for(let o=0,s=0;oe().update(se(n)).digest(),r=e();return t.outputLen=r.outputLen,t.blockLen=r.blockLen,t.create=()=>e(),t}function Kt(e=32){if(Lt&&typeof Lt.getRandomValues=="function")return Lt.getRandomValues(new Uint8Array(e));if(Lt&&typeof Lt.randomBytes=="function")return Uint8Array.from(Lt.randomBytes(e));throw new Error("crypto.getRandomValues must be defined")}function zs(e,t,r,n){if(typeof e.setBigUint64=="function")return e.setBigUint64(t,r,n);let o=BigInt(32),s=BigInt(4294967295),i=Number(r>>o&s),c=Number(r&s),a=n?4:0,f=n?0:4;e.setUint32(t+a,i,n),e.setUint32(t+f,c,n)}function Mn(e,t,r){return e&t^~e&r}function Dn(e,t,r){return e&t^e&r^t&r}var $e=class extends Pt{constructor(t,r,n,o){super(),this.finished=!1,this.length=0,this.pos=0,this.destroyed=!1,this.blockLen=t,this.outputLen=r,this.padOffset=n,this.isLE=o,this.buffer=new Uint8Array(t),this.view=Ce(this.buffer)}update(t){Vt(this),t=se(t),ht(t);let{view:r,buffer:n,blockLen:o}=this,s=t.length;for(let i=0;io-i&&(this.process(n,0),i=0);for(let u=i;ud.length)throw new Error("_sha2: outputLen bigger than state");for(let u=0;u>>3,m=ut(h,17)^ut(h,19)^h>>>10;Tt[u]=m+Tt[u-7]+y+Tt[u-16]|0}let{A:n,B:o,C:s,D:i,E:c,F:a,G:f,H:d}=this;for(let u=0;u<64;u++){let l=ut(c,6)^ut(c,11)^ut(c,25),h=d+l+Mn(c,a,f)+Zs[u]+Tt[u]|0,m=(ut(n,2)^ut(n,13)^ut(n,22))+Dn(n,o,s)|0;d=f,f=a,a=c,c=i+h|0,i=s,s=o,o=n,n=h+m|0}n=n+this.A|0,o=o+this.B|0,s=s+this.C|0,i=i+this.D|0,c=c+this.E|0,a=a+this.F|0,f=f+this.G|0,d=d+this.H|0,this.set(n,o,s,i,c,a,f,d)}roundClean(){Rt(Tt)}destroy(){this.set(0,0,0,0,0,0,0,0),Rt(this.buffer)}};var Gt=Nn(()=>new Ke);var Ne=class extends Pt{constructor(t,r){super(),this.finished=!1,this.destroyed=!1,Le(t);let n=se(r);if(this.iHash=t.create(),typeof this.iHash.update!="function")throw new Error("Expected instance of class which extends utils.Hash");this.blockLen=this.iHash.blockLen,this.outputLen=this.iHash.outputLen;let o=this.blockLen,s=new Uint8Array(o);s.set(n.length>o?t.create().update(n).digest():n);for(let i=0;inew Ne(e,t).update(r).digest();hr.create=(e,t)=>new Ne(e,t);var pr=BigInt(0),dr=BigInt(1);function ie(e,t=""){if(typeof e!="boolean"){let r=t&&`"${t}"`;throw new Error(r+"expected boolean, got type="+typeof e)}return e}function It(e,t,r=""){let n=Ct(e),o=e?.length,s=t!==void 0;if(!n||s&&o!==t){let i=r&&`"${r}" `,c=s?` of length ${t}`:"",a=n?`length=${o}`:`type=${typeof e}`;throw new Error(i+"expected Uint8Array"+c+", got "+a)}return e}function ce(e){let t=e.toString(16);return t.length&1?"0"+t:t}function qn(e){if(typeof e!="string")throw new Error("hex string expected, got "+typeof e);return e===""?pr:BigInt("0x"+e)}function Et(e){return qn(xt(e))}function yr(e){return ht(e),qn(xt(Uint8Array.from(e).reverse()))}function zt(e,t){return $t(e.toString(16).padStart(t*2,"0"))}function mr(e,t){return zt(e,t).reverse()}function G(e,t,r){let n;if(typeof t=="string")try{n=$t(t)}catch(s){throw new Error(e+" must be hex string or Uint8Array, cause: "+s)}else if(Ct(t))n=Uint8Array.from(t);else throw new Error(e+" must be hex string or Uint8Array");let o=n.length;if(typeof r=="number"&&o!==r)throw new Error(e+" of length "+r+" expected, got "+o);return n}var ur=e=>typeof e=="bigint"&&pr<=e;function ae(e,t,r){return ur(e)&&ur(t)&&ur(r)&&t<=e&&epr;e>>=dr,t+=1);return t}var Ot=e=>(dr<new Uint8Array(h),o=h=>Uint8Array.of(h),s=n(e),i=n(e),c=0,a=()=>{s.fill(1),i.fill(0),c=0},f=(...h)=>r(i,s,...h),d=(h=n(0))=>{i=f(o(0),h),s=f(),h.length!==0&&(i=f(o(1),h),s=f())},u=()=>{if(c++>=1e3)throw new Error("drbg: tried 1000 values");let h=0,y=[];for(;h{a(),d(h);let m;for(;!(m=y(u()));)d();return a(),m}}function fe(e,t,r={}){if(!e||typeof e!="object")throw new Error("expected valid options object");function n(o,s,i){let c=e[o];if(i&&c===void 0)return;let a=typeof c;if(a!==s||c===null)throw new Error(`param "${o}" is invalid: expected ${s}, got ${a}`)}Object.entries(t).forEach(([o,s])=>n(o,s,!1)),Object.entries(r).forEach(([o,s])=>n(o,s,!0))}function wr(e){let t=new WeakMap;return(r,...n)=>{let o=t.get(r);if(o!==void 0)return o;let s=e(r,...n);return t.set(r,s),s}}var et=BigInt(0),Q=BigInt(1),Nt=BigInt(2),zn=BigInt(3),Zn=BigInt(4),Xn=BigInt(5),Xs=BigInt(7),Fn=BigInt(8),Fs=BigInt(9),Yn=BigInt(16);function it(e,t){let r=e%t;return r>=et?r:t+r}function ct(e,t,r){let n=e;for(;t-- >et;)n*=n,n%=r;return n}function Vn(e,t){if(e===et)throw new Error("invert: expected non-zero number");if(t<=et)throw new Error("invert: expected positive modulus, got "+t);let r=it(e,t),n=t,o=et,s=Q,i=Q,c=et;for(;r!==et;){let f=n/r,d=n%r,u=o-i*f,l=s-c*f;n=r,r=d,o=i,s=c,i=u,c=l}if(n!==Q)throw new Error("invert: does not exist");return it(o,t)}function br(e,t,r){if(!e.eql(e.sqr(t),r))throw new Error("Cannot find square root")}function Wn(e,t){let r=(e.ORDER+Q)/Zn,n=e.pow(t,r);return br(e,n,t),n}function Ys(e,t){let r=(e.ORDER-Xn)/Fn,n=e.mul(t,Nt),o=e.pow(n,r),s=e.mul(t,o),i=e.mul(e.mul(s,Nt),o),c=e.mul(s,e.sub(i,e.ONE));return br(e,c,t),c}function Ws(e){let t=_t(e),r=Jn(e),n=r(t,t.neg(t.ONE)),o=r(t,n),s=r(t,t.neg(n)),i=(e+Xs)/Yn;return(c,a)=>{let f=c.pow(a,i),d=c.mul(f,n),u=c.mul(f,o),l=c.mul(f,s),h=c.eql(c.sqr(d),a),y=c.eql(c.sqr(u),a);f=c.cmov(f,d,h),d=c.cmov(l,u,y);let m=c.eql(c.sqr(d),a),k=c.cmov(f,d,m);return br(c,k,a),k}}function Jn(e){if(e1e3)throw new Error("Cannot find square root: probably non-prime P");if(r===1)return Wn;let s=o.pow(n,t),i=(t+Q)/Nt;return function(a,f){if(a.is0(f))return f;if(Gn(a,f)!==1)throw new Error("Cannot find square root");let d=r,u=a.mul(a.ONE,s),l=a.pow(f,t),h=a.pow(f,i);for(;!a.eql(l,a.ONE);){if(a.is0(l))return a.ZERO;let y=1,m=a.sqr(l);for(;!a.eql(m,a.ONE);)if(y++,m=a.sqr(m),y===d)throw new Error("Cannot find square root");let k=Q<(n[o]="function",n),t);return fe(e,r),e}function ti(e,t,r){if(ret;)r&Q&&(n=e.mul(n,o)),o=e.sqr(o),r>>=Q;return n}function De(e,t,r=!1){let n=new Array(t.length).fill(r?e.ZERO:void 0),o=t.reduce((i,c,a)=>e.is0(c)?i:(n[a]=i,e.mul(i,c)),e.ONE),s=e.inv(o);return t.reduceRight((i,c,a)=>e.is0(c)?i:(n[a]=e.mul(i,n[a]),e.mul(i,c)),s),n}function Gn(e,t){let r=(e.ORDER-Q)/Nt,n=e.pow(t,r),o=e.eql(n,e.ONE),s=e.eql(n,e.ZERO),i=e.eql(n,e.neg(e.ONE));if(!o&&!s&&!i)throw new Error("invalid Legendre symbol result");return o?1:s?0:-1}function qe(e,t){t!==void 0&&oe(t);let r=t!==void 0?t:e.toString(2).length,n=Math.ceil(r/8);return{nBitLength:r,nByteLength:n}}function _t(e,t,r=!1,n={}){if(e<=et)throw new Error("invalid field: expected ORDER > 0, got "+e);let o,s,i=!1,c;if(typeof t=="object"&&t!=null){if(n.sqrt||r)throw new Error("cannot specify opts in two arguments");let l=t;l.BITS&&(o=l.BITS),l.sqrt&&(s=l.sqrt),typeof l.isLE=="boolean"&&(r=l.isLE),typeof l.modFromBytes=="boolean"&&(i=l.modFromBytes),c=l.allowedLengths}else typeof t=="number"&&(o=t),n.sqrt&&(s=n.sqrt);let{nBitLength:a,nByteLength:f}=qe(e,o);if(f>2048)throw new Error("invalid field: expected ORDER of <= 2048 bytes");let d,u=Object.freeze({ORDER:e,isLE:r,BITS:a,BYTES:f,MASK:Ot(a),ZERO:et,ONE:Q,allowedLengths:c,create:l=>it(l,e),isValid:l=>{if(typeof l!="bigint")throw new Error("invalid field element: expected bigint, got "+typeof l);return et<=l&&ll===et,isValidNot0:l=>!u.is0(l)&&u.isValid(l),isOdd:l=>(l&Q)===Q,neg:l=>it(-l,e),eql:(l,h)=>l===h,sqr:l=>it(l*l,e),add:(l,h)=>it(l+h,e),sub:(l,h)=>it(l-h,e),mul:(l,h)=>it(l*h,e),pow:(l,h)=>ti(u,l,h),div:(l,h)=>it(l*Vn(h,e),e),sqrN:l=>l*l,addN:(l,h)=>l+h,subN:(l,h)=>l-h,mulN:(l,h)=>l*h,inv:l=>Vn(l,e),sqrt:s||(l=>(d||(d=Js(e)),d(u,l))),toBytes:l=>r?mr(l,f):zt(l,f),fromBytes:(l,h=!0)=>{if(c){if(!c.includes(l.length)||l.length>f)throw new Error("Field.fromBytes: expected "+c+" bytes, got "+l.length);let m=new Uint8Array(f);m.set(l,r?0:m.length-l.length),l=m}if(l.length!==f)throw new Error("Field.fromBytes: expected "+f+" bytes, got "+l.length);let y=r?yr(l):Et(l);if(i&&(y=it(y,e)),!h&&!u.isValid(y))throw new Error("invalid field element: outside of range 0..ORDER");return y},invertBatch:l=>De(u,l),cmov:(l,h,y)=>y?h:l});return Object.freeze(u)}function Qn(e){if(typeof e!="bigint")throw new Error("field order must be bigint");let t=e.toString(2).length;return Math.ceil(t/8)}function xr(e){let t=Qn(e);return t+Math.ceil(t/2)}function je(e,t,r=!1){let n=e.length,o=Qn(t),s=xr(t);if(n<16||n1024)throw new Error("expected "+s+"-1024 bytes of input, got "+n);let i=r?yr(e):Et(e),c=it(i,t-Q)+Q;return r?mr(c,o):zt(c,o)}var Zt=BigInt(0),Mt=BigInt(1);function le(e,t){let r=t.negate();return e?r:t}function Ve(e,t){let r=De(e.Fp,t.map(n=>n.Z));return t.map((n,o)=>e.fromAffine(n.toAffine(r[o])))}function no(e,t){if(!Number.isSafeInteger(e)||e<=0||e>t)throw new Error("invalid window size, expected [1.."+t+"], got W="+e)}function vr(e,t){no(e,t);let r=Math.ceil(t/e)+1,n=2**(e-1),o=2**e,s=Ot(e),i=BigInt(e);return{windows:r,windowSize:n,mask:s,maxNumber:o,shiftBy:i}}function to(e,t,r){let{windowSize:n,mask:o,maxNumber:s,shiftBy:i}=r,c=Number(e&o),a=e>>i;c>n&&(c-=s,a+=Mt);let f=t*n,d=f+Math.abs(c)-1,u=c===0,l=c<0,h=t%2!==0;return{nextN:a,offset:d,isZero:u,isNeg:l,isNegF:h,offsetF:f}}function ei(e,t){if(!Array.isArray(e))throw new Error("array expected");e.forEach((r,n)=>{if(!(r instanceof t))throw new Error("invalid point at index "+n)})}function ri(e,t){if(!Array.isArray(e))throw new Error("array of scalars expected");e.forEach((r,n)=>{if(!t.isValid(r))throw new Error("invalid scalar at index "+n)})}var Er=new WeakMap,oo=new WeakMap;function kr(e){return oo.get(e)||1}function eo(e){if(e!==Zt)throw new Error("invalid wNAF")}var Pe=class{constructor(t,r){this.BASE=t.BASE,this.ZERO=t.ZERO,this.Fn=t.Fn,this.bits=r}_unsafeLadder(t,r,n=this.ZERO){let o=t;for(;r>Zt;)r&Mt&&(n=n.add(o)),o=o.double(),r>>=Mt;return n}precomputeWindow(t,r){let{windows:n,windowSize:o}=vr(r,this.bits),s=[],i=t,c=i;for(let a=0;aZt||n>Zt;)r&Mt&&(s=s.add(o)),n&Mt&&(i=i.add(o)),o=o.double(),r>>=Mt,n>>=Mt;return{p1:s,p2:i}}function io(e,t,r,n){ei(r,e),ri(n,t);let o=r.length,s=n.length;if(o!==s)throw new Error("arrays of points and scalars must have equal length");let i=e.ZERO,c=Me(BigInt(o)),a=1;c>12?a=c-3:c>4?a=c-2:c>0&&(a=2);let f=Ot(a),d=new Array(Number(f)+1).fill(i),u=Math.floor((t.BITS-1)/a)*a,l=i;for(let h=u;h>=0;h-=a){d.fill(i);for(let m=0;m>BigInt(h)&f);d[B]=d[B].add(r[m])}let y=i;for(let m=d.length-1,k=i;m>0;m--)k=k.add(d[m]),y=y.add(k);if(l=l.add(y),h!==0)for(let m=0;mZt))throw new Error(`CURVE.${a} must be positive bigint`)}let o=ro(t.p,r.Fp,n),s=ro(t.n,r.Fn,n),c=["Gx","Gy","a",e==="weierstrass"?"b":"d"];for(let a of c)if(!o.isValid(t[a]))throw new Error(`CURVE.${a} must be valid field element of CURVE.Fp`);return t=Object.freeze(Object.assign({},t)),{CURVE:t,Fp:o,Fn:s}}var ao=(e,t)=>(e+(e>=0?t:-t)/fo)/t;function ni(e,t,r){let[[n,o],[s,i]]=t,c=ao(i*e,r),a=ao(-o*e,r),f=e-c*n-a*s,d=-c*o-a*i,u=f=h||d=h)throw new Error("splitScalar (endomorphism): failed, k="+e);return{k1neg:u,k1:f,k2neg:l,k2:d}}function Sr(e){if(!["compact","recovered","der"].includes(e))throw new Error('Signature format must be "compact", "recovered", or "der"');return e}function Ar(e,t){let r={};for(let n of Object.keys(t))r[n]=e[n]===void 0?t[n]:e[n];return ie(r.lowS,"lowS"),ie(r.prehash,"prehash"),r.format!==void 0&&Sr(r.format),r}var Br=class extends Error{constructor(t=""){super(t)}},kt={Err:Br,_tlv:{encode:(e,t)=>{let{Err:r}=kt;if(e<0||e>256)throw new r("tlv.encode: wrong tag");if(t.length&1)throw new r("tlv.encode: unpadded data");let n=t.length/2,o=ce(n);if(o.length/2&128)throw new r("tlv.encode: long form length too big");let s=n>127?ce(o.length/2|128):"";return ce(e)+s+o+t},decode(e,t){let{Err:r}=kt,n=0;if(e<0||e>256)throw new r("tlv.encode: wrong tag");if(t.length<2||t[n++]!==e)throw new r("tlv.decode: wrong tlv");let o=t[n++],s=!!(o&128),i=0;if(!s)i=o;else{let a=o&127;if(!a)throw new r("tlv.decode(long): indefinite length not supported");if(a>4)throw new r("tlv.decode(long): byte length is too big");let f=t.subarray(n,n+a);if(f.length!==a)throw new r("tlv.decode: length bytes not complete");if(f[0]===0)throw new r("tlv.decode(long): zero leftmost byte");for(let d of f)i=i<<8|d;if(n+=a,i<128)throw new r("tlv.decode(long): not minimal encoding")}let c=t.subarray(n,n+i);if(c.length!==i)throw new r("tlv.decode: wrong value length");return{v:c,l:t.subarray(n+i)}}},_int:{encode(e){let{Err:t}=kt;if(e{let{X:w,Y:p,Z:E}=L;if(n.eql(E,n.ONE))return{x:w,y:p};let S=L.is0();b==null&&(b=S?n.ONE:n.inv(E));let _=n.mul(w,b),O=n.mul(p,b),T=n.mul(E,b);if(S)return{x:n.ZERO,y:n.ZERO};if(!n.eql(T,n.ONE))throw new Error("invZ was invalid");return{x:_,y:O}}),I=wr(L=>{if(L.is0()){if(t.allowInfinityPoint&&!n.is0(L.Y))return;throw new Error("bad point: ZERO")}let{x:b,y:w}=L.toAffine();if(!n.isValid(b)||!n.isValid(w))throw new Error("bad point: x or y not field elements");if(!k(b,w))throw new Error("bad point: equation left != right");if(!L.isTorsionFree())throw new Error("bad point: not in prime-order subgroup");return!0});function H(L,b,w,p,E){return w=new x(n.mul(w.X,L),w.Y,w.Z),b=le(p,b),w=le(E,w),b.add(w)}class x{constructor(b,w,p){this.X=g("x",b),this.Y=g("y",w,!0),this.Z=g("z",p),Object.freeze(this)}static CURVE(){return s}static fromAffine(b){let{x:w,y:p}=b||{};if(!b||!n.isValid(w)||!n.isValid(p))throw new Error("invalid affine point");if(b instanceof x)throw new Error("projective point not allowed");return n.is0(w)&&n.is0(p)?x.ZERO:new x(w,p,n.ONE)}static fromBytes(b){let w=x.fromAffine(y(It(b,void 0,"point")));return w.assertValidity(),w}static fromHex(b){return x.fromBytes(G("pointHex",b))}get x(){return this.toAffine().x}get y(){return this.toAffine().y}precompute(b=8,w=!0){return U.createCache(this,b),w||this.multiply(Ge),this}assertValidity(){I(this)}hasEvenY(){let{y:b}=this.toAffine();if(!n.isOdd)throw new Error("Field doesn't support isOdd");return!n.isOdd(b)}equals(b){v(b);let{X:w,Y:p,Z:E}=this,{X:S,Y:_,Z:O}=b,T=n.eql(n.mul(w,O),n.mul(S,E)),C=n.eql(n.mul(p,O),n.mul(_,E));return T&&C}negate(){return new x(this.X,n.neg(this.Y),this.Z)}double(){let{a:b,b:w}=s,p=n.mul(w,Ge),{X:E,Y:S,Z:_}=this,O=n.ZERO,T=n.ZERO,C=n.ZERO,K=n.mul(E,E),Z=n.mul(S,S),j=n.mul(_,_),D=n.mul(E,S);return D=n.add(D,D),C=n.mul(E,_),C=n.add(C,C),O=n.mul(b,C),T=n.mul(p,j),T=n.add(O,T),O=n.sub(Z,T),T=n.add(Z,T),T=n.mul(O,T),O=n.mul(D,O),C=n.mul(p,C),j=n.mul(b,j),D=n.sub(K,j),D=n.mul(b,D),D=n.add(D,C),C=n.add(K,K),K=n.add(C,K),K=n.add(K,j),K=n.mul(K,D),T=n.add(T,K),j=n.mul(S,_),j=n.add(j,j),K=n.mul(j,D),O=n.sub(O,K),C=n.mul(j,Z),C=n.add(C,C),C=n.add(C,C),new x(O,T,C)}add(b){v(b);let{X:w,Y:p,Z:E}=this,{X:S,Y:_,Z:O}=b,T=n.ZERO,C=n.ZERO,K=n.ZERO,Z=s.a,j=n.mul(s.b,Ge),D=n.mul(w,S),P=n.mul(p,_),X=n.mul(E,O),rt=n.add(w,p),V=n.add(S,_);rt=n.mul(rt,V),V=n.add(D,P),rt=n.sub(rt,V),V=n.add(w,E);let F=n.add(S,O);return V=n.mul(V,F),F=n.add(D,X),V=n.sub(V,F),F=n.add(p,E),T=n.add(_,O),F=n.mul(F,T),T=n.add(P,X),F=n.sub(F,T),K=n.mul(Z,V),T=n.mul(j,X),K=n.add(T,K),T=n.sub(P,K),K=n.add(P,K),C=n.mul(T,K),P=n.add(D,D),P=n.add(P,D),X=n.mul(Z,X),V=n.mul(j,V),P=n.add(P,X),X=n.sub(D,X),X=n.mul(Z,X),V=n.add(V,X),D=n.mul(P,V),C=n.add(C,D),D=n.mul(F,V),T=n.mul(rt,T),T=n.sub(T,D),D=n.mul(rt,P),K=n.mul(F,K),K=n.add(K,D),new x(T,C,K)}subtract(b){return this.add(b.negate())}is0(){return this.equals(x.ZERO)}multiply(b){let{endo:w}=t;if(!o.isValidNot0(b))throw new Error("invalid scalar: out of range");let p,E,S=_=>U.cached(this,_,O=>Ve(x,O));if(w){let{k1neg:_,k1:O,k2neg:T,k2:C}=A(b),{p:K,f:Z}=S(O),{p:j,f:D}=S(C);E=Z.add(D),p=H(w.beta,K,j,_,T)}else{let{p:_,f:O}=S(b);p=_,E=O}return Ve(x,[p,E])[0]}multiplyUnsafe(b){let{endo:w}=t,p=this;if(!o.isValid(b))throw new Error("invalid scalar: out of range");if(b===At||p.is0())return x.ZERO;if(b===Xt)return p;if(U.hasCache(this))return this.multiply(b);if(w){let{k1neg:E,k1:S,k2neg:_,k2:O}=A(b),{p1:T,p2:C}=so(x,p,S,O);return H(w.beta,T,C,E,_)}else return U.unsafe(p,b)}multiplyAndAddUnsafe(b,w,p){let E=this.multiplyUnsafe(w).add(b.multiplyUnsafe(p));return E.is0()?void 0:E}toAffine(b){return R(this,b)}isTorsionFree(){let{isTorsionFree:b}=t;return i===Xt?!0:b?b(x,this):U.unsafe(this,c).is0()}clearCofactor(){let{clearCofactor:b}=t;return i===Xt?this:b?b(x,this):this.multiplyUnsafe(i)}isSmallOrder(){return this.multiplyUnsafe(i).is0()}toBytes(b=!0){return ie(b,"isCompressed"),this.assertValidity(),h(x,this,b)}toHex(b=!0){return xt(this.toBytes(b))}toString(){return``}get px(){return this.X}get py(){return this.X}get pz(){return this.Z}toRawBytes(b=!0){return this.toBytes(b)}_setWindowSize(b){this.precompute(b)}static normalizeZ(b){return Ve(x,b)}static msm(b,w){return io(x,o,b,w)}static fromPrivateKey(b){return x.BASE.multiply(Ht(o,b))}}x.BASE=new x(s.Gx,s.Gy,n.ONE),x.ZERO=new x(n.ZERO,n.ONE,n.ZERO),x.Fp=n,x.Fn=o;let M=o.BITS,U=new Pe(x,t.endo?Math.ceil(M/2):M);return x.BASE.precompute(8),x}function lo(e){return Uint8Array.of(e?2:3)}function ho(e,t){return{secretKey:t.BYTES,publicKey:1+e.BYTES,publicKeyUncompressed:1+2*e.BYTES,publicKeyHasPrefix:!0,signature:2*t.BYTES}}function ii(e,t={}){let{Fn:r}=e,n=t.randomBytes||Kt,o=Object.assign(ho(e.Fp,r),{seed:xr(r.ORDER)});function s(h){try{return!!Ht(r,h)}catch{return!1}}function i(h,y){let{publicKey:m,publicKeyUncompressed:k}=o;try{let B=h.length;return y===!0&&B!==m||y===!1&&B!==k?!1:!!e.fromBytes(h)}catch{return!1}}function c(h=n(o.seed)){return je(It(h,o.seed,"seed"),r.ORDER)}function a(h,y=!0){return e.BASE.multiply(Ht(r,h)).toBytes(y)}function f(h){let y=c(h);return{secretKey:y,publicKey:a(y)}}function d(h){if(typeof h=="bigint")return!1;if(h instanceof e)return!0;let{secretKey:y,publicKey:m,publicKeyUncompressed:k}=o;if(r.allowedLengths||y===m)return;let B=G("key",h).length;return B===m||B===k}function u(h,y,m=!0){if(d(h)===!0)throw new Error("first arg must be private key");if(d(y)===!1)throw new Error("second arg must be public key");let k=Ht(r,h);return e.fromHex(y).multiply(k).toBytes(m)}return Object.freeze({getPublicKey:a,getSharedSecret:u,keygen:f,Point:e,utils:{isValidSecretKey:s,isValidPublicKey:i,randomSecretKey:c,isValidPrivateKey:s,randomPrivateKey:c,normPrivateKeyToScalar:h=>Ht(r,h),precompute(h=8,y=e.BASE){return y.precompute(h,!1)}},lengths:o})}function ci(e,t,r={}){Le(t),fe(r,{},{hmac:"function",lowS:"boolean",randomBytes:"function",bits2int:"function",bits2int_modN:"function"});let n=r.randomBytes||Kt,o=r.hmac||((w,...p)=>hr(t,w,ot(...p))),{Fp:s,Fn:i}=e,{ORDER:c,BITS:a}=i,{keygen:f,getPublicKey:d,getSharedSecret:u,utils:l,lengths:h}=ii(e,r),y={prehash:!1,lowS:typeof r.lowS=="boolean"?r.lowS:!1,format:void 0,extraEntropy:!1},m="compact";function k(w){let p=c>>Xt;return w>p}function B(w,p){if(!i.isValidNot0(p))throw new Error(`invalid signature ${w}: out of range 1..Point.Fn.ORDER`);return p}function $(w,p){Sr(p);let E=h.signature,S=p==="compact"?E:p==="recovered"?E+1:void 0;return It(w,S,`${p} signature`)}class g{constructor(p,E,S){this.r=B("r",p),this.s=B("s",E),S!=null&&(this.recovery=S),Object.freeze(this)}static fromBytes(p,E=m){$(p,E);let S;if(E==="der"){let{r:C,s:K}=kt.toSig(It(p));return new g(C,K)}E==="recovered"&&(S=p[0],E="compact",p=p.subarray(1));let _=i.BYTES,O=p.subarray(0,_),T=p.subarray(_,_*2);return new g(i.fromBytes(O),i.fromBytes(T),S)}static fromHex(p,E){return this.fromBytes($t(p),E)}addRecoveryBit(p){return new g(this.r,this.s,p)}recoverPublicKey(p){let E=s.ORDER,{r:S,s:_,recovery:O}=this;if(O==null||![0,1,2,3].includes(O))throw new Error("recovery id invalid");if(c*fo1)throw new Error("recovery id is ambiguous for h>1 curve");let C=O===2||O===3?S+c:S;if(!s.isValid(C))throw new Error("recovery id 2 or 3 invalid");let K=s.toBytes(C),Z=e.fromBytes(ot(lo((O&1)===0),K)),j=i.inv(C),D=A(G("msgHash",p)),P=i.create(-D*j),X=i.create(_*j),rt=e.BASE.multiplyUnsafe(P).add(Z.multiplyUnsafe(X));if(rt.is0())throw new Error("point at infinify");return rt.assertValidity(),rt}hasHighS(){return k(this.s)}toBytes(p=m){if(Sr(p),p==="der")return $t(kt.hexFromSig(this));let E=i.toBytes(this.r),S=i.toBytes(this.s);if(p==="recovered"){if(this.recovery==null)throw new Error("recovery bit must be present");return ot(Uint8Array.of(this.recovery),E,S)}return ot(E,S)}toHex(p){return xt(this.toBytes(p))}assertValidity(){}static fromCompact(p){return g.fromBytes(G("sig",p),"compact")}static fromDER(p){return g.fromBytes(G("sig",p),"der")}normalizeS(){return this.hasHighS()?new g(this.r,i.neg(this.s),this.recovery):this}toDERRawBytes(){return this.toBytes("der")}toDERHex(){return xt(this.toBytes("der"))}toCompactRawBytes(){return this.toBytes("compact")}toCompactHex(){return xt(this.toBytes("compact"))}}let v=r.bits2int||function(p){if(p.length>8192)throw new Error("input is too large");let E=Et(p),S=p.length*8-a;return S>0?E>>BigInt(S):E},A=r.bits2int_modN||function(p){return i.create(v(p))},R=Ot(a);function I(w){return jn("num < 2^"+a,w,At,R),i.toBytes(w)}function H(w,p){return It(w,void 0,"message"),p?It(t(w),void 0,"prehashed message"):w}function x(w,p,E){if(["recovered","canonical"].some(P=>P in E))throw new Error("sign() legacy options not supported");let{lowS:S,prehash:_,extraEntropy:O}=Ar(E,y);w=H(w,_);let T=A(w),C=Ht(i,p),K=[I(C),I(T)];if(O!=null&&O!==!1){let P=O===!0?n(h.secretKey):O;K.push(G("extraEntropy",P))}let Z=ot(...K),j=T;function D(P){let X=v(P);if(!i.isValidNot0(X))return;let rt=i.inv(X),V=e.BASE.multiply(X).toAffine(),F=i.create(V.x);if(F===At)return;let pe=i.create(rt*i.create(j+F*C));if(pe===At)return;let Dr=(V.x===F?0:2)|Number(V.y&Xt),qr=pe;return S&&k(pe)&&(qr=i.neg(pe),Dr^=1),new g(F,qr,Dr)}return{seed:Z,k2sig:D}}function M(w,p,E={}){w=G("message",w);let{seed:S,k2sig:_}=x(w,p,E);return Pn(t.outputLen,i.BYTES,o)(S,_)}function U(w){let p,E=typeof w=="string"||Ct(w),S=!E&&w!==null&&typeof w=="object"&&typeof w.r=="bigint"&&typeof w.s=="bigint";if(!E&&!S)throw new Error("invalid signature, expected Uint8Array, hex string or Signature instance");if(S)p=new g(w.r,w.s);else if(E){try{p=g.fromBytes(G("sig",w),"der")}catch(_){if(!(_ instanceof kt.Err))throw _}if(!p)try{p=g.fromBytes(G("sig",w),"compact")}catch{return!1}}return p||!1}function L(w,p,E,S={}){let{lowS:_,prehash:O,format:T}=Ar(S,y);if(E=G("publicKey",E),p=H(G("message",p),O),"strict"in S)throw new Error("options.strict was renamed to lowS");let C=T===void 0?U(w):g.fromBytes(G("sig",w),T);if(C===!1)return!1;try{let K=e.fromBytes(E);if(_&&C.hasHighS())return!1;let{r:Z,s:j}=C,D=A(p),P=i.inv(j),X=i.create(D*P),rt=i.create(Z*P),V=e.BASE.multiplyUnsafe(X).add(K.multiplyUnsafe(rt));return V.is0()?!1:i.create(V.x)===Z}catch{return!1}}function b(w,p,E={}){let{prehash:S}=Ar(E,y);return p=H(p,S),g.fromBytes(w,"recovered").recoverPublicKey(p).toBytes()}return Object.freeze({keygen:f,getPublicKey:d,getSharedSecret:u,utils:l,lengths:h,Point:e,sign:M,verify:L,recoverPublicKey:b,Signature:g,hash:t})}function ai(e){let t={a:e.a,b:e.b,p:e.Fp.ORDER,n:e.n,h:e.h,Gx:e.Gx,Gy:e.Gy},r=e.Fp,n=e.allowedPrivateKeyLengths?Array.from(new Set(e.allowedPrivateKeyLengths.map(i=>Math.ceil(i/2)))):void 0,o=_t(t.n,{BITS:e.nBitLength,allowedLengths:n,modFromBytes:e.wrapPrivateKey}),s={Fp:r,Fn:o,allowInfinityPoint:e.allowInfinityPoint,endo:e.endo,isTorsionFree:e.isTorsionFree,clearCofactor:e.clearCofactor,fromBytes:e.fromBytes,toBytes:e.toBytes};return{CURVE:t,curveOpts:s}}function fi(e){let{CURVE:t,curveOpts:r}=ai(e),n={hmac:e.hmac,randomBytes:e.randomBytes,lowS:e.lowS,bits2int:e.bits2int,bits2int_modN:e.bits2int_modN};return{CURVE:t,curveOpts:r,hash:e.hash,ecdsaOpts:n}}function li(e,t){let r=t.Point;return Object.assign({},t,{ProjectivePoint:r,CURVE:Object.assign({},e,qe(r.Fn.ORDER,r.Fn.BITS))})}function uo(e){let{CURVE:t,curveOpts:r,hash:n,ecdsaOpts:o}=fi(e),s=si(t,r),i=ci(s,n,o);return li(e,i)}function po(e,t){let r=n=>uo({...e,hash:n});return{...r(t),create:r}}var Ft={p:BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"),n:BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"),h:BigInt(1),a:BigInt(0),b:BigInt(7),Gx:BigInt("0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"),Gy:BigInt("0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8")},hi={beta:BigInt("0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee"),basises:[[BigInt("0x3086d221a7d46bcde86c90e49284eb15"),-BigInt("0xe4437ed6010e88286f547fa90abfe4c3")],[BigInt("0x114ca50f7a8e2f3f657c1108d9d44cfd8"),BigInt("0x3086d221a7d46bcde86c90e49284eb15")]]},ui=BigInt(0),yo=BigInt(1),Tr=BigInt(2);function di(e){let t=Ft.p,r=BigInt(3),n=BigInt(6),o=BigInt(11),s=BigInt(22),i=BigInt(23),c=BigInt(44),a=BigInt(88),f=e*e*e%t,d=f*f*e%t,u=ct(d,r,t)*d%t,l=ct(u,r,t)*d%t,h=ct(l,Tr,t)*f%t,y=ct(h,o,t)*h%t,m=ct(y,s,t)*y%t,k=ct(m,c,t)*m%t,B=ct(k,a,t)*k%t,$=ct(B,c,t)*m%t,g=ct($,r,t)*d%t,v=ct(g,i,t)*y%t,A=ct(v,n,t)*f%t,R=ct(A,Tr,t);if(!ze.eql(ze.sqr(R),e))throw new Error("Cannot find square root");return R}var ze=_t(Ft.p,{sqrt:di}),bo=po({...Ft,Fp:ze,lowS:!0,endo:hi},Gt),mo={};function Ze(e,...t){let r=mo[e];if(r===void 0){let n=Gt(Re(e));r=ot(n,n),mo[e]=r}return Gt(ot(r,...t))}var Or=e=>e.toBytes(!0).slice(1),Yt=bo.Point,_r=e=>e%Tr===ui;function Ir(e){let{Fn:t,BASE:r}=Yt,n=Ht(t,e),o=r.multiply(n);return{scalar:_r(o.y)?n:t.neg(n),bytes:Or(o)}}function go(e){let t=ze;if(!t.isValidNot0(e))throw new Error("invalid x: Fail if x \u2265 p");let r=t.create(e*e),n=t.create(r*e+BigInt(7)),o=t.sqrt(n);_r(o)||(o=t.neg(o));let s=Yt.fromAffine({x:e,y:o});return s.assertValidity(),s}var he=Et;function xo(...e){return Yt.Fn.create(he(Ze("BIP0340/challenge",...e)))}function wo(e){return Ir(e).bytes}function pi(e,t,r=Kt(32)){let{Fn:n}=Yt,o=G("message",e),{bytes:s,scalar:i}=Ir(t),c=G("auxRand",r,32),a=n.toBytes(i^he(Ze("BIP0340/aux",c))),f=Ze("BIP0340/nonce",a,s,o),{bytes:d,scalar:u}=Ir(f),l=xo(d,s,o),h=new Uint8Array(64);if(h.set(d,0),h.set(n.toBytes(n.create(u+l*i)),32),!vo(h,o,s))throw new Error("sign: Invalid signature produced");return h}function vo(e,t,r){let{Fn:n,BASE:o}=Yt,s=G("signature",e,64),i=G("message",t),c=G("publicKey",r,32);try{let a=go(he(c)),f=he(s.subarray(0,32));if(!ae(f,yo,Ft.p))return!1;let d=he(s.subarray(32,64));if(!ae(d,yo,Ft.n))return!1;let u=xo(n.toBytes(f),Or(a),i),l=o.multiplyUnsafe(d).add(a.multiplyUnsafe(n.neg(u))),{x:h,y}=l.toAffine();return!(l.is0()||!_r(y)||h!==f)}catch{return!1}}var Xe=(()=>{let r=(o=Kt(48))=>je(o,Ft.n);bo.utils.randomSecretKey;function n(o){let s=r(o);return{secretKey:s,publicKey:wo(s)}}return{keygen:n,getPublicKey:wo,sign:pi,verify:vo,Point:Yt,utils:{randomSecretKey:r,randomPrivateKey:r,taggedHash:Ze,lift_x:go,pointToBytes:Or,numberToBytesBE:zt,bytesToNumberBE:Et,mod:it},lengths:{secretKey:32,publicKey:32,publicKeyHasPrefix:!1,signature:64,seed:48}}})();var Eo=Gt;var yi=4e3,ko=14e3,mi=3e3,Ao=16e3,So=6e5,Bo=25,Fe=20111,de=new TextEncoder,Hr=new TextDecoder,wi=e=>{let t="";for(let r=0;rUint8Array.from(atob(e),t=>t.charCodeAt(0)),To=e=>de.encode(`private-talk/html-room/v3/mesh|${e}`),Io=["key","msg","seen"];async function gi(e,t){let r=tt(12),n=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:r,additionalData:To(e.topic)},e.meshKey,de.encode(JSON.stringify(t)))),o=new Uint8Array(12+n.length);return o.set(r),o.set(n,12),wi(o)}async function xi(e,t,r=Date.now()){if(typeof t!="string"||t.length<40||t.length>Ao||!/^[A-Za-z0-9+/]+={0,2}$/.test(t))return null;let n;try{let o=bi(t);n=JSON.parse(Hr.decode(await crypto.subtle.decrypt({name:"AES-GCM",iv:o.subarray(0,12),additionalData:To(e.topic)},e.meshKey,o.subarray(12))))}catch{return null}return!n||typeof n!="object"||!/^[0-9a-f]{16}$/.test(n.d??"")||!Number.isInteger(n.n)||n.n<0||n.n>Bo||typeof n.t!="number"||Math.abs(r-n.t)>So||!["hb","f","bye"].includes(n.k)||n.c!==void 0&&(typeof n.c!="number"||!Number.isFinite(n.c))||n.k==="f"&&(!n.f||typeof n.f!="object"||!Io.includes(n.f.type)||typeof n.f.iv!="string"||typeof n.f.ct!="string")?null:n}var ue=class{constructor(t,r){Object.assign(this,{mesh:t,entry:r,kind:ft(r),url:Bt(r),open:!1,backoff:1e3})}protocols(){}target(){return this.url}connect(){if(this.mesh.stopped)return;let t;try{t=this.ws=this.mesh.createSocket(this.target(),this.protocols())}catch{this.fail();return}t.binaryType="arraybuffer",this.timeout=setTimeout(()=>this.fail(t),1e4),t.onopen=()=>{t===this.ws&&this.opened()},t.onmessage=r=>{t===this.ws&&!this.mesh.stopped&&this.data(r.data)},t.onclose=t.onerror=()=>this.fail(t)}ready(){clearTimeout(this.timeout),this.open=!0,this.backoff=1e3,this.mesh.linkChanged()}fail(t=this.ws){if(t!==this.ws||this.mesh.stopped)return;clearTimeout(this.timeout),clearInterval(this.keepalive),this.ws=null;let r=this.open;this.open=!1;try{t?.close()}catch{}r&&this.mesh.linkChanged(),clearTimeout(this.retry),this.retry=setTimeout(()=>this.connect(),this.backoff),this.backoff=Math.min(this.backoff*2,3e4)}stop(){clearTimeout(this.timeout),clearTimeout(this.retry),clearInterval(this.keepalive);let t=this.ws;this.ws=null,this.open=!1;try{t?.close(1e3)}catch{}}notice(t){this.noticed||(this.noticed=!0,this.mesh.onnotice(`${this.url}\uFF1A${String(t).slice(0,120)}`))}},Ur=class extends ue{target(){return this.url+this.mesh.d.topic}opened(){this.ready()}data(t){typeof t=="string"&&this.mesh.receive(t)}send(t){this.ws.send(t)}},Lr=class extends ue{opened(){this.sub="hr"+z(tt(6)),this.ws.send(JSON.stringify(["REQ",this.sub,{kinds:[Fe],"#t":[this.mesh.d.topic],since:Math.floor(Date.now()/1e3)-30}])),this.ready()}data(t){if(typeof t!="string"||t.length>Ao+2e3)return;let r;try{r=JSON.parse(t)}catch{return}Array.isArray(r)&&(r[0]==="EVENT"&&r[1]===this.sub&&r[2]?.kind===Fe&&typeof r[2].content=="string"?this.mesh.receive(r[2].content):r[0]==="OK"&&r[2]===!1?this.notice(`\u8FD9\u4E2A Nostr \u4E2D\u7EE7\u62D2\u7EDD\u4E86\u6D88\u606F\uFF08${r[3]??""}\uFF09\uFF0C\u53EF\u6362\u4E00\u4E2A`):r[0]==="AUTH"?this.notice("\u8FD9\u4E2A Nostr \u4E2D\u7EE7\u8981\u6C42\u767B\u5F55\uFF0C\u6682\u4E0D\u652F\u6301\uFF0C\u8BF7\u6362\u4E00\u4E2A"):r[0]==="CLOSED"&&r[1]===this.sub&&(this.notice(`\u8FD9\u4E2A Nostr \u4E2D\u7EE7\u505C\u6B62\u4E86\u63A5\u6536\uFF08${r[2]??""}\uFF09\uFF0C\u53EF\u6362\u4E00\u4E2A`),this.fail()))}send(t){this.ws.send(JSON.stringify(["EVENT",vi(this.mesh.nostrKey,this.mesh.d.topic,t)]))}};function vi(e,t,r,n=Date.now()){let o=z(Xe.getPublicKey(e)),s=Math.floor(n/1e3),i=[["t",t]],c=Eo(de.encode(JSON.stringify([0,o,s,Fe,i,r])));return{id:z(c),pubkey:o,created_at:s,kind:Fe,tags:i,content:r,sig:z(Xe.sign(c,e))}}var Ei=e=>{let t=[];do{let r=e%128;e=Math.floor(e/128),e&&(r|=128),t.push(r)}while(e);return t},Ye=e=>{let t=de.encode(e);return[t.length>>8,t.length&255,...t]},Rr=(e,t)=>Uint8Array.from([e,...Ei(t.length),...t]),ki=(e,t=60)=>Rr(16,[...Ye("MQTT"),4,2,t>>8,t&255,...Ye(e)]),Ai=(e,t=1)=>Rr(130,[t>>8,t&255,...Ye(e),0]),Si=(e,t)=>Rr(48,[...Ye(e),...de.encode(t)]);function Bi(e){let t=[],r=0;for(;r=e.length)return{packets:t,rest:e.subarray(r)};if(i=e[s++],n+=(i&127)*o,o*=128,o>128**4)throw Error("bad length")}while(i&128);if(s+n>e.length)return{packets:t,rest:e.subarray(r)};t.push({type:e[r]>>4,flags:e[r]&15,body:e.subarray(s,s+n)}),r=s+n}return{packets:t,rest:e.subarray(r)}}var Cr=class extends ue{protocols(){return["mqtt"]}topic(){return`htmlroom/${this.mesh.d.topic}`}opened(){this.buf=new Uint8Array(0),this.ws.send(ki("hr"+z(tt(8))))}data(t){if(!(t instanceof ArrayBuffer))return;let r=new Uint8Array(this.buf.length+t.byteLength);if(r.set(this.buf),r.set(new Uint8Array(t),this.buf.length),r.length>65536){this.fail();return}let n;try{n=Bi(r)}catch{this.fail();return}this.buf=n.rest.slice();for(let o of n.packets)if(o.type===2){if(o.body[1]!==0){this.notice(`\u8FD9\u4E2A MQTT \u670D\u52A1\u5668\u62D2\u7EDD\u4E86\u8FDE\u63A5\uFF08\u4EE3\u7801 ${o.body[1]}\uFF09\uFF0C\u53EF\u6362\u4E00\u4E2A`),this.fail();return}this.ws.send(Ai(this.topic()))}else if(o.type===9){if(o.body[2]===128){this.notice("\u8FD9\u4E2A MQTT \u670D\u52A1\u5668\u62D2\u7EDD\u4E86\u8BA2\u9605\uFF0C\u53EF\u6362\u4E00\u4E2A"),this.fail();return}this.ready(),this.keepalive=setInterval(()=>{try{this.ws?.send(Uint8Array.of(192,0))}catch{}},3e4)}else if(o.type===3){let s=o.body[0]<<8|o.body[1],i=2+s+(o.flags>>1&3?2:0);Hr.decode(o.body.subarray(2,2+s))===this.topic()&&this.mesh.receive(Hr.decode(o.body.subarray(i)))}}send(t){this.ws.send(Si(this.topic(),t))}},Ti={bc:Ur,nostr:Lr,mqtt:Cr},We=class{constructor(t,{derived:r,onhello:n,onmembers:o,onframe:s,onstatus:i=()=>{},onnotice:c=()=>{},onfatal:a=()=>{},createSocket:f=(l,h)=>h?new WebSocket(l,h):new WebSocket(l),now:d=()=>Date.now(),claimDelay:u=mi+Math.random()*1e3}){Object.assign(this,{d:r,onhello:n,onmembers:o,onframe:s,onstatus:i,onnotice:c,onfatal:a,createSocket:f,now:d,claimDelay:u}),this.node=z(tt(8)),this.me=0,this.peers=new Map,this.known=new Set,this.seenIvs=new Map,this.owners=new Map,this.lastHb=0,this.nostrKey=Xe.utils.randomPrivateKey(),this.links=t.map(h=>new Ti[ft(h)](this,h));let l=this;this.socket={get readyState(){return l.me&&l.openCount()?1:0},send:h=>l.sendSession(h),close(){}}}start(){for(let t of this.links)t.connect();this.claimTimer=setTimeout(()=>this.claim(),this.claimDelay),this.timer=setInterval(()=>this.sweep(),1e3),this.linkChanged()}stop(){this.stopped||(this.me&&this.broadcast({k:"bye"}).catch(()=>{}),this.stopped=!0,clearTimeout(this.claimTimer),clearTimeout(this.answer),clearInterval(this.timer),setTimeout(()=>this.links.forEach(t=>t.stop()),50))}openCount(){return this.links.filter(t=>t.open).length}linkChanged(){let t=this.openCount();t&&!this.me&&!this.stopped&&this.broadcast({k:"hb"}).catch(()=>{}),this.onstatus(t?`\u4E2D\u7EE7\uFF1A\u5DF2\u8FDE\u63A5 ${t}/${this.links.length} \u4E2A\uFF08\u6D88\u606F\u540C\u65F6\u8D70\u6240\u6709\u4E2D\u7EE7\uFF09`:"\u4E2D\u7EE7\u5168\u90E8\u65AD\u5F00\u4E86\uFF0C\u6B63\u5728\u91CD\u8FDE\u2026\u2026")}async broadcast(t){let r=this.links.filter(s=>s.open);if(!r.length)return 0;let n=await gi(this.d,{...t,d:this.node,n:this.me,t:this.now(),...this.me&&{c:this.claimedAt}});this.remember(n.slice(0,16));let o=0;for(let s of r)try{s.send(n),o++}catch{s.fail()}return o}remember(t){for(this.seenIvs.set(t,this.now());this.seenIvs.size>4096;)this.seenIvs.delete(this.seenIvs.keys().next().value)}sendSession(t){let r;try{r=JSON.parse(t)}catch{return}if(!Io.includes(r?.type)||!this.me)return;let n=this.me;this.broadcast({k:"f",f:r}).then(o=>{o&&r.type==="msg"&&n===this.me&&this.onframe(r,n)}).catch(()=>{})}async receive(t){if(this.stopped||typeof t!="string")return;let r=t.slice(0,16);if(this.seenIvs.has(r))return;this.remember(r);let n=await xi(this.d,t,this.now());!n||n.d===this.node||this.stopped||(!this.known.has(n.d)&&n.k!=="bye"&&(this.known.add(n.d),this.answerSoon()),n.k==="bye"?this.peers.delete(n.d):n.n>0&&this.peers.set(n.d,{n:n.n,c:n.c??n.t,seen:this.now()}),this.reconcile(),n.k==="f"&&n.n>0&&this.owners.get(n.n)===n.d&&this.onframe({...n.f,at:this.now()},n.n))}answerSoon(){!this.me||this.answer||(this.answer=setTimeout(()=>{this.answer=null,this.lastHb=0,this.sweep()},50+Math.random()*250))}claim(){if(this.stopped)return;let t=new Set([...this.live()].map(([,n])=>n.n)),r=1;for(;t.has(r);)r++;if(r>Bo){this.stop(),this.onfatal("\u623F\u95F4\u5DF2\u6EE1\uFF0825 \u4EBA\uFF09\u3002");return}this.me=r,this.claimedAt=this.now(),this.lastHb=0,this.onhello(r),this.sweep()}*live(){let t=this.now();for(let r of this.peers)t-r[1].seen<=ko&&(yield r)}reconcile(){let t=new Map,r=new Map,n=(i,c,a)=>{let f=r.get(i);(!f||athis.owners.has(i)&&this.owners.get(i)!==c).map(([i])=>i);t.size===this.owners.size&&[...t].every(([i,c])=>this.owners.get(i)===c)||(o.length&&this.onmembers([...this.owners.keys()].filter(i=>!o.includes(i)).sort((i,c)=>i-c)),this.owners=t,this.me&&this.onmembers([...t.keys()].sort((i,c)=>i-c)))}sweep(){if(this.stopped)return;let t=this.now();for(let[r,n]of this.peers)t-n.seen>ko*4&&this.peers.delete(r);for(let[r,n]of this.seenIvs)if(t-n>So*2)this.seenIvs.delete(r);else break;this.reconcile(),this.me&&t-this.lastHb>=yi&&this.openCount()&&(this.lastHb=t,this.broadcast({k:"hb"}).catch(()=>{}))}};function Oo(e){let t=f=>document.getElementById(f),r=f=>{t("status").textContent=f};t("preview-help").hidden=!0;let n=0,o=!1,s=0,i,c=new Map;function a(f,d){if((f==="notice"||f==="fatal")&&r(d),f==="relay-change"&&(t("safety").textContent="\u6362\u4E86\u4E2D\u7EE7\uFF0C\u6B63\u5728\u91CD\u65B0\u786E\u8BA4\u6210\u5458\u3002\u8BF7\u518D\u6838\u5BF9\u4E00\u6B21\u5B89\u5168\u7801\u3002"),f==="presence"&&(t("presence").textContent=`\u5728\u7EBF ${d} \u4EBA`),f==="ready"&&(r("\u2705 \u5DF2\u52A0\u5BC6\u8FDE\u63A5\uFF0C\u53EF\u4EE5\u804A\u5929\u4E86"),t("safety").textContent=`\u5B89\u5168\u7801\uFF1A${d}\u3002\u8BF7\u6253\u7535\u8BDD\u6216\u5F53\u9762\u548C\u5BF9\u65B9\u6838\u5BF9\uFF0C\u4E00\u6837\u624D\u8BF4\u660E\u6CA1\u6709\u4EBA\u5192\u5145\u3002`),f==="message"){let u=document.createElement("li");u.textContent=`${d.from}\uFF1A${d.text}`;let l=document.createElement("small");u.append(l),t("messages").append(u),c.set(d.id,l),c.size>200&&(c.delete(c.keys().next().value),t("messages").firstChild.remove())}f==="delivery"&&c.has(d.id)&&(c.get(d.id).textContent=d.text)}if(!globalThis.crypto?.subtle||!globalThis.WebSocket||!globalThis.TextEncoder)r("\u8FD9\u4E2A\u9884\u89C8\u6253\u4E0D\u5F00\u804A\u5929\u5BA4\uFF0C\u8BF7\u7528 Safari\u3001Chrome \u7B49\u6D4F\u89C8\u5668\u6253\u5F00\u8FD9\u4E2A\u6587\u4EF6\u3002"),t("enter").disabled=!0;else if(e.version)try{lt(e),r(e.encrypted?"\u8BF7\u8F93\u5165\u53E3\u4EE4\uFF0C\u7136\u540E\u70B9\u300C\u8FDB\u5165\u623F\u95F4\u300D\u3002":"\u70B9\u300C\u8FDB\u5165\u623F\u95F4\u300D\u5F00\u59CB\u804A\u5929\u3002"),t("password").disabled=!1,t("enter").disabled=!1}catch{r("\u623F\u95F4\u6587\u4EF6\u6709\u95EE\u9898\uFF0C\u8BF7\u8BA9\u5BF9\u65B9\u91CD\u65B0\u53D1\u4E00\u4EFD\u3002"),t("enter").disabled=!0}else r("\u8FD9\u662F\u7A7A\u767D\u6A21\u677F\uFF0C\u8BF7\u5148\u5728\u521B\u5EFA\u9875\u9762\u91CC\u521B\u5EFA\u623F\u95F4\u3002"),t("enter").disabled=!0;t("unlock").addEventListener("submit",async f=>{if(f.preventDefault(),!(o||Date.now()i.run(()=>(i.ws=u.socket,r("\u5DF2\u8FDB\u5165\u623F\u95F4\uFF0C\u6B63\u5728\u786E\u8BA4\u5176\u4ED6\u6210\u5458\u2026\u2026"),i.incoming({type:"hello",you:l,resume:""}))),onmembers:l=>i.run(()=>i.incoming({type:"members",members:l})),onframe:(l,h)=>i.run(()=>i.incoming({...l,from:h})),onstatus:l=>{t("relay-status").textContent=l},onnotice:r,onfatal:l=>{i.halt(),r(l||"\u804A\u5929\u5DF2\u7ED3\u675F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u623F\u95F4\u6587\u4EF6\u3002")}});i.connection=u,u.start(),r("\u6B63\u5728\u8FDE\u63A5\u2026\u2026")}else{let u=new Ue(e.relays,{roomId:e.roomId,protocols:l=>["pt2",`a${z(i.d.access)}`,...i.resume&&u.lastIndex===l?[`r${i.resume}`]:[]],onactive:(l,h,y)=>i.run(()=>{y&&i.resetRelay(),i.ws=l,t("relay-status").textContent=`\u4E2D\u7EE7\uFF1A${e.relays[h]}${e.relays.length>1?`\uFF08\u7B2C ${h+1} \u4E2A\uFF0C\u5171 ${e.relays.length} \u4E2A\uFF09`:""}`,r(y?"\u5DF2\u6362\u5230\u53E6\u4E00\u4E2A\u4E2D\u7EE7\uFF0C\u6B63\u5728\u91CD\u65B0\u786E\u8BA4\u6210\u5458\u2026\u2026":"\u5DF2\u8FDE\u4E0A\u4E2D\u7EE7\uFF0C\u6B63\u5728\u786E\u8BA4\u5176\u4ED6\u6210\u5458\u2026\u2026")}),onmessage:l=>{let h=i.ws;i.run(()=>{if(h===i.ws)return i.incoming(l)})},ondisconnect:()=>{i.ws=null,t("relay-status").textContent="\u4E2D\u7EE7\u65AD\u5F00\u4E86\uFF0C\u6B63\u5728\u91CD\u8FDE\u2026\u2026",a("presence",0)},onnotice:r,onfatal:()=>{i.halt(),r("\u8FDB\u4E0D\u53BB\u623F\u95F4\uFF1A\u53EF\u80FD\u4EBA\u6EE1\u4E86\u3001\u623F\u95F4\u6587\u4EF6\u4E0D\u5BF9\uFF0C\u6216\u4E2D\u7EE7\u62D2\u7EDD\u4E86\u8FDE\u63A5\u3002\u8BF7\u68C0\u67E5\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{n++,s=Date.now()+Math.min(3e4,1e3*2**Math.min(n,5)),t("password").value="",r("\u89E3\u9501\u5931\u8D25\uFF1A\u53E3\u4EE4\u4E0D\u5BF9\uFF0C\u6216\u6587\u4EF6\u4E0D\u5B8C\u6574\u3002\u8BF7\u7B49\u51E0\u79D2\u518D\u8BD5\u3002")}finally{o=!1,setTimeout(()=>{t("enter").disabled=!1},Math.max(0,s-Date.now()))}}}),t("chat").addEventListener("submit",f=>{f.preventDefault(),i?.queue(t("text").value)?t("text").value="":r("\u53D1\u4E0D\u51FA\u53BB\uFF1A\u6D88\u606F\u662F\u7A7A\u7684\u3001\u592A\u957F\u4E86\uFF0C\u6216\u8005\u53D1\u5F97\u592A\u5FEB\u4E86\u3002")}),addEventListener("pagehide",()=>i?.halt())}function Ii(e){if(e.version===2)return e.relays[0].replace(/^ws/,"http");if(e.version===3){let t=e.relays.map(r=>ft(r)==="bc"?new URL(Bt(r)).origin:null);if(t.every(r=>r&&r===t[0]))return t[0].replace(/^ws/,"http")}return""}var _o=e=>JSON.stringify(e).replace(/[<>/\u2028\u2029]/g,t=>`\\u${t.charCodeAt(0).toString(16).padStart(4,"0")}`),Oi=e=>btoa(String.fromCharCode(...new TextEncoder().encode(e))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");function Nr(e){return lt(e),"#hr1="+Oi(JSON.stringify(e))}function Kr(e){let t=e.version?Ii(e):"",r=t?`${t}/hr.html${Nr(e)}`:"";return`

                        \u9875\u9762\u6CA1\u6709\u542F\u52A8\uFF1F\u5FAE\u4FE1\u3001QQ \u6216\u624B\u673A\u300C\u6587\u4EF6\u300DApp \u7684\u9884\u89C8\u6253\u4E0D\u5F00\u804A\u5929\u5BA4\uFF0C\u8BF7\u7528 Safari\u3001Chrome \u7B49\u6D4F\u89C8\u5668\u6253\u5F00\u8FD9\u4E2A\u6587\u4EF6\u3002

                        ${r?`

                        \u7528\u6D4F\u89C8\u5668\u6253\u5F00\u8FD9\u4E2A\u623F\u95F4\uFF08\u70B9\u4E86\u6CA1\u53CD\u5E94\uFF0C\u5C31\u590D\u5236\u8FD9\u4E2A\u94FE\u63A5\uFF0C\u7C98\u8D34\u5230\u6D4F\u89C8\u5668\u5730\u5740\u680F\uFF09\u3002

                        \u6CE8\u610F\uFF1A\u8FD9\u4E2A\u94FE\u63A5\u548C\u6587\u4EF6\u4E00\u6837\u662F\u623F\u95F4\u94A5\u5319\uFF0C\u522B\u968F\u4FBF\u8F6C\u53D1\u3002

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

                        加密聊天房间

                        正在启动……如果一直停在这里,请看下面的说明。

                        ${previewHelp(config)}
                        ${RETARGET}

                          HTML Room 开源项目:${repoLink}

                          \n`; + const html=`加密聊天房间

                          加密聊天房间

                          正在启动……如果一直停在这里,请看下面的说明。

                          ${previewHelp(config)}
                          ${RETARGET}

                            HTML Room 开源项目:${repoLink}

                            \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) { +export function withRelays(config, input, custom = config.custom === true) { validate(config); + const { custom: _, ...rest } = config; config = custom ? { ...rest, custom: true } : rest; // Moving between classic (v2) and decentralized (v3) relays only changes the // version tag: the key material and roomId stay the same. const relays = relayList(input); @@ -32,9 +33,9 @@ export function withRelays(config, 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) { +export async function retargetHtml(config, input, script, style, custom) { if (typeof script !== 'string' || typeof style !== 'string' || /<\/script/i.test(script) || /<\/style/i.test(style)) throw Error('脚本或样式不可用'); - const next = withRelays(config, input); + const next = withRelays(config, input, custom); return { config: next, ...roomHtml(next, script, style, await digest(script), await digest(style)) }; } diff --git a/html-room/transport.js b/html-room/transport.js index d2627f1..f3b138f 100644 --- a/html-room/transport.js +++ b/html-room/transport.js @@ -1,4 +1,4 @@ -import { validate, relayEntry, relayKind, relayUrl, cspSource } from './crypto.js'; +import { validate, relayEntry, relayKind, relayUrl, connectSources } from './crypto.js'; // The page that can open this room from a link: a classic relay's /hr.html, or // for a decentralized room whose relays are all bc paths on one server, that // server's page. Other relay mixes have no web entry; use the file. @@ -33,5 +33,5 @@ export async function fillTemplate(template,config,allowed,scriptHash,styleHash, const csp=`default-src 'none'; script-src 'sha256-${scriptHash}'; style-src 'sha256-${styleHash}'; connect-src 'none'; base-uri 'none'; form-action 'none'`; const marker=``; if(!template.includes(marker))throw Error('模板 CSP 无效'); - return template.replace(marker,``).replace(previewHelp({}),previewHelp(config)).replace('',``); + return template.replace(marker,``).replace(previewHelp({}),previewHelp(config)).replace('',``); } diff --git a/html-room/web.js b/html-room/web.js index 724a48d..68164b9 100644 --- a/html-room/web.js +++ b/html-room/web.js @@ -25,7 +25,7 @@ function openFragment(fragment){ try{ const config=configFromHash(fragment); if(!linkable(config.relays))throw Error('网页入口仅允许预设中继,请用房间文件连接其他中继。'); - boot(config); + boot(config,{allowCustom:false}); }catch{el('status').textContent='房间链接打不开:链接不完整,或者它用的中继本站不支持。请让对方重发链接,或直接用房间文件。';el('enter').disabled=true;} } addEventListener('hashchange',()=>{if(location.hash)openFragment(location.hash);}); @@ -40,7 +40,7 @@ else{ const relays=relayList(el('mk-relay').value),password=el('mk-pass').value; if(password && new TextEncoder().encode(password).length>1024)throw Error('口令过长'); // Submitted canonical sources form the explicit file generation allowlist. - const config=await makeConfig(relays,password||undefined); + const config=await makeConfig(relays,password||undefined,{custom:el('mk-custom').checked}); const html=await filledTemplate(config); if(downloadUrl)URL.revokeObjectURL(downloadUrl); downloadUrl=URL.createObjectURL(new Blob([html],{type:'text/html'})); @@ -60,7 +60,7 @@ else{ const file=el('cv-file').files?.[0];if(!file)throw Error('请先选择房间文件'); if(file.size>400000)throw Error('这个文件太大了,不是房间文件'); let old;try{old=configFromRoomHtml(await file.text());}catch{throw Error('读不出房间信息。如果是很早的旧版文件,请重新创建房间');} - const config=withRelays(old,el('cv-relay').value); + const config=withRelays(old,el('cv-relay').value,el('cv-custom').checked); const html=await filledTemplate(config); if(convertUrl)URL.revokeObjectURL(convertUrl); convertUrl=URL.createObjectURL(new Blob([html],{type:'text/html'})); diff --git a/package.json b/package.json index 208d978..edb2130 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build:pages": "node scripts/build-html-room.mjs pages", "build:check": "node scripts/build-html-room.mjs template --check", "test": "node --test", - "test:browser": "node browser-tests/htmlroom.mjs && node browser-tests/html-room-preview.mjs && node browser-tests/multi-relay.mjs && node browser-tests/retarget.mjs && node browser-tests/mesh.mjs", + "test:browser": "node browser-tests/htmlroom.mjs && node browser-tests/html-room-preview.mjs && node browser-tests/multi-relay.mjs && node browser-tests/retarget.mjs && node browser-tests/mesh.mjs && node browser-tests/custom-relay.mjs", "test:retarget": "node browser-tests/retarget.mjs", "test:failover": "node browser-tests/multi-relay.mjs", "test:pages": "npm run build:pages && node browser-tests/pages.mjs", diff --git a/scripts/build-html-room.mjs b/scripts/build-html-room.mjs index aa036b5..c2f0d54 100644 --- a/scripts/build-html-room.mjs +++ b/scripts/build-html-room.mjs @@ -26,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=`

                            创建加密聊天室

                            点一下就能建好一个房间,把文件或链接发给朋友,打开就能聊。全程在你的浏览器里完成,服务器看不到聊天内容。

                            中继服务器(一般不用改)

                            中继负责帮大家传消息,看不到内容。想换成自己的或多个服务器:经典 wss://… 最多 3 个,坏了会自动换下一个;去中心化 bc+wss://…、nostr+wss://…、mqtt+wss://…/mqtt 最多 5 个,同时使用。两类不能混用。

                            已经有房间文件?换一个中继服务器

                            选择房间文件,填上新的中继地址,会生成同一个房间的新文件,口令不变。文件只在你的浏览器里读取,不会上传。

                            HTML Room 开源项目:${repoLink}

                            `; + const maker=`

                            创建加密聊天室

                            点一下就能建好一个房间,把文件或链接发给朋友,打开就能聊。全程在你的浏览器里完成,服务器看不到聊天内容。

                            中继服务器(一般不用改)

                            中继负责帮大家传消息,看不到内容。想换成自己的或多个服务器:经典 wss://… 最多 3 个,坏了会自动换下一个;去中心化 bc+wss://…、nostr+wss://…、mqtt+wss://…/mqtt 最多 5 个,同时使用。两类不能混用。

                            打开时,拿到文件的人进房前可以改填任意 wss:// 中继。关掉更严格:文件只能连上面这些中继。

                            已经有房间文件?换一个中继服务器

                            选择房间文件,填上新的中继地址,会生成同一个房间的新文件,口令不变。文件只在你的浏览器里读取,不会上传。

                            HTML Room 开源项目:${repoLink}

                            `; 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}; @@ -51,19 +51,22 @@ if(process.argv[1]===fileURLToPath(import.meta.url)){ 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); + let custom;for(const f of ['--strict','--custom'])if(flags.includes(f)){custom=f==='--custom';flags.splice(flags.indexOf(f),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 config=withRelays(configFromRoomHtml(await readFile(getAll('--in')[0],'utf8')),relays,custom); 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; + let output,relay,allowed,strict=true; if(args[0]==='room'){ + strict=false; const flags=args.slice(1); - if(flags.length%2 || flags.some((value,i)=>i%2===0&&!['--out','--relay','--allow-relay'].includes(value)) || flags.filter(x=>x==='--out').length!==1)throw Error('room 仅支持一个 --out 和可重复的 --relay、--allow-relay;口令请用 HR_PASSWORD_STDIN'); + strict=flags.includes('--strict');if(strict)flags.splice(flags.indexOf('--strict'),1); + if(flags.length%2 || flags.some((value,i)=>i%2===0&&!['--out','--relay','--allow-relay'].includes(value)) || flags.filter(x=>x==='--out').length!==1)throw Error('room 仅支持一个 --out、可重复的 --relay、--allow-relay 和 --strict(不允许在文件里自定义中继);口令请用 HR_PASSWORD_STDIN'); const getAll=name=>flags.filter((value,i)=>i%2===1&&flags[i-1]===name); [output]=getAll('--out');relay=getAll('--relay');allowed=getAll('--allow-relay'); if(!output||!relay.length||!allowed.length)throw Error('room 需要 --out、--relay 与明确 --allow-relay'); @@ -71,7 +74,8 @@ if(process.argv[1]===fileURLToPath(import.meta.url)){ if(!output)throw Error('用法:node scripts/build-html-room.mjs out/index.html wss://relay.example wss://relay.example (从标准输入可选读取口令,HR_PASSWORD_STDIN=1)'); let pass; if(process.env.HR_PASSWORD_STDIN==='1'){let s='';for await(const chunk of process.stdin){s+=chunk;if(Buffer.byteLength(s)>4096)throw Error('输入过长');}pass=s.replace(/\r?\n$/,'');if(!pass)throw Error('口令不能为空');} - const config=relay?await makeConfig(relay,pass):{}; + // room: custom relays allowed unless --strict; the legacy positional form stays strict. + const config=relay?await makeConfig(relay,pass,{custom:!strict}):{}; const r=await render(config,allowed); await mkdir(new URL('../out/',import.meta.url),{recursive:true}); await writeFile(output,r.html,{mode:0o600,flag:'wx'}); diff --git a/test/mesh.test.mjs b/test/mesh.test.mjs index 7c8138b..496c096 100644 --- a/test/mesh.test.mjs +++ b/test/mesh.test.mjs @@ -131,3 +131,28 @@ test('frames reach every member through any one surviving relay, without duplica await wait(100); assert.equal(a.seen.frames.filter(([, type]) => type === 'key').length, 1, 'two relays, one delivery'); }); + +test('custom-relay files: wider CSP only when asked, typed relays change routing not keys', async () => { + const { sessionRelays } = await import('../html-room/crypto.js'); + const { checkHtml } = await import('../scripts/check-html-room.mjs'); + const strict = await makeConfig('wss://a.example', 'pw'), open = await makeConfig('wss://a.example', 'pw', { custom: true }); + assert.equal(strict.custom, undefined); assert.equal(open.custom, true); + assert.equal(Object.keys(open).at(-1), 'custom'); + assert.throws(() => validate({ ...open, custom: false })); + const strictHtml = (await render(strict, strict.relays)).html, openHtml = await render(open, open.relays); + assert.match(strictHtml, /connect-src wss:\/\/a\.example;/); + assert.match(openHtml.html, /connect-src wss: ws:\/\/localhost:\* ws:\/\/127\.0\.0\.1:\* wss:\/\/a\.example;/); + await checkHtml(openHtml.html, openHtml.scriptHash); + assert.equal(sessionRelays(open, ''), open); + assert.equal(sessionRelays(open, ' '), open); + const typed = sessionRelays(open, 'wss://b.example'); + assert.deepEqual(typed.relays, ['wss://b.example']); assert.equal(typed.roomId, open.roomId); assert.equal(typed.ciphertext, open.ciphertext); + const mesh = sessionRelays(open, 'nostr+wss://n.example bc+wss://b.example'); + assert.equal(mesh.version, 3); + assert.equal((await unlock(mesh, 'pw')).roomId, open.roomId); + assert.throws(() => sessionRelays(strict, 'wss://b.example'), /不允许/); + assert.throws(() => sessionRelays(open, 'http://b.example')); + assert.equal(withRelays(open, 'wss://c.example').custom, true, 'converting keeps the setting'); + assert.equal(withRelays(open, 'wss://c.example', false).custom, undefined); + assert.equal(withRelays(strict, 'wss://c.example', true).custom, true); +}); From bbdd81f3f3a4ac652496134988b258a934c4313c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 17:00:07 -0400 Subject: [PATCH 05/13] ci: follow the new page title, add a Go relay job, Windows-safe tests Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PCUwNSXQdqfDSugmFo4qmM --- .github/workflows/test.yml | 15 ++++++++++++++- test/fixtures/mesh-relays.mjs | 2 +- test/quickstart.test.mjs | 6 ++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5a6ac74..55dd57a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,6 +30,19 @@ jobs: - run: npx playwright install --with-deps chromium - run: npm run test:browser - run: npm run test:pages + go: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + - name: Vet, format check and cross-compile the broadcast relay + working-directory: relay/go + run: | + test -z "$(gofmt -l .)" + go vet ./... + for t in linux/amd64 linux/mipsle windows/amd64 darwin/arm64; do GOOS=${t%/*} GOARCH=${t#*/} CGO_ENABLED=0 go build -o /dev/null .; done worker: runs-on: ubuntu-latest env: @@ -52,7 +65,7 @@ jobs: - run: | docker run -d --name htmlroom -p 127.0.0.1:18080:8080 htmlroom:test for i in $(seq 1 30); do curl -fs http://127.0.0.1:18080/healthz && break; sleep 1; done - curl -fs http://127.0.0.1:18080/hr.html | grep -q '创建 HTML 房间' + curl -fs http://127.0.0.1:18080/hr.html | grep -q '创建加密聊天室' installer: runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/test/fixtures/mesh-relays.mjs b/test/fixtures/mesh-relays.mjs index 2519495..bcffb6b 100644 --- a/test/fixtures/mesh-relays.mjs +++ b/test/fixtures/mesh-relays.mjs @@ -28,7 +28,7 @@ let goBinary; export function goAvailable() { try { execFileSync('go', ['version'], { stdio: 'ignore' }); return true; } catch { return false; } } export async function goRelay(args = []) { if (!goBinary) { - goBinary = mkdtempSync(tmpdir() + '/hr-go-') + '/htmlroom-relay'; + goBinary = mkdtempSync(tmpdir() + '/hr-go-') + '/htmlroom-relay' + (process.platform === 'win32' ? '.exe' : ''); execFileSync('go', ['build', '-o', goBinary, '.'], { cwd: root + 'relay/go', stdio: 'ignore', env: { ...process.env, CGO_ENABLED: '0' } }); } const port = 20000 + Math.floor(Math.random() * 20000); diff --git a/test/quickstart.test.mjs b/test/quickstart.test.mjs index b7c968e..840388d 100644 --- a/test/quickstart.test.mjs +++ b/test/quickstart.test.mjs @@ -8,6 +8,8 @@ import { mkdtempSync } from 'node:fs'; import { tmpdir } from 'node:os'; const script = new URL('../scripts/relay-quickstart.sh', import.meta.url).pathname; +// A POSIX sh script (Linux, macOS, BSD, WSL, Termux, OpenWrt); Windows users get the .exe. +const skip = process.platform === 'win32' && 'POSIX shell script'; // Async: the fake release server below runs in this same process. const run = (args, env = {}) => new Promise(resolve => { const child = spawn('sh', [script, ...args], { env: { ...process.env, ...env } }); @@ -15,7 +17,7 @@ const run = (args, env = {}) => new Promise(resolve => { child.on('close', code => resolve({ code, out })); }); -test('quickstart parses, detects the platform and refuses bad flags', async () => { +test('quickstart parses, detects the platform and refuses bad flags', { skip }, async () => { execFileSync('sh', ['-n', script]); const check = await run(['--check', '--tunnel']); assert.equal(check.code, 0); @@ -25,7 +27,7 @@ test('quickstart parses, detects the platform and refuses bad flags', async () = assert.equal((await run(['--bogus'])).code, 2); }); -test('quickstart installs only a binary matching the published checksum', async (t) => { +test('quickstart installs only a binary matching the published checksum', { skip }, async (t) => { const files = new Map(); const server = createServer((req, res) => { const f = files.get(req.url.slice(1)); if (!f) { res.writeHead(404).end(); return; } res.end(f); }); await new Promise(r => server.listen(0, '127.0.0.1', r)); t.after(() => server.close()); From 4bd450cbbb5330623c642f9c5bc5cd39caf4a28b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 17:31:07 -0400 Subject: [PATCH 06/13] feat: every file creates the next room offline; the site is one file - Room files fold a creator: a new room with a new key and optional new password, built offline from the page's own script and style bytes (htmlFor), byte-identical to a release build. - A blank file ({} or {relays}) is the creator: the offline generator and the hosted site. It also opens #hr1= links (CSP wss: + loopback ws) and converts old files; hosted, it offers itself as an offline download. - The separate web entry (web.js, hr.js, hr.css) is gone: hr.html and the Pages index.html are the creator file; _headers is generated with the same script and style hashes. Tests: offline chain blank -> A -> B (new password) -> C with no network requests and release-identical output, chat in B, site self-download; existing suites updated for links on the site reaching any relay. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PCUwNSXQdqfDSugmFo4qmM --- README.md | 6 ++ browser-tests/custom-relay.mjs | 6 +- browser-tests/generations.mjs | 89 ++++++++++++++++ browser-tests/multi-relay.mjs | 14 ++- browser-tests/pages.mjs | 10 +- dist/_headers | 2 +- dist/hr-template.html | 4 +- dist/hr.css | 1 - dist/hr.html | 3 +- dist/hr.js | 1 - docs/DEVELOPER.md | 18 +++- docs/html-room/protocol.md | 4 +- docs/html-room/release-integrity.json | 12 +-- docs/html-room/security-model.md | 4 +- html-room/crypto.js | 10 +- html-room/file.js | 145 +++++++++++++++++++++----- html-room/template.js | 18 +++- html-room/transport.js | 8 +- html-room/web.js | 74 ------------- package.json | 2 +- scripts/build-html-room.mjs | 28 +++-- scripts/check-html-room.mjs | 4 +- server/server.mjs | 2 +- src/worker.js | 2 +- 24 files changed, 319 insertions(+), 148 deletions(-) create mode 100644 browser-tests/generations.mjs delete mode 100644 dist/hr.css delete mode 100644 dist/hr.js delete mode 100644 html-room/web.js diff --git a/README.md b/README.md index 5307bb1..eaa0acc 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,12 @@ > 💡 每点一次「创建房间」都会生成一个**全新的**房间,就算密码一样,也进不了以前的房间。 +## 不联网也能创建:一个文件就是整个网站 + +- **每个房间文件都能再生一个新房间。** 打开任意 `index.html`,展开「创建一个新房间」,就能生成钥匙不同、可设新口令的新房间文件。全程在文件里完成,**不需要联网,也不依赖任何网站**;生成的文件和官方构建逐字节一致。 +- **离线创建器。** 网站本身就是一个不带房间的 `index.html`。在网站底部点「下载离线版」保存下来,以后断网也能用它创建房间。 +- 只有聊天时需要连上中继。 + ## 能做什么,不能做什么 | ✅ 可以 | ❌ 不可以 | diff --git a/browser-tests/custom-relay.mjs b/browser-tests/custom-relay.mjs index 66d13ba..57565bf 100644 --- a/browser-tests/custom-relay.mjs +++ b/browser-tests/custom-relay.mjs @@ -1,5 +1,5 @@ // A room file made for a relay that is gone: members type their own relay into -// the file and chat. Strict files and web links show no relay box. +// the file and chat. Strict files show no relay box; links opened on the site do. import { chromium } from 'playwright'; import { mkdtemp, writeFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; @@ -61,7 +61,7 @@ try { const webConfig = await makeConfig(own.url, undefined, { custom: true }); const w = await browser.newPage(); await w.goto(`http://127.0.0.1:${own.port}/hr.html${fragmentFor(webConfig)}`); await w.locator('#status').filter({ hasText: '进入房间' }).waitFor(); - assert.equal(await w.locator('#custom-relay').isVisible(), false); + assert.equal(await w.locator('#custom-relay').isVisible(), true, 'the hosted creator can reach typed relays too'); // The creation page makes custom-relay files by default, strict when unticked. const maker = await browser.newPage(); await maker.goto(`http://127.0.0.1:${own.port}/`); @@ -72,7 +72,7 @@ try { await maker.locator('#mk-advanced summary').click(); await maker.locator('#mk-custom').uncheck(); assert.doesNotMatch(await made(), /connect-src wss: /); assert.deepEqual(errors, []); - console.log('PASS: custom relays typed into the file (classic and decentralized), bad address handling, strict files and web links without the box, maker default'); + console.log('PASS: custom relays typed into the file (classic and decentralized), bad address handling, strict files without the box, links on the site with it, maker default'); } finally { await browser?.close(); await Promise.all([own.stop(), nostr.stop()]); await rm(dir, { recursive: true, force: true }); } diff --git a/browser-tests/generations.mjs b/browser-tests/generations.mjs new file mode 100644 index 0000000..9cbf974 --- /dev/null +++ b/browser-tests/generations.mjs @@ -0,0 +1,89 @@ +// Every file creates the next: a blank creator makes room A offline, A makes +// room B (new key, new password), B makes C; each output is byte-identical to +// a release build. Then members chat in B. The hosted site offers itself as +// an offline creator. +import { chromium } from 'playwright'; +import { mkdtemp, writeFile, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { pathToFileURL } from 'node:url'; +import assert from 'node:assert/strict'; +import { unlock } from '../html-room/crypto.js'; +import { configFromRoomHtml } from '../html-room/template.js'; +import { render } from '../scripts/build-html-room.mjs'; +import { checkHtml } from '../scripts/check-html-room.mjs'; +import { nodeRelay } from '../test/fixtures/mesh-relays.mjs'; + +const dir = await mkdtemp(tmpdir() + '/hr-gen-'); +const relay = await nodeRelay(); +let browser; +try { + const blank = await render({}, []); + const creatorPath = dir + '/creator.html'; await writeFile(creatorPath, blank.html); + browser = await chromium.launch({ headless: true }); + const offline = await browser.newContext({ acceptDownloads: true }); await offline.setOffline(true); + const errors = [], requests = []; + const watch = p => { p.on('pageerror', e => errors.push(e.message)); p.on('request', r => { if (!/^(file|blob|data):/.test(r.url())) requests.push(r.url()); }); return p; }; + const create = async (page, { relays, password, expand }) => { + if (expand) await page.locator('#new-room > summary').click(); + if (relays) { if (!(await page.locator('#mk-relay').isVisible())) await page.locator('#mk-advanced > summary').click(); await page.locator('#mk-relay').fill(relays); } + if (password) await page.locator('#mk-pass').fill(password); + await page.locator('#mk-make').click(); + await page.locator('#mk-out').waitFor({ state: 'visible' }); + assert.equal(await page.locator('#mk-link-row').isVisible(), false, 'no link from a local file'); + const dl = page.waitForEvent('download'); await page.locator('#mk-download').click(); + const path = dir + `/${Math.random().toString(36).slice(2)}.html`; await (await dl).saveAs(path); + const html = await readFile(path, 'utf8'); + await checkHtml(html, blank.scriptHash); // identical to a release build of the same config + return { path, html, config: configFromRoomHtml(html) }; + }; + + // Generation 0: the blank creator, offline. + const g0 = watch(await offline.newPage()); await g0.goto(pathToFileURL(creatorPath).href); + assert.equal(await g0.locator('#maker').isVisible(), true); + assert.equal(await g0.locator('#unlock').isVisible(), false); + assert.equal(await g0.locator('#mk-relay').inputValue(), ''); + const a = await create(g0, { relays: relay.url, password: 'first pass' }); + assert.equal(a.config.custom, true); + + // Generation 1: room A creates room B with a different password. + const g1 = watch(await offline.newPage()); await g1.goto(pathToFileURL(a.path).href); + assert.equal(await g1.locator('#unlock').isVisible(), true); + assert.equal(await g1.locator('#maker').isVisible(), false, 'folded in a room file'); + await g1.locator('#new-room > summary').click(); + await g1.locator('#mk-advanced > summary').click(); + assert.equal(await g1.locator('#mk-relay').inputValue(), relay.url, 'starts from this room\'s relays'); + const b = await create(g1, { password: 'second pass' }); + assert.notEqual(b.config.roomId, a.config.roomId, 'a new room, not a copy'); + assert.equal((await unlock(b.config, 'second pass')).roomId, b.config.roomId); + await assert.rejects(unlock(b.config, 'first pass')); + + // Generation 2: B creates C, no password, decentralized. + const g2 = watch(await offline.newPage()); await g2.goto(pathToFileURL(b.path).href); + const c = await create(g2, { expand: true, relays: `bc+${relay.url}/bc/` }); + assert.equal(c.config.version, 3); assert.equal(c.config.encrypted, false); + assert.deepEqual(requests, [], 'creating needs no network at all'); + + // Members chat in B (online). + const online = await browser.newContext(); + const members = [watch(await online.newPage()), watch(await online.newPage())]; + for (const m of members) { await m.goto(pathToFileURL(b.path).href); await m.locator('#password').fill('second pass'); await m.locator('#enter').click(); } + for (const m of members) await m.locator('#status').filter({ hasText: '已加密连接' }).waitFor({ timeout: 20000 }); + await members[0].locator('#text').fill('第二代房间'); await members[0].locator('#send').click(); + await members[1].locator('#messages li').filter({ hasText: '第二代房间' }).waitFor({ timeout: 15000 }); + + // The site is the creator; it hands out itself as an offline creator. + const site = watch(await online.newPage()); await site.goto(`http://127.0.0.1:${relay.port}/`); + assert.equal(await site.locator('#maker').isVisible(), true); + await site.locator('#mk-self-row').waitFor({ state: 'visible' }); + const dl = site.waitForEvent('download'); await site.locator('#mk-self').click(); + const selfPath = dir + '/offline-creator.html'; await (await dl).saveAs(selfPath); + const selfHtml = await readFile(selfPath, 'utf8'); + assert.equal(selfHtml, (await render({ relays: [relay.url] }, [])).html, 'the offline creator remembers this site\'s relay'); + await checkHtml(selfHtml, blank.scriptHash); + const again = watch(await offline.newPage()); await again.goto(pathToFileURL(selfPath).href); + assert.equal(await again.locator('#mk-relay').inputValue(), relay.url); + assert.deepEqual(errors, []); + console.log('PASS: blank creator -> room A -> room B (new key, new password) -> room C, all offline and byte-identical to release builds; chat in B; site saves itself as an offline creator'); +} finally { + await browser?.close(); await relay.stop(); await rm(dir, { recursive: true, force: true }); +} diff --git a/browser-tests/multi-relay.mjs b/browser-tests/multi-relay.mjs index ca6e09a..bb77a05 100644 --- a/browser-tests/multi-relay.mjs +++ b/browser-tests/multi-relay.mjs @@ -70,14 +70,20 @@ try { await maker.locator('#mk-advanced summary').click(); await maker.locator('#mk-relay').fill(relays.join(', ')); await maker.locator('#mk-make').click(); await maker.locator('#mk-out').waitFor({ state: 'visible' }); - assert.equal(await maker.locator('#mk-link-row').isHidden(), true, 'hosted entry cannot expand its trusted relay list'); + // The hosted site is the creator file and opens any room link (CSP wss:). + assert.equal(await maker.locator('#mk-link-row').isVisible(), true); const download = maker.waitForEvent('download'); await maker.locator('#mk-download').click(); const html = await readFile(await (await download).path(), 'utf8'); assert.deepEqual(JSON.parse(html.match(/type="application\/json">(.*?)<\/script>/s)[1]).relays, relays); - let opened = 0; maker.on('websocket', () => opened++); + const opened = []; maker.on('websocket', s => opened.push(s.url())); await maker.goto(`http://127.0.0.1:${port}/hr.html${fragmentFor(config)}`); - await maker.getByText(/房间链接打不开/).waitFor(); assert.equal(opened, 0); - console.log('PASS: two real relays, failover, bidirectional backup chat, primary recovery, maker and hosted allowlist'); + await maker.locator('#enter').click(); + await maker.locator('#relay-status').filter({ hasText: relays[0] }).waitFor({ timeout: 15000 }); + assert.ok(opened.length && opened.every(u => relays.some(r => u.startsWith(r))), 'a link connects only to its own relays'); + let bad = 0; const broken = await browser.newPage(); broken.on('websocket', () => bad++); + await broken.goto(`http://127.0.0.1:${port}/hr.html#hr1=broken`); + await broken.getByText(/房间链接打不开/).waitFor(); assert.equal(bad, 0); + console.log('PASS: two real relays, failover, bidirectional backup chat, primary recovery, maker and hosted links to any relays'); } finally { await browser?.close(); await stop(0); await stop(1); await rm(dir, { recursive: true, force: true }); } diff --git a/browser-tests/pages.mjs b/browser-tests/pages.mjs index b614111..e05be34 100644 --- a/browser-tests/pages.mjs +++ b/browser-tests/pages.mjs @@ -4,7 +4,8 @@ import { chromium } from 'playwright'; import assert from 'node:assert/strict'; const assets = new Map(); -for (const [name,type] of [['index.html','text/html'],['hr.js','text/javascript'],['hr.css','text/css'],['hr-template.html','text/html']]) { +// The whole site is one file: index.html is the creator (hr.html is the same). +for (const [name,type] of [['index.html','text/html'],['hr.html','text/html'],['hr-template.html','text/html']]) { assets.set('/htmlroom/'+(name==='index.html'?'':name),{body:await readFile(new URL('../pages/'+name,import.meta.url)),type}); } const server=createServer((req,res)=>{ @@ -20,7 +21,9 @@ try { page.on('pageerror',e=>errors.push(e.message)); page.on('console',m=>{if(m.type()==='error')errors.push(m.text());}); const base=`http://127.0.0.1:${server.address().port}/htmlroom/`; + assert.deepEqual(assets.get('/htmlroom/').body, assets.get('/htmlroom/hr.html').body); await page.goto(base); + await page.locator('#mk-advanced > summary').click(); assert.equal(await page.locator('#mk-relay').inputValue(),'wss://chat.nya.je'); await page.locator('#mk-make').click();await page.locator('#mk-out').waitFor({state:'visible'}); const link=await page.locator('#mk-link').inputValue();assert.ok(link.startsWith(base+'#hr1=')); @@ -30,11 +33,12 @@ try { const config=JSON.parse(html.match(/type="application\/json">(.*?)<\/script>/s)[1]); assert.deepEqual(config.relays,['wss://chat.nya.je']); assert.ok(/connect-src wss: [^;]*wss:\/\/chat\.nya\.je;/.test(html), 'custom-relay file still lists its relay'); - await page.goto(link);await page.waitForFunction(()=>location.hash===''); + // A fresh tab, as a friend opening the link would. + await page.goto('about:blank');await page.goto(link);await page.waitForFunction(()=>location.hash===''); await page.locator('#enter').waitFor({state:'visible'}); assert.equal(await page.locator('#enter').isEnabled(),true); assert.deepEqual(errors,[]); - console.log('PASS: Pages project subpath, external relay, local download index.html, and Fragment room entry'); + console.log('PASS: Pages project subpath, one-file site with preset relay, local download index.html, and Fragment room entry'); } finally { await browser?.close();await new Promise(resolve=>server.close(resolve)); } diff --git a/dist/_headers b/dist/_headers index 8457fac..8dbadde 100644 --- a/dist/_headers +++ b/dist/_headers @@ -1,5 +1,5 @@ /* - Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: blob:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none' + Content-Security-Policy: default-src 'none'; script-src 'sha256-PkklTQ3hbwbMoeHTqP56bCU2n7j/1XWIx9+g322v2ko='; style-src 'sha256-+cH4mX6DQpXWnU8EWkUQE30KyFp1e6rCFFdGd+i+pzA='; connect-src 'self' wss: ws://localhost:* ws://127.0.0.1:*; img-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none' Cache-Control: no-store NEL: {"max_age":0} Referrer-Policy: no-referrer diff --git a/dist/hr-template.html b/dist/hr-template.html index 3903fce..c788b9c 100644 --- a/dist/hr-template.html +++ b/dist/hr-template.html @@ -1,2 +1,2 @@ -加密聊天房间

                            加密聊天房间

                            正在启动……如果一直停在这里,请看下面的说明。

                            页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                              +加密聊天房间

                              加密聊天房间

                              正在启动……如果一直停在这里,请看下面的说明。

                              页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                                diff --git a/dist/hr.css b/dist/hr.css deleted file mode 100644 index 47a77f1..0000000 --- a/dist/hr.css +++ /dev/null @@ -1 +0,0 @@ -:root{color-scheme:dark;font-family:system-ui,sans-serif;background:#121820;color:#edf2f7}body{max-width:760px;margin:auto;padding:20px}h1{font-size:1.5rem}p{line-height:1.6}aside{border:1px solid #dcac58;padding:12px;border-radius:10px;color:#ffe0a3}input,textarea,button{font:inherit;padding:12px;border:1px solid #65778c;border-radius:8px;background:#202d3b;color:inherit;box-sizing:border-box}input,textarea{width:100%;margin:8px 0}button{cursor:pointer}button:disabled{opacity:.5}ol{padding:0;list-style:none}li{padding:12px;margin:8px 0;background:#202d3b;border-radius:8px;white-space:pre-wrap;overflow-wrap:anywhere}small{display:block;color:#b9c8d8;margin-top:6px}[hidden]{display:none}#status{color:#a7d9ff}textarea{min-height:90px}.hint{color:#b9c8d8;font-size:.92rem}code{background:#202d3b;padding:1px 4px;border-radius:4px;overflow-wrap:anywhere}footer{margin-top:32px;border-top:1px solid #34465a}a{color:#a7d9ff}details{margin:14px 0}summary{cursor:pointer}#mk-make,#enter,#send{background:#2f6fb0;border-color:#4a8bd0;font-weight:600;padding:12px 22px}button{margin:4px 0}.check{display:flex;gap:8px;align-items:center;margin:10px 0}.check input{width:auto;margin:0} diff --git a/dist/hr.html b/dist/hr.html index 4aafb03..c788b9c 100644 --- a/dist/hr.html +++ b/dist/hr.html @@ -1 +1,2 @@ -HTML Room · 加密聊天室

                                创建加密聊天室

                                点一下就能建好一个房间,把文件或链接发给朋友,打开就能聊。全程在你的浏览器里完成,服务器看不到聊天内容。

                                中继服务器(一般不用改)

                                中继负责帮大家传消息,看不到内容。想换成自己的或多个服务器:经典 wss://… 最多 3 个,坏了会自动换下一个;去中心化 bc+wss://…、nostr+wss://…、mqtt+wss://…/mqtt 最多 5 个,同时使用。两类不能混用。

                                打开时,拿到文件的人进房前可以改填任意 wss:// 中继。关掉更严格:文件只能连上面这些中继。

                                已经有房间文件?换一个中继服务器

                                选择房间文件,填上新的中继地址,会生成同一个房间的新文件,口令不变。文件只在你的浏览器里读取,不会上传。

                                HTML Room 开源项目:github.com/Freecode100Year/htmlroom

                                +加密聊天房间

                                加密聊天房间

                                正在启动……如果一直停在这里,请看下面的说明。

                                页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                                  diff --git a/dist/hr.js b/dist/hr.js deleted file mode 100644 index a046d4b..0000000 --- a/dist/hr.js +++ /dev/null @@ -1 +0,0 @@ -(()=>{var Mo=Object.defineProperty;var Do=(e,t,r)=>t in e?Mo(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r;var st=(e,t,r)=>Do(e,typeof t!="symbol"?t+"":t,r);var q=[];var $i=new TextEncoder;var jo=[256,512,1024,2048,4096];function me(e){let t=jo.find(n=>n>e.length)??e.length+1,r=new Uint8Array(t);return r.set(e),r[e.length]=128,r}function we(e){let t=e.length-1;for(;t>=0&&e[t]===0;)t--;return t>=0&&e[t]===128?e.subarray(0,t):e}var St=new TextEncoder,z=e=>Array.from(new Uint8Array(e),t=>t.toString(16).padStart(2,"0")).join("");function yt(e,t){if(typeof e!="string"||e.length!==t*2||!/^[0-9a-f]+$/.test(e))throw Error("\u914D\u7F6E\u5B57\u8282\u683C\u5F0F\u9519\u8BEF");return Uint8Array.from(e.match(/../g),r=>parseInt(r,16))}var tt=e=>crypto.getRandomValues(new Uint8Array(e));async function jr(e){let t=typeof e=="string"?yt(e,32):e;if(!(t instanceof Uint8Array)||t.length!==32)throw Error("\u623F\u95F4\u5BC6\u94A5\u957F\u5EA6\u9519\u8BEF");let r=await crypto.subtle.importKey("raw",t,"HKDF",!1,["deriveBits"]),n=async u=>new Uint8Array(await crypto.subtle.deriveBits({name:"HKDF",hash:"SHA-256",salt:new Uint8Array(32),info:St.encode(`private-talk/html-room/v1/${u}`)},r,256)),o=await n("access"),s=await n("key"),i=await n("bind"),c=async u=>new Uint8Array(await crypto.subtle.deriveBits({name:"HKDF",hash:"SHA-256",salt:new Uint8Array(32),info:St.encode(`private-talk/html-room/v3/${u}`)},r,256)),a=await c("mesh"),f=z(await c("topic")).slice(0,32),d=await crypto.subtle.importKey("raw",a,"AES-GCM",!1,["encrypt","decrypt"]);return a.fill(0),{access:o,keyBytes:s,bind:i,topic:f,meshKey:d,roomId:z(await crypto.subtle.digest("SHA-256",o)),key:await crypto.subtle.importKey("raw",s,"AES-GCM",!1,["encrypt","decrypt"])}}var Po=/^(?:[a-z0-9.-]+|\[[0-9a-f:]+\])$/,Vo=["localhost","127.0.0.1","[::1]"];function Pr(e){if(!Po.test(e.hostname))throw Error("\u4E2D\u7EE7\u5730\u5740\u91CC\u6709\u4E0D\u652F\u6301\u7684\u5B57\u7B26");if(e.username||e.password||e.search||e.hash||!["ws:","wss:"].includes(e.protocol))throw Error("\u4E2D\u7EE7\u5730\u5740\u8981\u4EE5 wss:// \u5F00\u5934\uFF0C\u4E0D\u80FD\u5E26\u8D26\u53F7\u3001? \u6216 #");if(e.protocol==="ws:"&&!Vo.includes(e.hostname))throw Error("\u4E2D\u7EE7\u5730\u5740\u8981\u7528 wss://\uFF08\u52A0\u5BC6\u8FDE\u63A5\uFF09")}function Go(e){let t=new URL(e);if(Pr(t),t.pathname!=="/")throw Error("\u7ECF\u5178\u4E2D\u7EE7\u53EA\u586B\u57DF\u540D\uFF0C\u4E0D\u8981\u5E26\u8DEF\u5F84\uFF0C\u4F8B\u5982 wss://relay.example.com");return t.origin}var Vr=/^(bc|nostr|mqtt)\+/i,zo=/^(?:\/[A-Za-z0-9._~-]+)*\/?$/,lt=e=>Vr.exec(e)?.[1].toLowerCase()??"hr",Bt=e=>e.replace(Vr,""),Zo=Bt;function Qe(e){if(typeof e!="string")throw Error("\u4E2D\u7EE7\u5730\u5740\u65E0\u6548");let t=lt(e.trim());if(t==="hr")return Go(e.trim());let r=new URL(Bt(e.trim()));Pr(r);let n=r.pathname;if(!zo.test(n)||n.includes("//"))throw Error("\u4E2D\u7EE7\u5730\u5740\u7684\u8DEF\u5F84\u91CC\u6709\u4E0D\u652F\u6301\u7684\u5B57\u7B26");if(t==="bc"){if(n==="/"&&(n="/bc/"),!n.endsWith("/"))throw Error("bc+ \u5730\u5740\u7684\u8DEF\u5F84\u8981\u4EE5 / \u7ED3\u5C3E")}else n==="/"&&(n="");return`${t}+${r.origin}${n}`}function Ut(e){let t=Array.isArray(e)?e:typeof e=="string"?e.trim().split(/[\s,]+/):[],r=[...new Set(t.map(Qe))],n=r.some(o=>lt(o)!=="hr");if(n&&r.some(o=>lt(o)==="hr"))throw Error("\u7ECF\u5178\u4E2D\u7EE7\uFF08wss://\uFF09\u4E0D\u80FD\u4E0E\u53BB\u4E2D\u5FC3\u5316\u4E2D\u7EE7\uFF08bc+ / nostr+ / mqtt+\uFF09\u6DF7\u7528");if(!r.length||r.length>(n?5:3))throw Error(n?"\u53BB\u4E2D\u5FC3\u5316\u4E2D\u7EE7\u8BF7\u586B 1 \u5230 5 \u4E2A\u4E0D\u540C\u5730\u5740":"\u8BF7\u586B 1 \u5230 3 \u4E2A\u4E0D\u540C\u7684\u4E2D\u7EE7\u5730\u5740");return r}var Xo=["wss:","ws://localhost:*","ws://127.0.0.1:*"],tr=e=>[...e.custom?Xo:[],...e.relays.map(Zo)],te=e=>e.some(t=>lt(t)!=="hr")?3:2,er={name:"PBKDF2",hash:"SHA-256",iterations:31e4},Gr=e=>St.encode(`private-talk/html-room/v1/wrap|${e}`);async function zr(e,t){if(typeof e!="string"||!e.length||St.encode(e).length>1024)throw Error("\u53E3\u4EE4\u957F\u5EA6\u65E0\u6548");let r=await crypto.subtle.importKey("raw",St.encode(e),"PBKDF2",!1,["deriveKey"]);return crypto.subtle.deriveKey({...er,salt:t},r,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}function it(e){if(!e||![2,3].includes(e.version)||!/^[0-9a-f]{64}$/.test(e.roomId))throw Error("\u623F\u95F4\u7248\u672C\u6216\u5730\u5740\u9519\u8BEF");if(!Array.isArray(e.relays)||JSON.stringify(Ut(e.relays))!==JSON.stringify(e.relays))throw Error("\u4E2D\u7EE7\u5217\u8868\u683C\u5F0F\u9519\u8BEF");if(te(e.relays)!==e.version)throw Error("\u623F\u95F4\u7248\u672C\u4E0E\u4E2D\u7EE7\u7C7B\u578B\u4E0D\u7B26");if(e.custom!==void 0&&e.custom!==!0)throw Error("\u914D\u7F6E\u5B57\u6BB5\u9519\u8BEF");let t=["version","relays","roomId","encrypted","custom"];if(e.encrypted===!0){let r=[...t,"salt","iv","ciphertext","kdf"];if(Object.keys(e).some(n=>!r.includes(n))||!e.kdf||Object.keys(e.kdf).length!==3||Object.entries(er).some(([n,o])=>e.kdf[n]!==o))throw Error("\u4E0D\u652F\u6301\u7684 KDF \u914D\u7F6E");yt(e.salt,16),yt(e.iv,12),yt(e.ciphertext,48)}else{if(e.encrypted!==!1||Object.keys(e).some(r=>![...t,"master"].includes(r)))throw Error("\u914D\u7F6E\u5B57\u6BB5\u9519\u8BEF");yt(e.master,32)}return e}async function Zr(e,t,{custom:r=!1}={}){let n=Ut(e),o=tt(32),s=await jr(o),i={version:te(n),relays:n,roomId:s.roomId,encrypted:!!t},c;if(!t)c={...i,master:z(o)};else{let a=tt(16),f=tt(12),d=await zr(t,a),u=await crypto.subtle.encrypt({name:"AES-GCM",iv:f,additionalData:Gr(i.roomId)},d,o);c={...i,salt:z(a),iv:z(f),ciphertext:z(u),kdf:{...er}}}return r?{...c,custom:!0}:c}function Xr(e,t){if(it(e),typeof t!="string"||!t.trim())return e;if(e.custom!==!0)throw Error("\u8FD9\u4E2A\u623F\u95F4\u6587\u4EF6\u4E0D\u5141\u8BB8\u81EA\u5B9A\u4E49\u4E2D\u7EE7");let r=Ut(t);return it({...e,version:te(r),relays:r})}async function Fr(e,t){it(e);let r=e.encrypted?new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:yt(e.iv,12),additionalData:Gr(e.roomId)},await zr(t,yt(e.salt,16)),yt(e.ciphertext,48))):yt(e.master,32),n=await jr(r);if(r.fill(0),n.roomId!==e.roomId)throw Error("\u623F\u95F4\u5730\u5740\u4E0E\u5BC6\u94A5\u4E0D\u5339\u914D");return n}async function Yr(e,t){let r=tt(12),n=await crypto.subtle.encrypt({name:"AES-GCM",iv:r,additionalData:St.encode(`private-talk/html-room/v1/keys|${e.roomId}`)},e.key,me(St.encode(t)));return{iv:btoa(String.fromCharCode(...r)),ct:btoa(String.fromCharCode(...new Uint8Array(n)))}}async function Wr(e,t){let r=n=>Uint8Array.from(atob(n),o=>o.charCodeAt(0));return new TextDecoder().decode(we(new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:r(t.iv),additionalData:St.encode(`private-talk/html-room/v1/keys|${e.roomId}`)},e.key,r(t.ct)))))}var be=BigInt(4294967295),Jr=BigInt(32);function Fo(e,t=!1){return t?{h:Number(e&be),l:Number(e>>Jr&be)}:{h:Number(e>>Jr&be)|0,l:Number(e&be)|0}}function Yo(e,t=!1){let r=e.length,n=new Uint32Array(r),o=new Uint32Array(r);for(let s=0;se?`"${e}" `:"";function mt(e,t=""){if(typeof e!="number")throw new TypeError(ve(t)+"expected number, got "+typeof e);if(!Number.isSafeInteger(e)||e<0)throw new RangeError(ve(t)+"expected integer >= 0, got "+e);return e}function Wo(e,t=""){if(typeof e!="boolean")throw new TypeError(ve(t)+"expected boolean, got type="+typeof e);return e}function pt(e,t,r=""){if(Qr(e)&&(t===void 0||e.length===t))return e;t!==void 0&&mt(t,"length");let n=Qr(e),o=t!==void 0?` of length ${t}`:"",s=n?`length=${e.length}`:`type=${typeof e}`,i=ve(r)+"expected Uint8Array"+o+", got "+s;throw n?new RangeError(i):new TypeError(i)}var Jo=(e,t)=>{if(e===null||typeof e!="object"||Array.isArray(e))throw new TypeError((t==="object"?"":`"${t}" `)+"expected object, got type="+typeof e)},tn=(e,t)=>{Jo(e,t);let r=Object.getPrototypeOf(e);if(r!==Object.prototype&&r!==null)throw new TypeError(`"${t}" expected plain object`);if(Object.prototype.hasOwnProperty.call(e,"__proto__"))throw new TypeError(`"${t}.__proto__" is not allowed`)};function en(e,t=!0){if(e.destroyed)throw new Error("hash was destroyed");if(t&&e.finished)throw new Error("digest() was already called")}function Qo(e,t){pt(e,void 0,"output");let r=t.outputLen;if(!(e.length>=r))throw new RangeError('"output" expected length >= '+r)}function ir(e){return new Uint32Array(e.buffer,e.byteOffset,Math.floor(e.byteLength/4))}function fn(...e){for(let t=0;t>>8&65280|e>>>24&255}function rs(e){for(let t=0;te:rs;function ln(e,t,r="opts"){return tn(e,"defaults"),t!==void 0&&tn(t,r),Object.assign(Object.create(null),e,t)}function hn(e,t={}){if(typeof e!="function")throw new TypeError('"hashCons" expected function, got type='+typeof e);t=ln({},t,"info");let r=(o,s)=>e(s).update(o).digest(),n=e(void 0);return r.outputLen=n.outputLen,r.blockLen=n.blockLen,r.canXOF=n.canXOF,r.create=o=>e(o),Object.assign(r,t),Object.freeze(r)}function ns(e=32){mt(e,"bytesLength");let t=typeof globalThis=="object"?globalThis.crypto:null;if(typeof t?.getRandomValues!="function")throw new Error("crypto.getRandomValues must be defined");if(e>65536)throw new RangeError(`"bytesLength" expected <= 65536, got ${e}`);return t.getRandomValues(new Uint8Array(e))}var ke=e=>({oid:Uint8Array.from([6,9,96,134,72,1,101,3,4,2,e])}),os=BigInt(0),ee=BigInt(1),ss=BigInt(2),is=BigInt(7),cs=BigInt(256),as=BigInt(113),un=[],dn=[],pn=[];for(let e=0,t=ee,r=1,n=0;e<24;e++){[r,n]=[n,(2*r+3*n)%5],un.push(2*(5*n+r)),dn.push((e+1)*(e+2)/2%64);let o=os;for(let s=0;s<7;s++)t=(t<>is)*as)%cs,t&ss&&(o^=ee<<(ee<e<>>32-r,us=(e,t,r)=>t<>>32-r,ds=(e,t,r)=>t<>>64-r,ps=(e,t,r)=>e<>>64-r,rn=(e,t,r)=>r>32?ds(e,t,r):hs(e,t,r),nn=(e,t,r)=>r>32?ps(e,t,r):us(e,t,r),qt=new Uint32Array(10);function ys(e,t=24){if(!(e instanceof Uint32Array))throw new TypeError('"s" expected Uint32Array(50), got type='+typeof e);if(e.length!==50)throw new RangeError('"s" expected Uint32Array(50), got length='+e.length);if(mt(t,"rounds"),t<1||t>24)throw new Error('"rounds" expected integer 1..24');for(let r=24-t;r<24;r++){for(let s=0;s<10;s++)qt[s]=e[s]^e[s+10]^e[s+20]^e[s+30]^e[s+40];for(let s=0;s<10;s+=2){let i=(s+8)%10,c=(s+2)%10,a=qt[c],f=qt[c+1],d=rn(a,f,1)^qt[i],u=nn(a,f,1)^qt[i+1];for(let l=0;l<50;l+=10)e[s+l]^=d,e[s+l+1]^=u}let n=e[2],o=e[3];for(let s=0;s<24;s++){let i=dn[s],c=rn(n,o,i),a=nn(n,o,i),f=un[s];n=e[f],o=e[f+1],e[f]=c,e[f+1]=a}for(let s=0;s<50;s+=10){let i=e[s],c=e[s+1],a=e[s+2],f=e[s+3];e[s]^=~e[s+2]&e[s+4],e[s+1]^=~e[s+3]&e[s+5],e[s+2]^=~e[s+4]&e[s+6],e[s+3]^=~e[s+5]&e[s+7],e[s+4]^=~e[s+6]&e[s+8],e[s+5]^=~e[s+7]&e[s+9],e[s+6]^=~e[s+8]&i,e[s+7]^=~e[s+9]&c,e[s+8]^=~i&a,e[s+9]^=~c&f}e[0]^=fs[r],e[1]^=ls[r]}fn(qt)}var mn=class wn{constructor(t,r,n,o=!1,s=24){st(this,"state");st(this,"pos",0);st(this,"posOut",0);st(this,"finished",!1);st(this,"state32");st(this,"destroyed",!1);st(this,"blockLen");st(this,"suffix");st(this,"outputLen");st(this,"canXOF");st(this,"enableXOF",!1);st(this,"rounds");if(mt(t,"blockLen"),mt(r,"suffix"),mt(s,"rounds"),Wo(o,"enableXOF"),this.blockLen=t,this.suffix=r,this.outputLen=n,this.enableXOF=o,this.canXOF=o,this.rounds=s,mt(n,"outputLen"),!(0=r?ir(t):void 0;for(let f=0;f=r){for(let u=0,l=f/4;u=n&&this.keccak();let i=Math.min(n-this.posOut,s-o);t.set(r.subarray(this.posOut,this.posOut+i),o),this.posOut+=i,o+=i}return t}xofInto(t){if(!this.enableXOF)throw new Error("XOF is not enabled");return this.writeInto(t)}xof(t){return mt(t),this.xofInto(new Uint8Array(t))}digestInto(t){if(Qo(t,this),this.finished)throw new Error("digest() was already called");this.writeInto(t.length===this.outputLen?t:t.subarray(0,this.outputLen)),this.destroy()}digest(){let t=new Uint8Array(this.outputLen);return this.digestInto(t),t}destroy(){this.destroyed=!0,fn(this.state)}_cloneInto(t){let{blockLen:r,suffix:n,outputLen:o,rounds:s,enableXOF:i}=this;return t||(t=new wn(r,n,o,i,s)),t.blockLen=r,t.state32.set(this.state32),t.pos=this.pos,t.posOut=this.posOut,t.finished=this.finished,t.rounds=s,t.suffix=n,t.outputLen=o,t.enableXOF=i,t.canXOF=this.canXOF,t.destroyed=this.destroyed,t}},bn=(e,t,r,n={})=>hn(()=>new mn(t,e,r),n),ms=bn(6,136,32,ke(8)),ws=bn(6,72,64,ke(10)),gn=(e,t,r,n={})=>hn((o={})=>(o=ln({},o),new mn(t,e,o.dkLen===void 0?r:o.dkLen,!0)),n),bs=gn(31,168,16,ke(11)),xn=gn(31,136,32,ke(12));function rr(e,t="object"){if(e===null||typeof e!="object"||Array.isArray(e))throw new TypeError(t==="object"?"expected valid options object":`"${t}" expected object, got type=${typeof e}`);return e}function gs(e,t={},r={},n="object"){rr(e,n),rr(t,"fields"),rr(r,"optFields");function o(i,c,a){let f=n==="object"?`param "${String(i)}"`:`"${n}.${String(i)}"`,d=e[i];if(!Object.prototype.hasOwnProperty.call(e,i)&&(a?d!==void 0:c!=="function"))throw new TypeError(`${f} is invalid: expected own property`);if(a&&d===void 0)return;let u=typeof d;if(u!==c||d===null)throw new TypeError(`${f} is invalid: expected ${c}, got ${u}`)}let s=(i,c)=>Object.entries(i).forEach(([a,f])=>o(a,f,c));s(t,!1),s(r,!0)}function oe(e,t="n"){if(typeof e!="number")throw new TypeError(`wrong u32 integer "${t}": expected number, got type=${typeof e}`);if(!Number.isSafeInteger(e)||e<0||e>4294967295)throw new RangeError(`wrong u32 integer "${t}": expected 0..4294967295, got ${e}`);return e}function vn(e){return oe(e,"x"),(e&e-1)===0&&e!==0}function En(e,t){if(oe(e),typeof t!="number")throw new TypeError('"bits" expected number, got type='+typeof t);if(!Number.isSafeInteger(t)||t<0||t>32)throw new Error(`expected integer 0 <= bits <= 32, got ${t}`);let r=0;for(let n=0;n>>=1)r=r<<1|e&1;return r>>>0}function kn(e){return oe(e),31-Math.clz32(e)}function on(e){if(!e||typeof e!="object"||typeof e.length!="number")throw new TypeError('"values" expected array-like, got type='+typeof e);let t=e.length;if(!vn(t))throw new Error("expected positive power-of-two length, got "+t);let r=kn(t);for(let n=0;n{gs(t,{N:"number",roots:"object",dit:"boolean"},{invertButterflies:"boolean",skipStages:"number",brp:"boolean"},"coreOpts");let{N:r,roots:n,dit:o,invertButterflies:s=!1,skipStages:i=0,brp:c=!0}=t;oe(r,"coreOpts.N");let a=kn(r);if(!vn(r))throw new Error("FFT: Polynomial size should be power of two");oe(i,"coreOpts.skipStages");let f=a===0?0:a-1;if(i>f)throw new Error(`FFT: wrong skipStages: expected 0 <= skipStages <= ${f}`);if(n.length!==r)throw new Error(`FFT: wrong roots length: expected ${r}, got ${n.length}`);let d=o!==s;return u=>{if(u.length!==r)throw new Error("FFT: wrong Polynomial length");o&&c&&on(u);for(let l=0,h=1;l>1,A=r>>y;for(let $=0;${}){if(!Array.isArray(e))throw new TypeError(`"${t}" expected array, got type=${typeof e}`);for(let n=0;ntypeof o=="number"?o:o.bytesLen,n=t.reduce((o,s)=>o+r(s),0);return{bytesLen:n,encode:o=>{let s=new Uint8Array(n);for(let i=0,c=0;i{pt(o,n,e);let s=[];for(let i of t){let c=r(i),a=o.subarray(0,c);s.push(typeof i=="number"?a:i.decode(a)),o=o.subarray(c)}return s}}}function or(e,t){let r=e,n=t*r.bytesLen;return{bytesLen:n,encode:o=>{let s=xs(o,"u");if(s.length!==t)throw new RangeError(`vecCoder.encode: wrong length=${s.length}. Expected: ${t}`);let i=new Uint8Array(n);for(let c=0,a=0;c{pt(o,n);let s=[];for(let i=0;i32)throw new RangeError('"bits" expected <= 32, got '+e);return e===32?4294967295:~(-1<>>0}var vs=e=>{let{newPoly:t,N:r,Q:n,F:o,ROOT_OF_UNITY:s,brvBits:i,isKyber:c}=e,a=(g,v=n)=>{let S=g%v|0;return(S>=0?S|0:v+S|0)|0},f=(g,v=n)=>{let S=a(g,v)|0;return(S>v>>1?S-v|0:S)|0};function d(){let g=t(r);for(let v=0;v{throw new Error("not implemented")},h=c?{add:(g,v)=>{let S=g+v|0;return S>=n?S-n|0:S},sub:(g,v)=>{let S=g-v|0;return S<0?S+n|0:S},mul:(g,v)=>a((g|0)*(v|0))|0,inv:l}:{add:(g,v)=>a((g|0)+(v|0))|0,sub:(g,v)=>a((g|0)-(v|0))|0,mul:(g,v)=>a((g|0)*(v|0))|0,inv:l},y={N:r,roots:u,invertButterflies:!0,skipStages:c?1:0,brp:!1},p=sn(h,{dit:!1,...y}),k=sn(h,{dit:!0,...y}),A={encode:g=>p(g),decode:g=>{k(g);for(let v=0;vA.encode(g),decode:g=>A.decode(g)},bitsCoder:(g,v)=>{for(let I=0,H=0;I32&&an(H),H%=8;let S=an(g),R=g*(r/8);return{bytesLen:R,encode:I=>{let H=I,x=new Uint8Array(R);for(let M=0,U=0,C=0,b=0;M=8;C-=8,U>>=8)x[b++]=U&255;return x},decode:I=>{let H=t(r);for(let x=0,M=0,U=0,C=0;x=g;U-=g,M>>=g)H[C++]=v.decode(M&S);return H}}}}},Es=e=>(t,r)=>{r||(r=e.blockLen);let n=new Uint8Array(t.length+2);n.set(t);let o=t.length,s=new Uint8Array(r),i=e.create({}),c=0,a=0;return{stats:()=>({calls:c,xofs:a}),get:(f,d)=>(n[o+0]=f,n[o+1]=d,i.destroy(),i=e.create({}).update(n),c++,()=>(a++,i.xofInto(s))),clean:()=>{i.destroy(),Y(s,n)}}},ks=Es(bs);var J=256,W=3329,As=3303,Ss=17,nt=vs({N:J,Q:W,F:As,ROOT_OF_UNITY:Ss,newPoly:e=>new Uint16Array(e),brvBits:7,isKyber:!0}),Bs=Object.freeze({512:Object.freeze({N:J,Q:W,K:2,ETA1:3,ETA2:2,du:10,dv:4,RBGstrength:128}),768:Object.freeze({N:J,Q:W,K:3,ETA1:2,ETA2:2,du:10,dv:4,RBGstrength:192}),1024:Object.freeze({N:J,Q:W,K:4,ETA1:2,ETA2:2,du:11,dv:5,RBGstrength:256})}),Ts=e=>{if(e>=12)return{encode:r=>r,decode:r=>r>=W?r-W:r};let t=2**(e-1);return{encode:r=>((r<r*W+t>>>e}},Is=e=>nt.bitsCoder(e,e===12?{encode:t=>t,decode:t=>t>=W?t-W:t}:{encode:t=>t,decode:t=>t}),re=e=>e===12?Is(12):nt.bitsCoder(e,Ts(e));function Ct(e,t){let r=e,n=t;for(let o=0;o=W?s-W:s}}function Os(e,t){let r=e,n=t;for(let o=0;o>1)];o&1&&(s=-s);let{c0:i,c1:c}=_s(r[2*o+0],r[2*o+1],n[2*o+0],n[2*o+1],s);r[2*o+0]=i,r[2*o+1]=c}return r}function sr(e){let t=e,r=new Uint16Array(J);for(let n=0;n>0|o[s+1]<<8)&4095,c=(o[s+1]>>4|o[s+2]<<4)&4095;i{let r=new Uint16Array(J),n=ir(e);Ee(n);let o=0;for(let s=0,i=0,c=0,a=0;s>=1,o+=1,o===t?(a=c,c=0):o===2*t&&(r[i++]=nt.mod(a-c),c=0,o=0)}if(Ee(n),o)throw new Error(`sampleCBD: leftover bits: ${o}`);return r};function ne(e,t,r,n){return Hs(e(n*J/4,t,r),n)}var Us=e=>{let t=e,{K:r,PRF:n,XOF:o,HASH512:s,ETA1:i,ETA2:c,du:a,dv:f}=t,d=re(1),u=re(f),l=re(a),h=xe("publicKey",or(re(12),r),32),y=or(re(12),r),p=xe("ciphertext",or(l,r),u),k=xe("seed",32,32),A=($,g,v,S)=>{let R=[];for(let U=0;U{dt($,32,"seed");let g=new Uint8Array(33);g.set($),g[32]=r;let v=s(g),[S,R]=k.decode(v),I=[],H=[];for(let U=0;U{let[S,R]=h.decode($),I=o(R),H=A(S,(x,M)=>sr(I.get(x,M)),g,v);return I.clean(),H},prepare:$=>{let[g,v]=h.decode($),S=o(v),R=[];for(let I=0;IA(g.map(x=>x.slice()),(x,M)=>R[x*r+M].slice(),I,H),clean:()=>Y(g,R)}},decrypt:($,g)=>{let[v,S]=p.decode($),R=y.decode(g),I=new Uint16Array(J);for(let x=0;x{let p=h.subarray(0,384*t.K),k=i.encode(i.decode(cn(p))),A=jt(k,p);if(Y(k),!A)throw new Error(`ML-KEM.${y}: wrong publicKey modulus`)},l=Object.freeze({...c,seed:64,msg:f,msgRand:f,secretKey:a.bytesLen});return Object.freeze({info:Object.freeze({type:"ml-kem"}),lengths:l,keygen:h=>{let y=h===void 0,p=y?nr(d):h,k,A;try{dt(p,d,"seed");let $=r.keygen(p.subarray(0,32)),g=$.publicKey;k=$.secretKey,A=n(g);let v=a.encode([k,g,A,p.subarray(32)]);return{publicKey:g,secretKey:v}}finally{k!==void 0&&Y(k),A!==void 0&&Y(A),y&&Y(p)}},getPublicKey:h=>{let[y,p,k,A]=a.decode(h);return Uint8Array.from(p)},encapsulate:(h,y)=>{let p=y===void 0,k=p?nr(f):y,A;try{return dt(h,c.publicKey,"publicKey"),dt(k,f,"message"),u(h,"encapsulate"),A=o.create().update(k).update(n(h)).digest(),{cipherText:r.encrypt(h,k,A.subarray(32,64)),sharedSecret:A.subarray(0,32)}}finally{A!==void 0&&Y(A.subarray(32)),p&&Y(k)}},decapsulate:(h,y)=>{dt(y,a.bytesLen,"secretKey"),dt(h,c.cipherText,"cipherText");let p=a.bytesLen-96,k=p+32,A=n(y.subarray(p/2,k));if(!jt(A,y.subarray(k,k+32)))throw new Error("invalid secretKey: hash check failed");let[$,g,v,S]=a.decode(y),R=r.decrypt(h,$),I=o.create().update(R).update(v).digest(),H=I.subarray(0,32),x=r.encrypt(g,R,I.subarray(32,64)),M=jt(h,x),U=s.create({dkLen:32}).update(S).update(h).digest();return Y(R,x,I.subarray(32),M?U:H),M?H:U},prepare:h=>{dt(h,c.publicKey,"publicKey"),u(h,"prepare");let y=cn(h),p=n(y),k=r.prepare(y);return Object.freeze({publicKey:y,encapsulate:A=>{let $=A===void 0,g=$?nr(f):A,v;try{return dt(g,f,"message"),v=o.create().update(g).update(p).digest(),{cipherText:k.encrypt(g,v.subarray(32,64)),sharedSecret:v.subarray(0,32)}}finally{v!==void 0&&Y(v.subarray(32)),$&&Y(g)}},decapsulate:(A,$)=>{dt($,a.bytesLen,"secretKey"),dt(A,c.cipherText,"cipherText");let[g,v,S,R]=a.decode($);if(!jt(v,y)||!jt(S,p))throw new Error("ML-KEM.decapsulate: secretKey does not match prepared publicKey");let I=r.decrypt(A,g),H=o.create().update(I).update(p).digest(),x=H.subarray(0,32),M=k.encrypt(I,H.subarray(32,64)),U=jt(A,M),C=s.create({dkLen:32}).update(R).update(A).digest();return Y(I,M,H.subarray(32),U?C:x),U?x:C},clean:k.clean})}})}function Ls(e,t,r){return xn.create({dkLen:e}).update(t).update(new Uint8Array([r])).digest()}var Rs={HASH256:ms,HASH512:ws,KDF:xn,XOF:ks,PRF:Ls},$s=e=>Cs({...Rs,...e}),Ae=$s(Bs[768]);var ft=new TextEncoder,cr=new TextDecoder,Un={name:"ECDH",namedCurve:"P-256"},An={name:"ECDSA",namedCurve:"P-256"},Se={name:"ECDSA",hash:"SHA-256"},Ks=2e3,Ns=100,Ms=600*1e3,Sn=60*1e3,Ds=20*1e3,qs=4,js=4e3,Bn=1580,Ps=1452,Vs=/^(\d{1,9})\.(\d{1,9})$/,Be=/^[A-Za-z0-9+/]+={0,2}$/,Gs=/,"s":"([A-Za-z0-9+/]{86}==)"\}$/,bt=e=>btoa(String.fromCharCode(...new Uint8Array(e))),wt=e=>Uint8Array.from(atob(e),t=>t.charCodeAt(0)),Tn=e=>Array.from(new Uint8Array(e),t=>t.toString(16).padStart(2,"0")).join("");async function In(e,t){let r=await crypto.subtle.importKey("raw",e,{name:"HMAC",hash:"SHA-256"},!1,["sign"]);return new Uint8Array(await crypto.subtle.sign("HMAC",r,new Uint8Array([t])))}async function ar(e){return{mk:await In(e,1),next:await In(e,2)}}async function On(e,t){return crypto.subtle.importKey("raw",e,"AES-GCM",!1,t)}async function Te(e,t,r){let n=crypto.getRandomValues(new Uint8Array(12)),o=await crypto.subtle.encrypt({name:"AES-GCM",iv:n,additionalData:ft.encode(r)},e,t);return{iv:bt(n),ct:bt(o)}}async function fr(e,{iv:t,ct:r},n){return new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:wt(t),additionalData:ft.encode(n)},e,wt(r)))}var zs=(e,t)=>{let r=new Uint8Array(e.length+t.length);return r.set(e),r.set(t,e.length),r};async function _n(){let e=await crypto.subtle.generateKey(Un,!1,["deriveBits"]);return{pair:e,epk:bt(await crypto.subtle.exportKey("raw",e.publicKey))}}var Hn=e=>`pt31a|${e.from}|${e.id}|${e.t}|${e.epk}|${e.hello?1:0}|${JSON.stringify(e.w)}`,Ie=class e{static async create(t,r,n){let o=new e;return o.key=t,o.bind=r,o.me=n,o.signing=await crypto.subtle.generateKey(An,!1,["sign","verify"]),o.spk=bt(await crypto.subtle.exportKey("raw",o.signing.publicKey)),o.kem=Ae.keygen(),o.kpk=bt(o.kem.publicKey),o.ecdh=await _n(),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(t=this.mine.epoch+1,r=Date.now()){this.mine&&r-this.ecdhSince>=Ds&&(this.oldEcdh=[{ecdh:this.ecdh,until:r+Sn},...this.oldEcdh].slice(0,qs),this.ecdh=await _n(),this.ecdhSince=r),this.oldEcdh=this.oldEcdh.filter(n=>n.until>r),this.mine={epoch:t,ck:crypto.getRandomValues(new Uint8Array(32)),c:0,since:r,handed:new Map};for(let n of this.peers.values())n.sent=-1,n.gotAt=null}due(t=Date.now()){return this.mine.c>=Ns||t-this.mine.since>=Ms}forget(t){this.peers.delete(t)}async pairKey(t,r,n=!1){let o=n?`${t.epk}|${r.kcIn??r.kc}`:t.epk;if(r.wraps.has(o))return r.wraps.get(o);let s=await crypto.subtle.importKey("raw",wt(r.epk),Un,!1,[]),i=new Uint8Array(await crypto.subtle.deriveBits({name:"ECDH",public:s},t.pair.privateKey,256)),c=await crypto.subtle.importKey("raw",n?zs(i,r.ss):i,"HKDF",!1,["deriveKey"]),a=ft.encode(`private-talk/${n?"v3.2":"v3"}/pair|${[t.epk,r.epk].sort().join("|")}`),f=await crypto.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:this.bind,info:a},c,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"]);return r.wraps.set(o,f),f}pqFor(t,r){if(r.v!==32||this.mer!==this.me&&this.peers.has(r)&&this.peers.get(r).sent!==this.mine.epoch)}async announce(t,r,n=Date.now()){return(await this.announceAll(t,r,n,1/0))[0]}async announceAll(t,r,n=Date.now(),o=js){let s=this.mine;for(let[y,p]of s.handed)n-p.at>Sn&&s.handed.delete(y);let i=[...this.peers].some(([y,p])=>y{let y=c&&u?{q:1,k:this.kpk,...f}:{q:1,...f},p={kind:"keys",v:32,from:this.me,id:Tn(crypto.getRandomValues(new Uint8Array(8))),t:n,epk:this.epk,spk:this.spk,hello:!!r&&u,w:y};p.sig=bt(await crypto.subtle.sign(Se,this.signing.privateKey,ft.encode(Hn(p)))),a.push(JSON.stringify(p)),f={},d=0,u=!1},h=()=>430+(c&&u?Bn+8:0);for(let y of t){let p=this.peers.get(y);if(!p)continue;let{pq:k,can:A}=this.pqFor(y,p);if(!A)continue;let $=p.gotAt!=null&&s.handed.has(p.gotAt),g=$?p.gotAt:s.c,v=$?s.handed.get(g).ck:s.ck;$||(p.gotAt=g,s.handed.has(g)||s.handed.set(g,{ck:v,at:n}));let S=ft.encode(JSON.stringify({e:s.epoch,ck:bt(v),c:g})),R=k?{...await Te(await this.pairKey(this.ecdh,p,!0),S,`pt32w|${this.me}|${y}|${p.epk}`),q:1,...p.kc&&{kc:p.kc}}:await Te(await this.pairKey(this.ecdh,p),S,`pt3w|${this.me}|${y}|${p.epk}`),I=JSON.stringify(R).length+8;Object.keys(f).length&&h()+d+I>o&&await l(),f[y]=R,d+=I,p.sent=this.mine.epoch}return(u||Object.keys(f).length)&&await l(),a}async receive(t,r){if(!t||t.kind!=="keys"||t.from!==r||r===this.me||typeof t.epk!="string"||!Be.test(t.epk)||t.epk.length>120||!t.w||typeof t.w!="object")return null;let n=typeof t.spk=="string",o=null;if(n){if(!Be.test(t.spk)||t.spk.length>120||typeof t.sig!="string")throw new Error("bad announcement");let c=this.peers.get(r);if(o=c?.spk===t.spk?c.verify:await crypto.subtle.importKey("raw",wt(t.spk),An,!1,["verify"]),!await crypto.subtle.verify(Se,o,wt(t.sig),ft.encode(Hn(t))))throw new Error("bad signature")}let s=this.peers.get(r);s&&(s.spk??null)!==(n?t.spk:null)&&(s.spk&&this.changed.push(r),s=null),s?s.epk!==t.epk&&(n?(s.epk=t.epk,s.wraps=new Map):(s={epk:t.epk,spk:null,verify:null,wraps:new Map,recv:null,prev:null,sent:-1,gotAt:null,v:30},this.peers.set(r,s))):(s={epk:t.epk,spk:n?t.spk:null,verify:o,wraps:new Map,recv:null,prev:null,sent:-1,gotAt:null,v:n?31:30,kpk:null,ss:null,kc:null,kcIn:null,pqIn:!1},this.peers.set(r,s)),n&&t.w.q===1&&s.v!==32&&(s.v=32,s.sent=-1),n&&typeof t.w.k=="string"&&t.w.k.length===Bn&&Be.test(t.w.k)&&t.w.k!==s.kpk&&(Object.assign(s,{kpk:t.w.k,ss:null,kc:null,kcIn:null,v:32}),s.wraps=new Map);let i=t.w[this.me];if(i&&typeof i.iv=="string"&&typeof i.ct=="string"){let c=i.q===1;c&&n&&(s.v=32),c&&ru.ecdh)],f=null;if(!c||s.ss)for(let u of a)try{f=JSON.parse(cr.decode(await fr(await this.pairKey(u,s,c),i,`${c?"pt32w":"pt3w"}|${r}|${this.me}|${u.epk}`)));break}catch{}f&&c&&this.me=0&&d.length===32&&(!s.recv||f.e>s.recv.epoch)?(s.prev=s.recv,s.recv={epoch:f.e,ck:d,c:f.c},s.pqIn=c):f&&c&&s.recv&&f.e===s.recv.epoch&&(s.pqIn=!0)}return t.hello&&(s.sent=-1),s}takeChanged(){let t=this.changed;return this.changed=[],t}ready(t,r=!1){return t.every(n=>n===this.me||this.peers.get(n)?.sent===this.mine.epoch&&(!r||this.peers.get(n).v>=31))}quantumSafe(t){return t.every(r=>r===this.me||this.peers.get(r)?.v===32&&this.peers.get(r).ss&&this.peers.get(r).pqIn)}older(t){return t.filter(r=>r!==this.me&&this.peers.get(r)?.v===30)}async pairSecret(t,r){let n=this.peers.get(t);if(!n?.ss)throw new Error("no pair secret");let o=await crypto.subtle.importKey("raw",n.ss,"HKDF",!1,["deriveKey"]),s=ft.encode(`private-talk/v3.2/for|${r}|${Math.min(t,this.me)}|${Math.max(t,this.me)}`);return crypto.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:this.bind,info:s},o,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}async sealFor(t,r,n){return Te(await this.pairSecret(t,n),ft.encode(r),`pt32f|${this.me}|${t}|${n}`)}async openFrom(t,r,n){return cr.decode(await fr(await this.pairSecret(t,n),r,`pt32f|${t}|${this.me}|${n}`))}async seal(t){let r=this.mine,n=`${r.epoch}.${r.c}`,{mk:o,next:s}=await ar(r.ck);r.ck=s,r.c++;let i=bt(await crypto.subtle.sign(Se,this.signing.privateKey,ft.encode(`pt31m|${this.me}|${n}|${t}`))),c=`${t.slice(0,-1)},"s":"${i}"}`;return{...await Te(await On(o,["encrypt"]),me(ft.encode(c)),`pt3|${this.me}|${n}`),h:n}}async open(t,r){let n=Vs.exec(t.h??""),o=this.peers.get(r);if(!n||!o?.recv)throw new Error("no key");let s=Number(n[1]),i=Number(n[2]),c=s===o.recv.epoch?"recv":s===o.prev?.epoch?"prev":null,a=o[c];if(!c||iKs)throw new Error("out of step");let f=a.ck;for(let p=a.c;p=31){if(!y)throw new Error("unsigned");if(l=`${l.slice(0,y.index)}}`,!await crypto.subtle.verify(Se,o.verify,wt(y[1]),ft.encode(`pt31m|${r}|${t.h}|${l}`)))throw new Error("bad signature");h=!0}return o[c]={epoch:s,ck:u,c:i+1},{text:l,missed:i-a.c,signed:h}}async safetyCode(t){let r=i=>i===this.me?this.spk:this.peers.get(i)?.spk??this.peers.get(i)?.epk??"?",n=t.map(i=>`${i}:${r(i)}`).sort().join("|"),o=new Uint8Array(await crypto.subtle.digest("SHA-256",ft.encode(`private-talk/v3.1/safety|${Tn(this.bind)}|${n}`))),s=0n;for(let i of o.subarray(0,12))s=s<<8n|BigInt(i);return(s%10n**20n).toString().padStart(20,"0").replace(/(\d{5})(?=\d)/g,"$1 ")}};function _e(e){let t=e.trim();return!t||Array.from(t).length>500||new TextEncoder().encode(t).byteLength>2048?null:t}var Oe=600*1e3,Cn=/^[0-9a-f]{16}$/;function Ln(e,t,r=Date.now(),n,o,s){let i=Array.from(crypto.getRandomValues(new Uint8Array(8)),a=>a.toString(16).padStart(2,"0")).join(""),c=Number.isInteger(s)&&s>=0&&s<=5;return JSON.stringify({from:e,id:i,t:r,text:t,...n&&{g:n},...o&&{f:o},...c&&{q:s}})}function Rn(e,t,r,n=Date.now()){let o;try{o=JSON.parse(e)}catch{return null}if(!o||typeof o!="object"||typeof o.text!="string"||typeof o.id!="string"||!Cn.test(o.id)||!Number.isInteger(o.from)||o.from<1||o.from>25||o.from!==t.from||!Number.isFinite(o.t)||!Number.isFinite(t.at)||Math.abs(n-o.t)>Oe||Math.abs(t.at-o.t)>Oe)return null;let s=Number.isInteger(o.q)&&o.q>=0&&o.q<=5;if(s){if(o.text!==""&&_e(o.text)!==o.text)return null}else if(_e(o.text)!==o.text)return null;for(let[f,d]of r)Math.abs(n-d)>2*Oe&&r.delete(f);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,c=o.f&&typeof o.f=="object"&&!Array.isArray(o.f)?o.f:void 0,a=s?o.q:void 0;return{text:o.text,id:o.id,g:i,f:c,q:a}}function lr(e,t,r,n=Date.now(),o="keys"){let s;try{s=JSON.parse(e)}catch{return null}return!s||typeof s!="object"||s.kind!==o||typeof s.id!="string"||!Cn.test(s.id)||!Number.isInteger(s.from)||s.from!==t.from||!Number.isFinite(s.t)||Math.abs(n-s.t)>Oe||r.has(s.id)?null:(r.set(s.id,s.t),s)}var Zs=()=>new Map,He=class{constructor(t,r,{storage:n=Zs(),emit:o=()=>{}}={}){Object.assign(this,{d:t,config:r,store:n,emit:o,deviceId:z(tt(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(t){return this.serial=this.serial.then(()=>{if(!this.stopped)return t()}).catch(()=>this.emit("notice","\u6536\u5230\u4E00\u6761\u65E0\u6CD5\u9A8C\u8BC1\u7684\u6570\u636E\uFF0C\u5DF2\u5FFD\u7565\u3002")),this.serial}send(t){!this.stopped&&this.ws?.readyState===1&&this.ws.send(JSON.stringify(t))}async announce(t=!1){if(!this.group)return;let r=this.group.owed(this.present);if(!(!t&&!r.length))for(let n of await this.group.announceAll(r,t))this.send({type:"key",...await Yr(this.d,n)})}ready(){return!this.stopped&&this.group&&this.present.length>1&&this.group.ready(this.present,!0)&&this.group.quantumSafe(this.present)}async incoming(t){if(this.stopped)return;if(t.type==="pong"){this.lastPong=Date.now();return}if(t.type==="wait"){this.nextSend=Date.now()+Math.min(3e4,Math.max(1e3,t.ms||6e3));return}if(t.type==="hello"){this.resume=t.resume,(!this.group||this.group.me!==t.you)&&(this.group=await Ie.create(this.d.key,this.d.bind,t.you),this.own.clear(),this.received.clear(),this.receipts=[],this.present=[]),await this.announce(!0);return}if(!this.group)return;if(t.type==="members"){if(!Array.isArray(t.members)||t.members.some(i=>!Number.isInteger(i)||i<1||i>25))return;let s=this.present.filter(i=>!t.members.includes(i));for(let i of s){this.group.forget(i),this.config.version===3&&this.pins.delete(i);for(let c of this.received.keys())c.startsWith(`${i}|`)&&this.received.delete(c)}this.present=t.members,s.length&&await this.group.rekey(),await this.announce(!0),this.emit("presence",this.present.length),await this.update();return}if(t.from===this.group.me){let s=this.own.get(t.h);t.type==="msg"&&s&&s.frame.iv===t.iv&&s.frame.ct===t.ct&&(s.echo=!0,this.emit("delivery",{id:s.id,text:"\u5DF2\u53D1\u51FA"}));return}if(!this.present.includes(t.from))return;if(t.type==="key"){let s=lr(await Wr(this.d,t),t,this.seen);if(!s||s.v!==32||!s.spk)return;let i=this.pins.get(t.from),c=await this.group.receive(s,t.from);if(!c)return;if(i&&i!==c.spk){this.halt(),this.emit("fatal","\u6709\u6210\u5458\u7684\u8EAB\u4EFD\u53D8\u4E86\uFF0C\u53EF\u80FD\u6709\u4EBA\u5192\u5145\u3002\u8BF7\u6253\u7535\u8BDD\u7B49\u65B9\u5F0F\u786E\u8BA4\u540E\uFF0C\u91CD\u65B0\u6253\u5F00\u623F\u95F4\u3002");return}this.pins.set(t.from,c.spk),c?.lost&&(c.sent=-1),await this.announce(),await this.update();return}if(!["msg","seen"].includes(t.type)||!t.h)return;let r=`${t.from}|${t.h}`,n=this.received.get(r);if(n){t.type==="msg"&&n.iv===t.iv&&n.ct===t.ct&&this.receipts.length<100&&this.receipts.push([t.from,t.h]);return}let o=await this.group.open(t,t.from);if(!(!o.signed||this.group.peers.get(t.from)?.v!==32))if(t.type==="seen"){let s=lr(o.text,t,this.seen,Date.now(),"seen");if(s&&Array.isArray(s.l)&&s.l.length<=25)for(let i of s.l){if(!Array.isArray(i)||i[0]!==this.group.me)continue;let c=this.own.get(i[1]);c&&c.targets.includes(t.from)&&(c.confirmed.add(t.from),this.emit("delivery",{id:c.id,text:`\u5DF2\u9001\u8FBE ${c.confirmed.size}/${c.targets.length}`}))}}else{let s=Rn(o.text,t,this.seen);if(!s||s.g||s.f)return;for(this.received.set(r,{iv:t.iv,ct:t.ct});this.received.size>200;)this.received.delete(this.received.keys().next().value);this.emit("message",{id:s.id,from:t.from,text:s.text}),this.receipts.length<100&&this.receipts.push([t.from,t.h])}}async update(){this.ready()&&this.emit("ready",await this.group.safetyCode(this.present)),await this.flush()}queue(t){if(this.stopped||(t=_e(t),!t||this.queued.length>=20))return!1;let r=z(tt(8));return this.queued.push({id:r,text:t}),this.emit("message",{id:r,from:"\u6211",text:t}),this.emit("delivery",{id:r,text:"\u53D1\u9001\u4E2D\u2026\u2026"}),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 t=JSON.stringify({kind:"seen",id:z(tt(8)),from:this.group.me,t:Date.now(),l:this.receipts.splice(0,25)});this.send({type:"seen",...await this.group.seal(t)})}if(this.queued.length&&Date.now()>=(this.nextSend??0)){let t=this.queued.shift(),r={type:"msg",...await this.group.seal(Ln(this.group.me,t.text))};for(this.own.set(r.h,{...t,frame:r,targets:this.present.filter(n=>n!==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[t,r]of this.seen)Date.now()-r>12e5&&this.seen.delete(t);for(;this.seen.size>4096;)this.seen.delete(this.seen.keys().next().value)}}async retryPending(){for(let t of this.own.values())!t.echo&&Date.now()-t.at>8e3&&(t.retries??0)<3&&(this.send(t.frame),t.at=Date.now(),t.retries=(t.retries??0)+1),!t.echo&&(t.retries??0)>=3&&this.emit("delivery",{id:t.id,text:"\u53EF\u80FD\u6CA1\u53D1\u51FA\u53BB\uFF0C\u8BF7\u786E\u8BA4\u540E\u91CD\u53D1"})}resetRelay(){for(let t of this.own.values())(t.confirmed.sizenew WebSocket(d,u)}){Object.assign(this,{relays:t,roomId:r,protocols:n,onactive:o,onmessage:s,onnotice:i,onfatal:c,ondisconnect:a,createSocket:f}),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 t=this.active;if(t?.ready){if(Date.now()-t.lastPong>65e3){this.fail(t);return}if(Date.now()-t.lastPing>=25e3){t.lastPing=Date.now();try{t.ws.send(JSON.stringify({type:"ping"}))}catch{this.fail(t)}}}},1e3)}connect(t,r=!1){if(this.stopped)return;let n={index:t,probe:r,frames:[],ready:!1,retired:!1};r?this.probe=n:this.pending=n;try{n.ws=this.createSocket(`${this.relays[t]}/hr/${this.roomId}`,this.protocols(t))}catch{this.fail(n);return}n.timeout=setTimeout(()=>this.fail(n),1e4),n.ws.onerror=()=>{!n.retired&&!n.probe&&this.onnotice("\u8FDE\u4E0D\u4E0A\u4E2D\u7EE7\uFF08\u53EF\u80FD\u662F\u7F51\u7EDC\u3001\u8BC1\u4E66\u6216\u6743\u9650\u95EE\u9898\uFF09\uFF0C\u6B63\u5728\u91CD\u8BD5\u2026\u2026")},n.ws.onclose=o=>this.fail(n,o.code),n.ws.onmessage=o=>{if(n.retired||this.stopped||typeof o.data!="string"||o.data.length>7e3)return;let s;try{s=JSON.parse(o.data)}catch{return}if(!(!s||typeof s!="object")){if(n.ready){if(this.active!==n)return;s.type==="pong"&&(n.lastPong=Date.now()),this.onmessage(s);return}if(n.frames.length>=50){this.fail(n);return}n.frames.push(s),s.type==="hello"&&!n.activating&&Number.isInteger(s.you)&&s.you>=1&&s.you<=25&&typeof s.resume=="string"&&/^[0-9a-f]{32}$/.test(s.resume)&&(n.activating=!0,this.activate(n).catch(()=>this.fail(n)))}}}async activate(t){let r=this.active,n=this.lastIndex!==-1&&this.lastIndex!==t.index;if(this.active=t,this.probe===t&&(this.probe=null),this.pending===t&&(this.pending=null),r&&r!==t&&this.retire(r),await this.onactive(t.ws,t.index,n),!(t.retired||this.stopped||this.active!==t)){clearTimeout(t.timeout),this.lastIndex=t.index,this.backoff=1e3,t.ready=!0,t.lastPong=t.lastPing=Date.now();for(let o of t.frames)this.onmessage(o);t.frames=[]}}retire(t){if(!(!t||t.retired)){t.retired=!0,clearTimeout(t.timeout);try{t.ws?.close(4e3)}catch{}}}fail(t,r){if(!(t.retired||this.stopped)){if(this.retire(t),this.probe===t){this.probe=null;return}if(this.pending===t&&(this.pending=null),this.active===t&&(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((t.index+1)%this.relays.length),this.backoff),this.backoff=Math.min(this.backoff*2,3e4),r!==1006&&this.onnotice("\u8FDE\u63A5\u65AD\u4E86\uFF0C\u6B63\u5728\u6362\u4E00\u4E2A\u4E2D\u7EE7\u91CD\u8BD5\u2026\u2026")}}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)}};var Lt=typeof globalThis=="object"&&"crypto"in globalThis?globalThis.crypto:void 0;function Rt(e){return e instanceof Uint8Array||ArrayBuffer.isView(e)&&e.constructor.name==="Uint8Array"}function se(e){if(!Number.isSafeInteger(e)||e<0)throw new Error("positive integer expected, got "+e)}function ht(e,...t){if(!Rt(e))throw new Error("Uint8Array expected");if(t.length>0&&!t.includes(e.length))throw new Error("Uint8Array expected of length "+t+", got length="+e.length)}function Ce(e){if(typeof e!="function"||typeof e.create!="function")throw new Error("Hash should be wrapped by utils.createHasher");se(e.outputLen),se(e.blockLen)}function Vt(e,t=!0){if(e.destroyed)throw new Error("Hash instance has been destroyed");if(t&&e.finished)throw new Error("Hash#digest() has already been called")}function Kn(e,t){ht(e);let r=t.outputLen;if(e.length>>t}var Nn=typeof Uint8Array.from([]).toHex=="function"&&typeof Uint8Array.fromHex=="function",Xs=Array.from({length:256},(e,t)=>t.toString(16).padStart(2,"0"));function xt(e){if(ht(e),Nn)return e.toHex();let t="";for(let r=0;r=gt._0&&e<=gt._9)return e-gt._0;if(e>=gt.A&&e<=gt.F)return e-(gt.A-10);if(e>=gt.a&&e<=gt.f)return e-(gt.a-10)}function Kt(e){if(typeof e!="string")throw new Error("hex string expected, got "+typeof e);if(Nn)return Uint8Array.fromHex(e);let t=e.length,r=t/2;if(t%2)throw new Error("hex string expected, got unpadded hex of length "+t);let n=new Uint8Array(r);for(let o=0,s=0;oe().update(ie(n)).digest(),r=e();return t.outputLen=r.outputLen,t.blockLen=r.blockLen,t.create=()=>e(),t}function Nt(e=32){if(Lt&&typeof Lt.getRandomValues=="function")return Lt.getRandomValues(new Uint8Array(e));if(Lt&&typeof Lt.randomBytes=="function")return Uint8Array.from(Lt.randomBytes(e));throw new Error("crypto.getRandomValues must be defined")}function Fs(e,t,r,n){if(typeof e.setBigUint64=="function")return e.setBigUint64(t,r,n);let o=BigInt(32),s=BigInt(4294967295),i=Number(r>>o&s),c=Number(r&s),a=n?4:0,f=n?0:4;e.setUint32(t+a,i,n),e.setUint32(t+f,c,n)}function Dn(e,t,r){return e&t^~e&r}function qn(e,t,r){return e&t^e&r^t&r}var $e=class extends Pt{constructor(t,r,n,o){super(),this.finished=!1,this.length=0,this.pos=0,this.destroyed=!1,this.blockLen=t,this.outputLen=r,this.padOffset=n,this.isLE=o,this.buffer=new Uint8Array(t),this.view=Le(this.buffer)}update(t){Vt(this),t=ie(t),ht(t);let{view:r,buffer:n,blockLen:o}=this,s=t.length;for(let i=0;io-i&&(this.process(n,0),i=0);for(let u=i;ud.length)throw new Error("_sha2: outputLen bigger than state");for(let u=0;u>>3,p=ut(h,17)^ut(h,19)^h>>>10;Tt[u]=p+Tt[u-7]+y+Tt[u-16]|0}let{A:n,B:o,C:s,D:i,E:c,F:a,G:f,H:d}=this;for(let u=0;u<64;u++){let l=ut(c,6)^ut(c,11)^ut(c,25),h=d+l+Dn(c,a,f)+Ys[u]+Tt[u]|0,p=(ut(n,2)^ut(n,13)^ut(n,22))+qn(n,o,s)|0;d=f,f=a,a=c,c=i+h|0,i=s,s=o,o=n,n=h+p|0}n=n+this.A|0,o=o+this.B|0,s=s+this.C|0,i=i+this.D|0,c=c+this.E|0,a=a+this.F|0,f=f+this.G|0,d=d+this.H|0,this.set(n,o,s,i,c,a,f,d)}roundClean(){$t(Tt)}destroy(){this.set(0,0,0,0,0,0,0,0),$t(this.buffer)}};var Gt=Mn(()=>new Ke);var Ne=class extends Pt{constructor(t,r){super(),this.finished=!1,this.destroyed=!1,Ce(t);let n=ie(r);if(this.iHash=t.create(),typeof this.iHash.update!="function")throw new Error("Expected instance of class which extends utils.Hash");this.blockLen=this.iHash.blockLen,this.outputLen=this.iHash.outputLen;let o=this.blockLen,s=new Uint8Array(o);s.set(n.length>o?t.create().update(n).digest():n);for(let i=0;inew Ne(e,t).update(r).digest();hr.create=(e,t)=>new Ne(e,t);var pr=BigInt(0),dr=BigInt(1);function ce(e,t=""){if(typeof e!="boolean"){let r=t&&`"${t}"`;throw new Error(r+"expected boolean, got type="+typeof e)}return e}function It(e,t,r=""){let n=Rt(e),o=e?.length,s=t!==void 0;if(!n||s&&o!==t){let i=r&&`"${r}" `,c=s?` of length ${t}`:"",a=n?`length=${o}`:`type=${typeof e}`;throw new Error(i+"expected Uint8Array"+c+", got "+a)}return e}function ae(e){let t=e.toString(16);return t.length&1?"0"+t:t}function jn(e){if(typeof e!="string")throw new Error("hex string expected, got "+typeof e);return e===""?pr:BigInt("0x"+e)}function Et(e){return jn(xt(e))}function yr(e){return ht(e),jn(xt(Uint8Array.from(e).reverse()))}function zt(e,t){return Kt(e.toString(16).padStart(t*2,"0"))}function mr(e,t){return zt(e,t).reverse()}function G(e,t,r){let n;if(typeof t=="string")try{n=Kt(t)}catch(s){throw new Error(e+" must be hex string or Uint8Array, cause: "+s)}else if(Rt(t))n=Uint8Array.from(t);else throw new Error(e+" must be hex string or Uint8Array");let o=n.length;if(typeof r=="number"&&o!==r)throw new Error(e+" of length "+r+" expected, got "+o);return n}var ur=e=>typeof e=="bigint"&&pr<=e;function fe(e,t,r){return ur(e)&&ur(t)&&ur(r)&&t<=e&&epr;e>>=dr,t+=1);return t}var Ot=e=>(dr<new Uint8Array(h),o=h=>Uint8Array.of(h),s=n(e),i=n(e),c=0,a=()=>{s.fill(1),i.fill(0),c=0},f=(...h)=>r(i,s,...h),d=(h=n(0))=>{i=f(o(0),h),s=f(),h.length!==0&&(i=f(o(1),h),s=f())},u=()=>{if(c++>=1e3)throw new Error("drbg: tried 1000 values");let h=0,y=[];for(;h{a(),d(h);let p;for(;!(p=y(u()));)d();return a(),p}}function le(e,t,r={}){if(!e||typeof e!="object")throw new Error("expected valid options object");function n(o,s,i){let c=e[o];if(i&&c===void 0)return;let a=typeof c;if(a!==s||c===null)throw new Error(`param "${o}" is invalid: expected ${s}, got ${a}`)}Object.entries(t).forEach(([o,s])=>n(o,s,!1)),Object.entries(r).forEach(([o,s])=>n(o,s,!0))}function wr(e){let t=new WeakMap;return(r,...n)=>{let o=t.get(r);if(o!==void 0)return o;let s=e(r,...n);return t.set(r,s),s}}var et=BigInt(0),Q=BigInt(1),Mt=BigInt(2),Zn=BigInt(3),Xn=BigInt(4),Fn=BigInt(5),Ws=BigInt(7),Yn=BigInt(8),Js=BigInt(9),Wn=BigInt(16);function ct(e,t){let r=e%t;return r>=et?r:t+r}function at(e,t,r){let n=e;for(;t-- >et;)n*=n,n%=r;return n}function Gn(e,t){if(e===et)throw new Error("invert: expected non-zero number");if(t<=et)throw new Error("invert: expected positive modulus, got "+t);let r=ct(e,t),n=t,o=et,s=Q,i=Q,c=et;for(;r!==et;){let f=n/r,d=n%r,u=o-i*f,l=s-c*f;n=r,r=d,o=i,s=c,i=u,c=l}if(n!==Q)throw new Error("invert: does not exist");return ct(o,t)}function br(e,t,r){if(!e.eql(e.sqr(t),r))throw new Error("Cannot find square root")}function Jn(e,t){let r=(e.ORDER+Q)/Xn,n=e.pow(t,r);return br(e,n,t),n}function Qs(e,t){let r=(e.ORDER-Fn)/Yn,n=e.mul(t,Mt),o=e.pow(n,r),s=e.mul(t,o),i=e.mul(e.mul(s,Mt),o),c=e.mul(s,e.sub(i,e.ONE));return br(e,c,t),c}function ti(e){let t=_t(e),r=Qn(e),n=r(t,t.neg(t.ONE)),o=r(t,n),s=r(t,t.neg(n)),i=(e+Ws)/Wn;return(c,a)=>{let f=c.pow(a,i),d=c.mul(f,n),u=c.mul(f,o),l=c.mul(f,s),h=c.eql(c.sqr(d),a),y=c.eql(c.sqr(u),a);f=c.cmov(f,d,h),d=c.cmov(l,u,y);let p=c.eql(c.sqr(d),a),k=c.cmov(f,d,p);return br(c,k,a),k}}function Qn(e){if(e1e3)throw new Error("Cannot find square root: probably non-prime P");if(r===1)return Jn;let s=o.pow(n,t),i=(t+Q)/Mt;return function(a,f){if(a.is0(f))return f;if(zn(a,f)!==1)throw new Error("Cannot find square root");let d=r,u=a.mul(a.ONE,s),l=a.pow(f,t),h=a.pow(f,i);for(;!a.eql(l,a.ONE);){if(a.is0(l))return a.ZERO;let y=1,p=a.sqr(l);for(;!a.eql(p,a.ONE);)if(y++,p=a.sqr(p),y===d)throw new Error("Cannot find square root");let k=Q<(n[o]="function",n),t);return le(e,r),e}function ni(e,t,r){if(ret;)r&Q&&(n=e.mul(n,o)),o=e.sqr(o),r>>=Q;return n}function De(e,t,r=!1){let n=new Array(t.length).fill(r?e.ZERO:void 0),o=t.reduce((i,c,a)=>e.is0(c)?i:(n[a]=i,e.mul(i,c)),e.ONE),s=e.inv(o);return t.reduceRight((i,c,a)=>e.is0(c)?i:(n[a]=e.mul(i,n[a]),e.mul(i,c)),s),n}function zn(e,t){let r=(e.ORDER-Q)/Mt,n=e.pow(t,r),o=e.eql(n,e.ONE),s=e.eql(n,e.ZERO),i=e.eql(n,e.neg(e.ONE));if(!o&&!s&&!i)throw new Error("invalid Legendre symbol result");return o?1:s?0:-1}function qe(e,t){t!==void 0&&se(t);let r=t!==void 0?t:e.toString(2).length,n=Math.ceil(r/8);return{nBitLength:r,nByteLength:n}}function _t(e,t,r=!1,n={}){if(e<=et)throw new Error("invalid field: expected ORDER > 0, got "+e);let o,s,i=!1,c;if(typeof t=="object"&&t!=null){if(n.sqrt||r)throw new Error("cannot specify opts in two arguments");let l=t;l.BITS&&(o=l.BITS),l.sqrt&&(s=l.sqrt),typeof l.isLE=="boolean"&&(r=l.isLE),typeof l.modFromBytes=="boolean"&&(i=l.modFromBytes),c=l.allowedLengths}else typeof t=="number"&&(o=t),n.sqrt&&(s=n.sqrt);let{nBitLength:a,nByteLength:f}=qe(e,o);if(f>2048)throw new Error("invalid field: expected ORDER of <= 2048 bytes");let d,u=Object.freeze({ORDER:e,isLE:r,BITS:a,BYTES:f,MASK:Ot(a),ZERO:et,ONE:Q,allowedLengths:c,create:l=>ct(l,e),isValid:l=>{if(typeof l!="bigint")throw new Error("invalid field element: expected bigint, got "+typeof l);return et<=l&&ll===et,isValidNot0:l=>!u.is0(l)&&u.isValid(l),isOdd:l=>(l&Q)===Q,neg:l=>ct(-l,e),eql:(l,h)=>l===h,sqr:l=>ct(l*l,e),add:(l,h)=>ct(l+h,e),sub:(l,h)=>ct(l-h,e),mul:(l,h)=>ct(l*h,e),pow:(l,h)=>ni(u,l,h),div:(l,h)=>ct(l*Gn(h,e),e),sqrN:l=>l*l,addN:(l,h)=>l+h,subN:(l,h)=>l-h,mulN:(l,h)=>l*h,inv:l=>Gn(l,e),sqrt:s||(l=>(d||(d=ei(e)),d(u,l))),toBytes:l=>r?mr(l,f):zt(l,f),fromBytes:(l,h=!0)=>{if(c){if(!c.includes(l.length)||l.length>f)throw new Error("Field.fromBytes: expected "+c+" bytes, got "+l.length);let p=new Uint8Array(f);p.set(l,r?0:p.length-l.length),l=p}if(l.length!==f)throw new Error("Field.fromBytes: expected "+f+" bytes, got "+l.length);let y=r?yr(l):Et(l);if(i&&(y=ct(y,e)),!h&&!u.isValid(y))throw new Error("invalid field element: outside of range 0..ORDER");return y},invertBatch:l=>De(u,l),cmov:(l,h,y)=>y?h:l});return Object.freeze(u)}function to(e){if(typeof e!="bigint")throw new Error("field order must be bigint");let t=e.toString(2).length;return Math.ceil(t/8)}function xr(e){let t=to(e);return t+Math.ceil(t/2)}function je(e,t,r=!1){let n=e.length,o=to(t),s=xr(t);if(n<16||n1024)throw new Error("expected "+s+"-1024 bytes of input, got "+n);let i=r?yr(e):Et(e),c=ct(i,t-Q)+Q;return r?mr(c,o):zt(c,o)}var Zt=BigInt(0),Dt=BigInt(1);function he(e,t){let r=t.negate();return e?r:t}function Ve(e,t){let r=De(e.Fp,t.map(n=>n.Z));return t.map((n,o)=>e.fromAffine(n.toAffine(r[o])))}function oo(e,t){if(!Number.isSafeInteger(e)||e<=0||e>t)throw new Error("invalid window size, expected [1.."+t+"], got W="+e)}function vr(e,t){oo(e,t);let r=Math.ceil(t/e)+1,n=2**(e-1),o=2**e,s=Ot(e),i=BigInt(e);return{windows:r,windowSize:n,mask:s,maxNumber:o,shiftBy:i}}function eo(e,t,r){let{windowSize:n,mask:o,maxNumber:s,shiftBy:i}=r,c=Number(e&o),a=e>>i;c>n&&(c-=s,a+=Dt);let f=t*n,d=f+Math.abs(c)-1,u=c===0,l=c<0,h=t%2!==0;return{nextN:a,offset:d,isZero:u,isNeg:l,isNegF:h,offsetF:f}}function oi(e,t){if(!Array.isArray(e))throw new Error("array expected");e.forEach((r,n)=>{if(!(r instanceof t))throw new Error("invalid point at index "+n)})}function si(e,t){if(!Array.isArray(e))throw new Error("array of scalars expected");e.forEach((r,n)=>{if(!t.isValid(r))throw new Error("invalid scalar at index "+n)})}var Er=new WeakMap,so=new WeakMap;function kr(e){return so.get(e)||1}function ro(e){if(e!==Zt)throw new Error("invalid wNAF")}var Pe=class{constructor(t,r){this.BASE=t.BASE,this.ZERO=t.ZERO,this.Fn=t.Fn,this.bits=r}_unsafeLadder(t,r,n=this.ZERO){let o=t;for(;r>Zt;)r&Dt&&(n=n.add(o)),o=o.double(),r>>=Dt;return n}precomputeWindow(t,r){let{windows:n,windowSize:o}=vr(r,this.bits),s=[],i=t,c=i;for(let a=0;aZt||n>Zt;)r&Dt&&(s=s.add(o)),n&Dt&&(i=i.add(o)),o=o.double(),r>>=Dt,n>>=Dt;return{p1:s,p2:i}}function co(e,t,r,n){oi(r,e),si(n,t);let o=r.length,s=n.length;if(o!==s)throw new Error("arrays of points and scalars must have equal length");let i=e.ZERO,c=Me(BigInt(o)),a=1;c>12?a=c-3:c>4?a=c-2:c>0&&(a=2);let f=Ot(a),d=new Array(Number(f)+1).fill(i),u=Math.floor((t.BITS-1)/a)*a,l=i;for(let h=u;h>=0;h-=a){d.fill(i);for(let p=0;p>BigInt(h)&f);d[A]=d[A].add(r[p])}let y=i;for(let p=d.length-1,k=i;p>0;p--)k=k.add(d[p]),y=y.add(k);if(l=l.add(y),h!==0)for(let p=0;pZt))throw new Error(`CURVE.${a} must be positive bigint`)}let o=no(t.p,r.Fp,n),s=no(t.n,r.Fn,n),c=["Gx","Gy","a",e==="weierstrass"?"b":"d"];for(let a of c)if(!o.isValid(t[a]))throw new Error(`CURVE.${a} must be valid field element of CURVE.Fp`);return t=Object.freeze(Object.assign({},t)),{CURVE:t,Fp:o,Fn:s}}var fo=(e,t)=>(e+(e>=0?t:-t)/lo)/t;function ii(e,t,r){let[[n,o],[s,i]]=t,c=fo(i*e,r),a=fo(-o*e,r),f=e-c*n-a*s,d=-c*o-a*i,u=f=h||d=h)throw new Error("splitScalar (endomorphism): failed, k="+e);return{k1neg:u,k1:f,k2neg:l,k2:d}}function Sr(e){if(!["compact","recovered","der"].includes(e))throw new Error('Signature format must be "compact", "recovered", or "der"');return e}function Ar(e,t){let r={};for(let n of Object.keys(t))r[n]=e[n]===void 0?t[n]:e[n];return ce(r.lowS,"lowS"),ce(r.prehash,"prehash"),r.format!==void 0&&Sr(r.format),r}var Br=class extends Error{constructor(t=""){super(t)}},kt={Err:Br,_tlv:{encode:(e,t)=>{let{Err:r}=kt;if(e<0||e>256)throw new r("tlv.encode: wrong tag");if(t.length&1)throw new r("tlv.encode: unpadded data");let n=t.length/2,o=ae(n);if(o.length/2&128)throw new r("tlv.encode: long form length too big");let s=n>127?ae(o.length/2|128):"";return ae(e)+s+o+t},decode(e,t){let{Err:r}=kt,n=0;if(e<0||e>256)throw new r("tlv.encode: wrong tag");if(t.length<2||t[n++]!==e)throw new r("tlv.decode: wrong tlv");let o=t[n++],s=!!(o&128),i=0;if(!s)i=o;else{let a=o&127;if(!a)throw new r("tlv.decode(long): indefinite length not supported");if(a>4)throw new r("tlv.decode(long): byte length is too big");let f=t.subarray(n,n+a);if(f.length!==a)throw new r("tlv.decode: length bytes not complete");if(f[0]===0)throw new r("tlv.decode(long): zero leftmost byte");for(let d of f)i=i<<8|d;if(n+=a,i<128)throw new r("tlv.decode(long): not minimal encoding")}let c=t.subarray(n,n+i);if(c.length!==i)throw new r("tlv.decode: wrong value length");return{v:c,l:t.subarray(n+i)}}},_int:{encode(e){let{Err:t}=kt;if(e{let{X:w,Y:m,Z:E}=C;if(n.eql(E,n.ONE))return{x:w,y:m};let B=C.is0();b==null&&(b=B?n.ONE:n.inv(E));let _=n.mul(w,b),O=n.mul(m,b),T=n.mul(E,b);if(B)return{x:n.ZERO,y:n.ZERO};if(!n.eql(T,n.ONE))throw new Error("invZ was invalid");return{x:_,y:O}}),I=wr(C=>{if(C.is0()){if(t.allowInfinityPoint&&!n.is0(C.Y))return;throw new Error("bad point: ZERO")}let{x:b,y:w}=C.toAffine();if(!n.isValid(b)||!n.isValid(w))throw new Error("bad point: x or y not field elements");if(!k(b,w))throw new Error("bad point: equation left != right");if(!C.isTorsionFree())throw new Error("bad point: not in prime-order subgroup");return!0});function H(C,b,w,m,E){return w=new x(n.mul(w.X,C),w.Y,w.Z),b=he(m,b),w=he(E,w),b.add(w)}class x{constructor(b,w,m){this.X=g("x",b),this.Y=g("y",w,!0),this.Z=g("z",m),Object.freeze(this)}static CURVE(){return s}static fromAffine(b){let{x:w,y:m}=b||{};if(!b||!n.isValid(w)||!n.isValid(m))throw new Error("invalid affine point");if(b instanceof x)throw new Error("projective point not allowed");return n.is0(w)&&n.is0(m)?x.ZERO:new x(w,m,n.ONE)}static fromBytes(b){let w=x.fromAffine(y(It(b,void 0,"point")));return w.assertValidity(),w}static fromHex(b){return x.fromBytes(G("pointHex",b))}get x(){return this.toAffine().x}get y(){return this.toAffine().y}precompute(b=8,w=!0){return U.createCache(this,b),w||this.multiply(Ge),this}assertValidity(){I(this)}hasEvenY(){let{y:b}=this.toAffine();if(!n.isOdd)throw new Error("Field doesn't support isOdd");return!n.isOdd(b)}equals(b){v(b);let{X:w,Y:m,Z:E}=this,{X:B,Y:_,Z:O}=b,T=n.eql(n.mul(w,O),n.mul(B,E)),L=n.eql(n.mul(m,O),n.mul(_,E));return T&&L}negate(){return new x(this.X,n.neg(this.Y),this.Z)}double(){let{a:b,b:w}=s,m=n.mul(w,Ge),{X:E,Y:B,Z:_}=this,O=n.ZERO,T=n.ZERO,L=n.ZERO,N=n.mul(E,E),Z=n.mul(B,B),j=n.mul(_,_),D=n.mul(E,B);return D=n.add(D,D),L=n.mul(E,_),L=n.add(L,L),O=n.mul(b,L),T=n.mul(m,j),T=n.add(O,T),O=n.sub(Z,T),T=n.add(Z,T),T=n.mul(O,T),O=n.mul(D,O),L=n.mul(m,L),j=n.mul(b,j),D=n.sub(N,j),D=n.mul(b,D),D=n.add(D,L),L=n.add(N,N),N=n.add(L,N),N=n.add(N,j),N=n.mul(N,D),T=n.add(T,N),j=n.mul(B,_),j=n.add(j,j),N=n.mul(j,D),O=n.sub(O,N),L=n.mul(j,Z),L=n.add(L,L),L=n.add(L,L),new x(O,T,L)}add(b){v(b);let{X:w,Y:m,Z:E}=this,{X:B,Y:_,Z:O}=b,T=n.ZERO,L=n.ZERO,N=n.ZERO,Z=s.a,j=n.mul(s.b,Ge),D=n.mul(w,B),P=n.mul(m,_),X=n.mul(E,O),rt=n.add(w,m),V=n.add(B,_);rt=n.mul(rt,V),V=n.add(D,P),rt=n.sub(rt,V),V=n.add(w,E);let F=n.add(B,O);return V=n.mul(V,F),F=n.add(D,X),V=n.sub(V,F),F=n.add(m,E),T=n.add(_,O),F=n.mul(F,T),T=n.add(P,X),F=n.sub(F,T),N=n.mul(Z,V),T=n.mul(j,X),N=n.add(T,N),T=n.sub(P,N),N=n.add(P,N),L=n.mul(T,N),P=n.add(D,D),P=n.add(P,D),X=n.mul(Z,X),V=n.mul(j,V),P=n.add(P,X),X=n.sub(D,X),X=n.mul(Z,X),V=n.add(V,X),D=n.mul(P,V),L=n.add(L,D),D=n.mul(F,V),T=n.mul(rt,T),T=n.sub(T,D),D=n.mul(rt,P),N=n.mul(F,N),N=n.add(N,D),new x(T,L,N)}subtract(b){return this.add(b.negate())}is0(){return this.equals(x.ZERO)}multiply(b){let{endo:w}=t;if(!o.isValidNot0(b))throw new Error("invalid scalar: out of range");let m,E,B=_=>U.cached(this,_,O=>Ve(x,O));if(w){let{k1neg:_,k1:O,k2neg:T,k2:L}=S(b),{p:N,f:Z}=B(O),{p:j,f:D}=B(L);E=Z.add(D),m=H(w.beta,N,j,_,T)}else{let{p:_,f:O}=B(b);m=_,E=O}return Ve(x,[m,E])[0]}multiplyUnsafe(b){let{endo:w}=t,m=this;if(!o.isValid(b))throw new Error("invalid scalar: out of range");if(b===At||m.is0())return x.ZERO;if(b===Xt)return m;if(U.hasCache(this))return this.multiply(b);if(w){let{k1neg:E,k1:B,k2neg:_,k2:O}=S(b),{p1:T,p2:L}=io(x,m,B,O);return H(w.beta,T,L,E,_)}else return U.unsafe(m,b)}multiplyAndAddUnsafe(b,w,m){let E=this.multiplyUnsafe(w).add(b.multiplyUnsafe(m));return E.is0()?void 0:E}toAffine(b){return R(this,b)}isTorsionFree(){let{isTorsionFree:b}=t;return i===Xt?!0:b?b(x,this):U.unsafe(this,c).is0()}clearCofactor(){let{clearCofactor:b}=t;return i===Xt?this:b?b(x,this):this.multiplyUnsafe(i)}isSmallOrder(){return this.multiplyUnsafe(i).is0()}toBytes(b=!0){return ce(b,"isCompressed"),this.assertValidity(),h(x,this,b)}toHex(b=!0){return xt(this.toBytes(b))}toString(){return``}get px(){return this.X}get py(){return this.X}get pz(){return this.Z}toRawBytes(b=!0){return this.toBytes(b)}_setWindowSize(b){this.precompute(b)}static normalizeZ(b){return Ve(x,b)}static msm(b,w){return co(x,o,b,w)}static fromPrivateKey(b){return x.BASE.multiply(Ht(o,b))}}x.BASE=new x(s.Gx,s.Gy,n.ONE),x.ZERO=new x(n.ZERO,n.ONE,n.ZERO),x.Fp=n,x.Fn=o;let M=o.BITS,U=new Pe(x,t.endo?Math.ceil(M/2):M);return x.BASE.precompute(8),x}function ho(e){return Uint8Array.of(e?2:3)}function uo(e,t){return{secretKey:t.BYTES,publicKey:1+e.BYTES,publicKeyUncompressed:1+2*e.BYTES,publicKeyHasPrefix:!0,signature:2*t.BYTES}}function fi(e,t={}){let{Fn:r}=e,n=t.randomBytes||Nt,o=Object.assign(uo(e.Fp,r),{seed:xr(r.ORDER)});function s(h){try{return!!Ht(r,h)}catch{return!1}}function i(h,y){let{publicKey:p,publicKeyUncompressed:k}=o;try{let A=h.length;return y===!0&&A!==p||y===!1&&A!==k?!1:!!e.fromBytes(h)}catch{return!1}}function c(h=n(o.seed)){return je(It(h,o.seed,"seed"),r.ORDER)}function a(h,y=!0){return e.BASE.multiply(Ht(r,h)).toBytes(y)}function f(h){let y=c(h);return{secretKey:y,publicKey:a(y)}}function d(h){if(typeof h=="bigint")return!1;if(h instanceof e)return!0;let{secretKey:y,publicKey:p,publicKeyUncompressed:k}=o;if(r.allowedLengths||y===p)return;let A=G("key",h).length;return A===p||A===k}function u(h,y,p=!0){if(d(h)===!0)throw new Error("first arg must be private key");if(d(y)===!1)throw new Error("second arg must be public key");let k=Ht(r,h);return e.fromHex(y).multiply(k).toBytes(p)}return Object.freeze({getPublicKey:a,getSharedSecret:u,keygen:f,Point:e,utils:{isValidSecretKey:s,isValidPublicKey:i,randomSecretKey:c,isValidPrivateKey:s,randomPrivateKey:c,normPrivateKeyToScalar:h=>Ht(r,h),precompute(h=8,y=e.BASE){return y.precompute(h,!1)}},lengths:o})}function li(e,t,r={}){Ce(t),le(r,{},{hmac:"function",lowS:"boolean",randomBytes:"function",bits2int:"function",bits2int_modN:"function"});let n=r.randomBytes||Nt,o=r.hmac||((w,...m)=>hr(t,w,ot(...m))),{Fp:s,Fn:i}=e,{ORDER:c,BITS:a}=i,{keygen:f,getPublicKey:d,getSharedSecret:u,utils:l,lengths:h}=fi(e,r),y={prehash:!1,lowS:typeof r.lowS=="boolean"?r.lowS:!1,format:void 0,extraEntropy:!1},p="compact";function k(w){let m=c>>Xt;return w>m}function A(w,m){if(!i.isValidNot0(m))throw new Error(`invalid signature ${w}: out of range 1..Point.Fn.ORDER`);return m}function $(w,m){Sr(m);let E=h.signature,B=m==="compact"?E:m==="recovered"?E+1:void 0;return It(w,B,`${m} signature`)}class g{constructor(m,E,B){this.r=A("r",m),this.s=A("s",E),B!=null&&(this.recovery=B),Object.freeze(this)}static fromBytes(m,E=p){$(m,E);let B;if(E==="der"){let{r:L,s:N}=kt.toSig(It(m));return new g(L,N)}E==="recovered"&&(B=m[0],E="compact",m=m.subarray(1));let _=i.BYTES,O=m.subarray(0,_),T=m.subarray(_,_*2);return new g(i.fromBytes(O),i.fromBytes(T),B)}static fromHex(m,E){return this.fromBytes(Kt(m),E)}addRecoveryBit(m){return new g(this.r,this.s,m)}recoverPublicKey(m){let E=s.ORDER,{r:B,s:_,recovery:O}=this;if(O==null||![0,1,2,3].includes(O))throw new Error("recovery id invalid");if(c*lo1)throw new Error("recovery id is ambiguous for h>1 curve");let L=O===2||O===3?B+c:B;if(!s.isValid(L))throw new Error("recovery id 2 or 3 invalid");let N=s.toBytes(L),Z=e.fromBytes(ot(ho((O&1)===0),N)),j=i.inv(L),D=S(G("msgHash",m)),P=i.create(-D*j),X=i.create(_*j),rt=e.BASE.multiplyUnsafe(P).add(Z.multiplyUnsafe(X));if(rt.is0())throw new Error("point at infinify");return rt.assertValidity(),rt}hasHighS(){return k(this.s)}toBytes(m=p){if(Sr(m),m==="der")return Kt(kt.hexFromSig(this));let E=i.toBytes(this.r),B=i.toBytes(this.s);if(m==="recovered"){if(this.recovery==null)throw new Error("recovery bit must be present");return ot(Uint8Array.of(this.recovery),E,B)}return ot(E,B)}toHex(m){return xt(this.toBytes(m))}assertValidity(){}static fromCompact(m){return g.fromBytes(G("sig",m),"compact")}static fromDER(m){return g.fromBytes(G("sig",m),"der")}normalizeS(){return this.hasHighS()?new g(this.r,i.neg(this.s),this.recovery):this}toDERRawBytes(){return this.toBytes("der")}toDERHex(){return xt(this.toBytes("der"))}toCompactRawBytes(){return this.toBytes("compact")}toCompactHex(){return xt(this.toBytes("compact"))}}let v=r.bits2int||function(m){if(m.length>8192)throw new Error("input is too large");let E=Et(m),B=m.length*8-a;return B>0?E>>BigInt(B):E},S=r.bits2int_modN||function(m){return i.create(v(m))},R=Ot(a);function I(w){return Pn("num < 2^"+a,w,At,R),i.toBytes(w)}function H(w,m){return It(w,void 0,"message"),m?It(t(w),void 0,"prehashed message"):w}function x(w,m,E){if(["recovered","canonical"].some(P=>P in E))throw new Error("sign() legacy options not supported");let{lowS:B,prehash:_,extraEntropy:O}=Ar(E,y);w=H(w,_);let T=S(w),L=Ht(i,m),N=[I(L),I(T)];if(O!=null&&O!==!1){let P=O===!0?n(h.secretKey):O;N.push(G("extraEntropy",P))}let Z=ot(...N),j=T;function D(P){let X=v(P);if(!i.isValidNot0(X))return;let rt=i.inv(X),V=e.BASE.multiply(X).toAffine(),F=i.create(V.x);if(F===At)return;let ye=i.create(rt*i.create(j+F*L));if(ye===At)return;let Dr=(V.x===F?0:2)|Number(V.y&Xt),qr=ye;return B&&k(ye)&&(qr=i.neg(ye),Dr^=1),new g(F,qr,Dr)}return{seed:Z,k2sig:D}}function M(w,m,E={}){w=G("message",w);let{seed:B,k2sig:_}=x(w,m,E);return Vn(t.outputLen,i.BYTES,o)(B,_)}function U(w){let m,E=typeof w=="string"||Rt(w),B=!E&&w!==null&&typeof w=="object"&&typeof w.r=="bigint"&&typeof w.s=="bigint";if(!E&&!B)throw new Error("invalid signature, expected Uint8Array, hex string or Signature instance");if(B)m=new g(w.r,w.s);else if(E){try{m=g.fromBytes(G("sig",w),"der")}catch(_){if(!(_ instanceof kt.Err))throw _}if(!m)try{m=g.fromBytes(G("sig",w),"compact")}catch{return!1}}return m||!1}function C(w,m,E,B={}){let{lowS:_,prehash:O,format:T}=Ar(B,y);if(E=G("publicKey",E),m=H(G("message",m),O),"strict"in B)throw new Error("options.strict was renamed to lowS");let L=T===void 0?U(w):g.fromBytes(G("sig",w),T);if(L===!1)return!1;try{let N=e.fromBytes(E);if(_&&L.hasHighS())return!1;let{r:Z,s:j}=L,D=S(m),P=i.inv(j),X=i.create(D*P),rt=i.create(Z*P),V=e.BASE.multiplyUnsafe(X).add(N.multiplyUnsafe(rt));return V.is0()?!1:i.create(V.x)===Z}catch{return!1}}function b(w,m,E={}){let{prehash:B}=Ar(E,y);return m=H(m,B),g.fromBytes(w,"recovered").recoverPublicKey(m).toBytes()}return Object.freeze({keygen:f,getPublicKey:d,getSharedSecret:u,utils:l,lengths:h,Point:e,sign:M,verify:C,recoverPublicKey:b,Signature:g,hash:t})}function hi(e){let t={a:e.a,b:e.b,p:e.Fp.ORDER,n:e.n,h:e.h,Gx:e.Gx,Gy:e.Gy},r=e.Fp,n=e.allowedPrivateKeyLengths?Array.from(new Set(e.allowedPrivateKeyLengths.map(i=>Math.ceil(i/2)))):void 0,o=_t(t.n,{BITS:e.nBitLength,allowedLengths:n,modFromBytes:e.wrapPrivateKey}),s={Fp:r,Fn:o,allowInfinityPoint:e.allowInfinityPoint,endo:e.endo,isTorsionFree:e.isTorsionFree,clearCofactor:e.clearCofactor,fromBytes:e.fromBytes,toBytes:e.toBytes};return{CURVE:t,curveOpts:s}}function ui(e){let{CURVE:t,curveOpts:r}=hi(e),n={hmac:e.hmac,randomBytes:e.randomBytes,lowS:e.lowS,bits2int:e.bits2int,bits2int_modN:e.bits2int_modN};return{CURVE:t,curveOpts:r,hash:e.hash,ecdsaOpts:n}}function di(e,t){let r=t.Point;return Object.assign({},t,{ProjectivePoint:r,CURVE:Object.assign({},e,qe(r.Fn.ORDER,r.Fn.BITS))})}function po(e){let{CURVE:t,curveOpts:r,hash:n,ecdsaOpts:o}=ui(e),s=ai(t,r),i=li(s,n,o);return di(e,i)}function yo(e,t){let r=n=>po({...e,hash:n});return{...r(t),create:r}}var Ft={p:BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"),n:BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"),h:BigInt(1),a:BigInt(0),b:BigInt(7),Gx:BigInt("0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"),Gy:BigInt("0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8")},pi={beta:BigInt("0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee"),basises:[[BigInt("0x3086d221a7d46bcde86c90e49284eb15"),-BigInt("0xe4437ed6010e88286f547fa90abfe4c3")],[BigInt("0x114ca50f7a8e2f3f657c1108d9d44cfd8"),BigInt("0x3086d221a7d46bcde86c90e49284eb15")]]},yi=BigInt(0),mo=BigInt(1),Tr=BigInt(2);function mi(e){let t=Ft.p,r=BigInt(3),n=BigInt(6),o=BigInt(11),s=BigInt(22),i=BigInt(23),c=BigInt(44),a=BigInt(88),f=e*e*e%t,d=f*f*e%t,u=at(d,r,t)*d%t,l=at(u,r,t)*d%t,h=at(l,Tr,t)*f%t,y=at(h,o,t)*h%t,p=at(y,s,t)*y%t,k=at(p,c,t)*p%t,A=at(k,a,t)*k%t,$=at(A,c,t)*p%t,g=at($,r,t)*d%t,v=at(g,i,t)*y%t,S=at(v,n,t)*f%t,R=at(S,Tr,t);if(!ze.eql(ze.sqr(R),e))throw new Error("Cannot find square root");return R}var ze=_t(Ft.p,{sqrt:mi}),go=yo({...Ft,Fp:ze,lowS:!0,endo:pi},Gt),wo={};function Ze(e,...t){let r=wo[e];if(r===void 0){let n=Gt(Re(e));r=ot(n,n),wo[e]=r}return Gt(ot(r,...t))}var Or=e=>e.toBytes(!0).slice(1),Yt=go.Point,_r=e=>e%Tr===yi;function Ir(e){let{Fn:t,BASE:r}=Yt,n=Ht(t,e),o=r.multiply(n);return{scalar:_r(o.y)?n:t.neg(n),bytes:Or(o)}}function xo(e){let t=ze;if(!t.isValidNot0(e))throw new Error("invalid x: Fail if x \u2265 p");let r=t.create(e*e),n=t.create(r*e+BigInt(7)),o=t.sqrt(n);_r(o)||(o=t.neg(o));let s=Yt.fromAffine({x:e,y:o});return s.assertValidity(),s}var ue=Et;function vo(...e){return Yt.Fn.create(ue(Ze("BIP0340/challenge",...e)))}function bo(e){return Ir(e).bytes}function wi(e,t,r=Nt(32)){let{Fn:n}=Yt,o=G("message",e),{bytes:s,scalar:i}=Ir(t),c=G("auxRand",r,32),a=n.toBytes(i^ue(Ze("BIP0340/aux",c))),f=Ze("BIP0340/nonce",a,s,o),{bytes:d,scalar:u}=Ir(f),l=vo(d,s,o),h=new Uint8Array(64);if(h.set(d,0),h.set(n.toBytes(n.create(u+l*i)),32),!Eo(h,o,s))throw new Error("sign: Invalid signature produced");return h}function Eo(e,t,r){let{Fn:n,BASE:o}=Yt,s=G("signature",e,64),i=G("message",t),c=G("publicKey",r,32);try{let a=xo(ue(c)),f=ue(s.subarray(0,32));if(!fe(f,mo,Ft.p))return!1;let d=ue(s.subarray(32,64));if(!fe(d,mo,Ft.n))return!1;let u=vo(n.toBytes(f),Or(a),i),l=o.multiplyUnsafe(d).add(a.multiplyUnsafe(n.neg(u))),{x:h,y}=l.toAffine();return!(l.is0()||!_r(y)||h!==f)}catch{return!1}}var Xe=(()=>{let r=(o=Nt(48))=>je(o,Ft.n);go.utils.randomSecretKey;function n(o){let s=r(o);return{secretKey:s,publicKey:bo(s)}}return{keygen:n,getPublicKey:bo,sign:wi,verify:Eo,Point:Yt,utils:{randomSecretKey:r,randomPrivateKey:r,taggedHash:Ze,lift_x:xo,pointToBytes:Or,numberToBytesBE:zt,bytesToNumberBE:Et,mod:ct},lengths:{secretKey:32,publicKey:32,publicKeyHasPrefix:!1,signature:64,seed:48}}})();var ko=Gt;var bi=4e3,Ao=14e3,gi=3e3,So=16e3,Bo=6e5,To=25,Fe=20111,pe=new TextEncoder,Hr=new TextDecoder,xi=e=>{let t="";for(let r=0;rUint8Array.from(atob(e),t=>t.charCodeAt(0)),Io=e=>pe.encode(`private-talk/html-room/v3/mesh|${e}`),Oo=["key","msg","seen"];async function Ei(e,t){let r=tt(12),n=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:r,additionalData:Io(e.topic)},e.meshKey,pe.encode(JSON.stringify(t)))),o=new Uint8Array(12+n.length);return o.set(r),o.set(n,12),xi(o)}async function ki(e,t,r=Date.now()){if(typeof t!="string"||t.length<40||t.length>So||!/^[A-Za-z0-9+/]+={0,2}$/.test(t))return null;let n;try{let o=vi(t);n=JSON.parse(Hr.decode(await crypto.subtle.decrypt({name:"AES-GCM",iv:o.subarray(0,12),additionalData:Io(e.topic)},e.meshKey,o.subarray(12))))}catch{return null}return!n||typeof n!="object"||!/^[0-9a-f]{16}$/.test(n.d??"")||!Number.isInteger(n.n)||n.n<0||n.n>To||typeof n.t!="number"||Math.abs(r-n.t)>Bo||!["hb","f","bye"].includes(n.k)||n.c!==void 0&&(typeof n.c!="number"||!Number.isFinite(n.c))||n.k==="f"&&(!n.f||typeof n.f!="object"||!Oo.includes(n.f.type)||typeof n.f.iv!="string"||typeof n.f.ct!="string")?null:n}var de=class{constructor(t,r){Object.assign(this,{mesh:t,entry:r,kind:lt(r),url:Bt(r),open:!1,backoff:1e3})}protocols(){}target(){return this.url}connect(){if(this.mesh.stopped)return;let t;try{t=this.ws=this.mesh.createSocket(this.target(),this.protocols())}catch{this.fail();return}t.binaryType="arraybuffer",this.timeout=setTimeout(()=>this.fail(t),1e4),t.onopen=()=>{t===this.ws&&this.opened()},t.onmessage=r=>{t===this.ws&&!this.mesh.stopped&&this.data(r.data)},t.onclose=t.onerror=()=>this.fail(t)}ready(){clearTimeout(this.timeout),this.open=!0,this.backoff=1e3,this.mesh.linkChanged()}fail(t=this.ws){if(t!==this.ws||this.mesh.stopped)return;clearTimeout(this.timeout),clearInterval(this.keepalive),this.ws=null;let r=this.open;this.open=!1;try{t?.close()}catch{}r&&this.mesh.linkChanged(),clearTimeout(this.retry),this.retry=setTimeout(()=>this.connect(),this.backoff),this.backoff=Math.min(this.backoff*2,3e4)}stop(){clearTimeout(this.timeout),clearTimeout(this.retry),clearInterval(this.keepalive);let t=this.ws;this.ws=null,this.open=!1;try{t?.close(1e3)}catch{}}notice(t){this.noticed||(this.noticed=!0,this.mesh.onnotice(`${this.url}\uFF1A${String(t).slice(0,120)}`))}},Ur=class extends de{target(){return this.url+this.mesh.d.topic}opened(){this.ready()}data(t){typeof t=="string"&&this.mesh.receive(t)}send(t){this.ws.send(t)}},Cr=class extends de{opened(){this.sub="hr"+z(tt(6)),this.ws.send(JSON.stringify(["REQ",this.sub,{kinds:[Fe],"#t":[this.mesh.d.topic],since:Math.floor(Date.now()/1e3)-30}])),this.ready()}data(t){if(typeof t!="string"||t.length>So+2e3)return;let r;try{r=JSON.parse(t)}catch{return}Array.isArray(r)&&(r[0]==="EVENT"&&r[1]===this.sub&&r[2]?.kind===Fe&&typeof r[2].content=="string"?this.mesh.receive(r[2].content):r[0]==="OK"&&r[2]===!1?this.notice(`\u8FD9\u4E2A Nostr \u4E2D\u7EE7\u62D2\u7EDD\u4E86\u6D88\u606F\uFF08${r[3]??""}\uFF09\uFF0C\u53EF\u6362\u4E00\u4E2A`):r[0]==="AUTH"?this.notice("\u8FD9\u4E2A Nostr \u4E2D\u7EE7\u8981\u6C42\u767B\u5F55\uFF0C\u6682\u4E0D\u652F\u6301\uFF0C\u8BF7\u6362\u4E00\u4E2A"):r[0]==="CLOSED"&&r[1]===this.sub&&(this.notice(`\u8FD9\u4E2A Nostr \u4E2D\u7EE7\u505C\u6B62\u4E86\u63A5\u6536\uFF08${r[2]??""}\uFF09\uFF0C\u53EF\u6362\u4E00\u4E2A`),this.fail()))}send(t){this.ws.send(JSON.stringify(["EVENT",Ai(this.mesh.nostrKey,this.mesh.d.topic,t)]))}};function Ai(e,t,r,n=Date.now()){let o=z(Xe.getPublicKey(e)),s=Math.floor(n/1e3),i=[["t",t]],c=ko(pe.encode(JSON.stringify([0,o,s,Fe,i,r])));return{id:z(c),pubkey:o,created_at:s,kind:Fe,tags:i,content:r,sig:z(Xe.sign(c,e))}}var Si=e=>{let t=[];do{let r=e%128;e=Math.floor(e/128),e&&(r|=128),t.push(r)}while(e);return t},Ye=e=>{let t=pe.encode(e);return[t.length>>8,t.length&255,...t]},Rr=(e,t)=>Uint8Array.from([e,...Si(t.length),...t]),Bi=(e,t=60)=>Rr(16,[...Ye("MQTT"),4,2,t>>8,t&255,...Ye(e)]),Ti=(e,t=1)=>Rr(130,[t>>8,t&255,...Ye(e),0]),Ii=(e,t)=>Rr(48,[...Ye(e),...pe.encode(t)]);function Oi(e){let t=[],r=0;for(;r=e.length)return{packets:t,rest:e.subarray(r)};if(i=e[s++],n+=(i&127)*o,o*=128,o>128**4)throw Error("bad length")}while(i&128);if(s+n>e.length)return{packets:t,rest:e.subarray(r)};t.push({type:e[r]>>4,flags:e[r]&15,body:e.subarray(s,s+n)}),r=s+n}return{packets:t,rest:e.subarray(r)}}var Lr=class extends de{protocols(){return["mqtt"]}topic(){return`htmlroom/${this.mesh.d.topic}`}opened(){this.buf=new Uint8Array(0),this.ws.send(Bi("hr"+z(tt(8))))}data(t){if(!(t instanceof ArrayBuffer))return;let r=new Uint8Array(this.buf.length+t.byteLength);if(r.set(this.buf),r.set(new Uint8Array(t),this.buf.length),r.length>65536){this.fail();return}let n;try{n=Oi(r)}catch{this.fail();return}this.buf=n.rest.slice();for(let o of n.packets)if(o.type===2){if(o.body[1]!==0){this.notice(`\u8FD9\u4E2A MQTT \u670D\u52A1\u5668\u62D2\u7EDD\u4E86\u8FDE\u63A5\uFF08\u4EE3\u7801 ${o.body[1]}\uFF09\uFF0C\u53EF\u6362\u4E00\u4E2A`),this.fail();return}this.ws.send(Ti(this.topic()))}else if(o.type===9){if(o.body[2]===128){this.notice("\u8FD9\u4E2A MQTT \u670D\u52A1\u5668\u62D2\u7EDD\u4E86\u8BA2\u9605\uFF0C\u53EF\u6362\u4E00\u4E2A"),this.fail();return}this.ready(),this.keepalive=setInterval(()=>{try{this.ws?.send(Uint8Array.of(192,0))}catch{}},3e4)}else if(o.type===3){let s=o.body[0]<<8|o.body[1],i=2+s+(o.flags>>1&3?2:0);Hr.decode(o.body.subarray(2,2+s))===this.topic()&&this.mesh.receive(Hr.decode(o.body.subarray(i)))}}send(t){this.ws.send(Ii(this.topic(),t))}},_i={bc:Ur,nostr:Cr,mqtt:Lr},We=class{constructor(t,{derived:r,onhello:n,onmembers:o,onframe:s,onstatus:i=()=>{},onnotice:c=()=>{},onfatal:a=()=>{},createSocket:f=(l,h)=>h?new WebSocket(l,h):new WebSocket(l),now:d=()=>Date.now(),claimDelay:u=gi+Math.random()*1e3}){Object.assign(this,{d:r,onhello:n,onmembers:o,onframe:s,onstatus:i,onnotice:c,onfatal:a,createSocket:f,now:d,claimDelay:u}),this.node=z(tt(8)),this.me=0,this.peers=new Map,this.known=new Set,this.seenIvs=new Map,this.owners=new Map,this.lastHb=0,this.nostrKey=Xe.utils.randomPrivateKey(),this.links=t.map(h=>new _i[lt(h)](this,h));let l=this;this.socket={get readyState(){return l.me&&l.openCount()?1:0},send:h=>l.sendSession(h),close(){}}}start(){for(let t of this.links)t.connect();this.claimTimer=setTimeout(()=>this.claim(),this.claimDelay),this.timer=setInterval(()=>this.sweep(),1e3),this.linkChanged()}stop(){this.stopped||(this.me&&this.broadcast({k:"bye"}).catch(()=>{}),this.stopped=!0,clearTimeout(this.claimTimer),clearTimeout(this.answer),clearInterval(this.timer),setTimeout(()=>this.links.forEach(t=>t.stop()),50))}openCount(){return this.links.filter(t=>t.open).length}linkChanged(){let t=this.openCount();t&&!this.me&&!this.stopped&&this.broadcast({k:"hb"}).catch(()=>{}),this.onstatus(t?`\u4E2D\u7EE7\uFF1A\u5DF2\u8FDE\u63A5 ${t}/${this.links.length} \u4E2A\uFF08\u6D88\u606F\u540C\u65F6\u8D70\u6240\u6709\u4E2D\u7EE7\uFF09`:"\u4E2D\u7EE7\u5168\u90E8\u65AD\u5F00\u4E86\uFF0C\u6B63\u5728\u91CD\u8FDE\u2026\u2026")}async broadcast(t){let r=this.links.filter(s=>s.open);if(!r.length)return 0;let n=await Ei(this.d,{...t,d:this.node,n:this.me,t:this.now(),...this.me&&{c:this.claimedAt}});this.remember(n.slice(0,16));let o=0;for(let s of r)try{s.send(n),o++}catch{s.fail()}return o}remember(t){for(this.seenIvs.set(t,this.now());this.seenIvs.size>4096;)this.seenIvs.delete(this.seenIvs.keys().next().value)}sendSession(t){let r;try{r=JSON.parse(t)}catch{return}if(!Oo.includes(r?.type)||!this.me)return;let n=this.me;this.broadcast({k:"f",f:r}).then(o=>{o&&r.type==="msg"&&n===this.me&&this.onframe(r,n)}).catch(()=>{})}async receive(t){if(this.stopped||typeof t!="string")return;let r=t.slice(0,16);if(this.seenIvs.has(r))return;this.remember(r);let n=await ki(this.d,t,this.now());!n||n.d===this.node||this.stopped||(!this.known.has(n.d)&&n.k!=="bye"&&(this.known.add(n.d),this.answerSoon()),n.k==="bye"?this.peers.delete(n.d):n.n>0&&this.peers.set(n.d,{n:n.n,c:n.c??n.t,seen:this.now()}),this.reconcile(),n.k==="f"&&n.n>0&&this.owners.get(n.n)===n.d&&this.onframe({...n.f,at:this.now()},n.n))}answerSoon(){!this.me||this.answer||(this.answer=setTimeout(()=>{this.answer=null,this.lastHb=0,this.sweep()},50+Math.random()*250))}claim(){if(this.stopped)return;let t=new Set([...this.live()].map(([,n])=>n.n)),r=1;for(;t.has(r);)r++;if(r>To){this.stop(),this.onfatal("\u623F\u95F4\u5DF2\u6EE1\uFF0825 \u4EBA\uFF09\u3002");return}this.me=r,this.claimedAt=this.now(),this.lastHb=0,this.onhello(r),this.sweep()}*live(){let t=this.now();for(let r of this.peers)t-r[1].seen<=Ao&&(yield r)}reconcile(){let t=new Map,r=new Map,n=(i,c,a)=>{let f=r.get(i);(!f||athis.owners.has(i)&&this.owners.get(i)!==c).map(([i])=>i);t.size===this.owners.size&&[...t].every(([i,c])=>this.owners.get(i)===c)||(o.length&&this.onmembers([...this.owners.keys()].filter(i=>!o.includes(i)).sort((i,c)=>i-c)),this.owners=t,this.me&&this.onmembers([...t.keys()].sort((i,c)=>i-c)))}sweep(){if(this.stopped)return;let t=this.now();for(let[r,n]of this.peers)t-n.seen>Ao*4&&this.peers.delete(r);for(let[r,n]of this.seenIvs)if(t-n>Bo*2)this.seenIvs.delete(r);else break;this.reconcile(),this.me&&t-this.lastHb>=bi&&this.openCount()&&(this.lastHb=t,this.broadcast({k:"hb"}).catch(()=>{}))}};function _o(e,{allowCustom:t=!0}={}){let r=u=>document.getElementById(u),n=u=>{r("status").textContent=u};r("preview-help").hidden=!0;let o=0,s=!1,i=0,c,a=new Map;function f(u,l){if((u==="notice"||u==="fatal")&&n(l),u==="relay-change"&&(r("safety").textContent="\u6362\u4E86\u4E2D\u7EE7\uFF0C\u6B63\u5728\u91CD\u65B0\u786E\u8BA4\u6210\u5458\u3002\u8BF7\u518D\u6838\u5BF9\u4E00\u6B21\u5B89\u5168\u7801\u3002"),u==="presence"&&(r("presence").textContent=`\u5728\u7EBF ${l} \u4EBA`),u==="ready"&&(n("\u2705 \u5DF2\u52A0\u5BC6\u8FDE\u63A5\uFF0C\u53EF\u4EE5\u804A\u5929\u4E86"),r("safety").textContent=`\u5B89\u5168\u7801\uFF1A${l}\u3002\u8BF7\u6253\u7535\u8BDD\u6216\u5F53\u9762\u548C\u5BF9\u65B9\u6838\u5BF9\uFF0C\u4E00\u6837\u624D\u8BF4\u660E\u6CA1\u6709\u4EBA\u5192\u5145\u3002`),u==="message"){let h=document.createElement("li");h.textContent=`${l.from}\uFF1A${l.text}`;let y=document.createElement("small");h.append(y),r("messages").append(h),a.set(l.id,y),a.size>200&&(a.delete(a.keys().next().value),r("messages").firstChild.remove())}u==="delivery"&&a.has(l.id)&&(a.get(l.id).textContent=l.text)}if(!globalThis.crypto?.subtle||!globalThis.WebSocket||!globalThis.TextEncoder)n("\u8FD9\u4E2A\u9884\u89C8\u6253\u4E0D\u5F00\u804A\u5929\u5BA4\uFF0C\u8BF7\u7528 Safari\u3001Chrome \u7B49\u6D4F\u89C8\u5668\u6253\u5F00\u8FD9\u4E2A\u6587\u4EF6\u3002"),r("enter").disabled=!0;else if(e.version)try{it(e),n(e.encrypted?"\u8BF7\u8F93\u5165\u53E3\u4EE4\uFF0C\u7136\u540E\u70B9\u300C\u8FDB\u5165\u623F\u95F4\u300D\u3002":"\u70B9\u300C\u8FDB\u5165\u623F\u95F4\u300D\u5F00\u59CB\u804A\u5929\u3002"),r("password").disabled=!1,r("enter").disabled=!1}catch{n("\u623F\u95F4\u6587\u4EF6\u6709\u95EE\u9898\uFF0C\u8BF7\u8BA9\u5BF9\u65B9\u91CD\u65B0\u53D1\u4E00\u4EFD\u3002"),r("enter").disabled=!0}else n("\u8FD9\u662F\u7A7A\u767D\u6A21\u677F\uFF0C\u8BF7\u5148\u5728\u521B\u5EFA\u9875\u9762\u91CC\u521B\u5EFA\u623F\u95F4\u3002"),r("enter").disabled=!0;let d=t&&e.version&&e.custom===!0;d&&(r("custom-relay").hidden=!1,r("relay-input").value=e.relays.join(" ")),r("unlock").addEventListener("submit",async u=>{if(u.preventDefault(),s||Date.now()c.run(()=>(c.ws=y.socket,n("\u5DF2\u8FDB\u5165\u623F\u95F4\uFF0C\u6B63\u5728\u786E\u8BA4\u5176\u4ED6\u6210\u5458\u2026\u2026"),c.incoming({type:"hello",you:p,resume:""}))),onmembers:p=>c.run(()=>c.incoming({type:"members",members:p})),onframe:(p,k)=>c.run(()=>c.incoming({...p,from:k})),onstatus:p=>{r("relay-status").textContent=p},onnotice:n,onfatal:p=>{c.halt(),n(p||"\u804A\u5929\u5DF2\u7ED3\u675F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u623F\u95F4\u6587\u4EF6\u3002")}});c.connection=y,y.start(),n("\u6B63\u5728\u8FDE\u63A5\u2026\u2026")}else{let y=new Ue(l.relays,{roomId:l.roomId,protocols:p=>["pt2",`a${z(c.d.access)}`,...c.resume&&y.lastIndex===p?[`r${c.resume}`]:[]],onactive:(p,k,A)=>c.run(()=>{A&&c.resetRelay(),c.ws=p,r("relay-status").textContent=`\u4E2D\u7EE7\uFF1A${l.relays[k]}${l.relays.length>1?`\uFF08\u7B2C ${k+1} \u4E2A\uFF0C\u5171 ${l.relays.length} \u4E2A\uFF09`:""}`,n(A?"\u5DF2\u6362\u5230\u53E6\u4E00\u4E2A\u4E2D\u7EE7\uFF0C\u6B63\u5728\u91CD\u65B0\u786E\u8BA4\u6210\u5458\u2026\u2026":"\u5DF2\u8FDE\u4E0A\u4E2D\u7EE7\uFF0C\u6B63\u5728\u786E\u8BA4\u5176\u4ED6\u6210\u5458\u2026\u2026")}),onmessage:p=>{let k=c.ws;c.run(()=>{if(k===c.ws)return c.incoming(p)})},ondisconnect:()=>{c.ws=null,r("relay-status").textContent="\u4E2D\u7EE7\u65AD\u5F00\u4E86\uFF0C\u6B63\u5728\u91CD\u8FDE\u2026\u2026",f("presence",0)},onnotice:n,onfatal:()=>{c.halt(),n("\u8FDB\u4E0D\u53BB\u623F\u95F4\uFF1A\u53EF\u80FD\u4EBA\u6EE1\u4E86\u3001\u623F\u95F4\u6587\u4EF6\u4E0D\u5BF9\uFF0C\u6216\u4E2D\u7EE7\u62D2\u7EDD\u4E86\u8FDE\u63A5\u3002\u8BF7\u68C0\u67E5\u540E\u91CD\u65B0\u6253\u5F00\u3002")}});c.connection=y,y.start()}c.tick=setInterval(()=>c.run(async()=>{c.ws?.readyState===1&&(Date.now()-(c.lastAnnounce??0)>1e4&&(await c.announce(!0),c.lastAnnounce=Date.now()),await c.flush(),await c.retryPending())}),1600)}catch{o++,i=Date.now()+Math.min(3e4,1e3*2**Math.min(o,5)),r("password").value="",n("\u89E3\u9501\u5931\u8D25\uFF1A\u53E3\u4EE4\u4E0D\u5BF9\uFF0C\u6216\u6587\u4EF6\u4E0D\u5B8C\u6574\u3002\u8BF7\u7B49\u51E0\u79D2\u518D\u8BD5\u3002")}finally{s=!1,setTimeout(()=>{r("enter").disabled=!1},Math.max(0,i-Date.now()))}}),r("chat").addEventListener("submit",u=>{u.preventDefault(),c?.queue(r("text").value)?r("text").value="":n("\u53D1\u4E0D\u51FA\u53BB\uFF1A\u6D88\u606F\u662F\u7A7A\u7684\u3001\u592A\u957F\u4E86\uFF0C\u6216\u8005\u53D1\u5F97\u592A\u5FEB\u4E86\u3002")}),addEventListener("pagehide",()=>c?.halt())}function Hi(e){if(e.version===2)return e.relays[0].replace(/^ws/,"http");if(e.version===3){let t=e.relays.map(r=>lt(r)==="bc"?new URL(Bt(r)).origin:null);if(t.every(r=>r&&r===t[0]))return t[0].replace(/^ws/,"http")}return""}var Ho=e=>JSON.stringify(e).replace(/[<>/\u2028\u2029]/g,t=>`\\u${t.charCodeAt(0).toString(16).padStart(4,"0")}`),Ui=e=>btoa(String.fromCharCode(...new TextEncoder().encode(e))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");function Nr(e){return it(e),"#hr1="+Ui(JSON.stringify(e))}function Kr(e){let t=e.version?Hi(e):"",r=t?`${t}/hr.html${Nr(e)}`:"";return`

                                  \u9875\u9762\u6CA1\u6709\u542F\u52A8\uFF1F\u5FAE\u4FE1\u3001QQ \u6216\u624B\u673A\u300C\u6587\u4EF6\u300DApp \u7684\u9884\u89C8\u6253\u4E0D\u5F00\u804A\u5929\u5BA4\uFF0C\u8BF7\u7528 Safari\u3001Chrome \u7B49\u6D4F\u89C8\u5668\u6253\u5F00\u8FD9\u4E2A\u6587\u4EF6\u3002

                                  ${r?`

                                  \u7528\u6D4F\u89C8\u5668\u6253\u5F00\u8FD9\u4E2A\u623F\u95F4\uFF08\u70B9\u4E86\u6CA1\u53CD\u5E94\uFF0C\u5C31\u590D\u5236\u8FD9\u4E2A\u94FE\u63A5\uFF0C\u7C98\u8D34\u5230\u6D4F\u89C8\u5668\u5730\u5740\u680F\uFF09\u3002

                                  \u6CE8\u610F\uFF1A\u8FD9\u4E2A\u94FE\u63A5\u548C\u6587\u4EF6\u4E00\u6837\u662F\u623F\u95F4\u94A5\u5319\uFF0C\u522B\u968F\u4FBF\u8F6C\u53D1\u3002

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

                                  加密聊天房间

                                  正在启动……如果一直停在这里,请看下面的说明。

                                  ${previewHelp(config)}
                                  ${RETARGET}

                                    HTML Room 开源项目:${repoLink}

                                    \n`; + const html=`加密聊天房间

                                    加密聊天房间

                                    正在启动……如果一直停在这里,请看下面的说明。

                                    ${previewHelp(config)}
                                    ${RETARGET}

                                      ${MAKER}

                                      HTML Room 开源项目:${repoLink}

                                      \n`; return { html, csp }; } @@ -45,3 +49,11 @@ export function configFromRoomHtml(html) { if (!m) throw Error('不是有效的房间文件'); return validate(JSON.parse(m[1])); } + +// A complete file for any config (a new room, a converted one, a blank +// creator), from the exact script and style bytes this page runs with. +export async function htmlFor(config, script, style) { + if (typeof script !== 'string' || typeof style !== 'string' || /<\/script/i.test(script) || /<\/style/i.test(style)) throw Error('脚本或样式不可用'); + if (config.version) validate(config); else validateBlank(config); + return roomHtml(config, script, style, await digest(script), await digest(style)).html; +} diff --git a/html-room/transport.js b/html-room/transport.js index f3b138f..c3de2d0 100644 --- a/html-room/transport.js +++ b/html-room/transport.js @@ -1,4 +1,4 @@ -import { validate, relayEntry, relayKind, relayUrl, connectSources } from './crypto.js'; +import { validate, validateBlank, relayEntry, relayKind, relayUrl, connectSources } from './crypto.js'; // The page that can open this room from a link: a classic relay's /hr.html, or // for a decentralized room whose relays are all bc paths on one server, that // server's page. Other relay mixes have no web entry; use the file. @@ -30,8 +30,8 @@ export async function fillTemplate(template,config,allowed,scriptHash,styleHash, const scripts=[...template.matchAll(/]*)>([\s\S]*?)<\/script>/g)],styles=[...template.matchAll(/

                                      加密聊天房间

                                      正在启动……如果一直停在这里,请看下面的说明。

                                      页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                                        + +加密聊天房间

                                        加密聊天房间

                                        正在启动……如果一直停在这里,请看下面的说明。

                                        页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                                          + diff --git a/dist/hr.html b/dist/hr.html index c788b9c..a55968b 100644 --- a/dist/hr.html +++ b/dist/hr.html @@ -1,2 +1,35 @@ -加密聊天房间

                                          加密聊天房间

                                          正在启动……如果一直停在这里,请看下面的说明。

                                          页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                                            + +加密聊天房间

                                            加密聊天房间

                                            正在启动……如果一直停在这里,请看下面的说明。

                                            页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                                              + diff --git a/docs/DEVELOPER.md b/docs/DEVELOPER.md index fc1d264..41f6ef3 100644 --- a/docs/DEVELOPER.md +++ b/docs/DEVELOPER.md @@ -232,6 +232,8 @@ Vercel 配置同名构建变量并重新部署。旧 `HR_PAGES_RELAY` 仍接受 新文件由当前页面正在运行的脚本和样式字节(`currentScript`、`

                                              加密聊天房间

                                              正在启动……如果一直停在这里,请看下面的说明。

                                              ${previewHelp(config)}
                                              ${RETARGET}

                                                ${MAKER}

                                                HTML Room 开源项目:${repoLink}

                                                \n`; + const html=`${comment ? `\n` : ''}加密聊天房间

                                                加密聊天房间

                                                正在启动……如果一直停在这里,请看下面的说明。

                                                ${previewHelp(config)}
                                                ${RETARGET}

                                                  ${MAKER}

                                                  HTML Room 开源项目:${repoLink}

                                                  \n${comment ? '\n' : ''}`; return { html, csp }; } @@ -37,10 +45,10 @@ export function withRelays(config, input, custom = config.custom === true) { } 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, custom) { - if (typeof script !== 'string' || typeof style !== 'string' || /<\/script/i.test(script) || /<\/style/i.test(style)) throw Error('脚本或样式不可用'); +// own: { script, style, comment } — the exact bytes of the running page. +export async function retargetHtml(config, input, own, custom) { const next = withRelays(config, input, custom); - return { config: next, ...roomHtml(next, script, style, await digest(script), await digest(style)) }; + return { config: next, html: await htmlFor(next, own) }; } export function configFromRoomHtml(html) { @@ -52,8 +60,8 @@ export function configFromRoomHtml(html) { // A complete file for any config (a new room, a converted one, a blank // creator), from the exact script and style bytes this page runs with. -export async function htmlFor(config, script, style) { +export async function htmlFor(config, { script, style, comment = '' }) { if (typeof script !== 'string' || typeof style !== 'string' || /<\/script/i.test(script) || /<\/style/i.test(style)) throw Error('脚本或样式不可用'); if (config.version) validate(config); else validateBlank(config); - return roomHtml(config, script, style, await digest(script), await digest(style)).html; + return roomHtml(config, script, style, await digest(script), await digest(style), comment).html; } diff --git a/package.json b/package.json index 996402c..4ba8b05 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build:pages": "node scripts/build-html-room.mjs pages", "build:check": "node scripts/build-html-room.mjs template --check", "test": "node --test", - "test:browser": "node browser-tests/htmlroom.mjs && node browser-tests/html-room-preview.mjs && node browser-tests/multi-relay.mjs && node browser-tests/retarget.mjs && node browser-tests/mesh.mjs && node browser-tests/custom-relay.mjs && node browser-tests/generations.mjs", + "test:browser": "node browser-tests/htmlroom.mjs && node browser-tests/html-room-preview.mjs && node browser-tests/multi-relay.mjs && node browser-tests/retarget.mjs && node browser-tests/mesh.mjs && node browser-tests/custom-relay.mjs && node browser-tests/generations.mjs && node browser-tests/file-server.mjs", "test:retarget": "node browser-tests/retarget.mjs", "test:failover": "node browser-tests/multi-relay.mjs", "test:pages": "npm run build:pages && node browser-tests/pages.mjs", diff --git a/scripts/build-html-room.mjs b/scripts/build-html-room.mjs index 4037bb2..a2406a2 100644 --- a/scripts/build-html-room.mjs +++ b/scripts/build-html-room.mjs @@ -4,7 +4,7 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { makeConfig, relayEntry, relayList, validate, validateBlank, CUSTOM_SOURCES } from '../html-room/crypto.js'; import { escapeConfig } from '../html-room/transport.js'; -import { roomHtml, withRelays, configFromRoomHtml } from '../html-room/template.js'; +import { roomHtml, withRelays, configFromRoomHtml, serverComment, SERVER_FORBIDDEN } from '../html-room/template.js'; export { escapeConfig }; const root=fileURLToPath(new URL('../',import.meta.url)); const hash=s=>createHash('sha256').update(s).digest('base64'); @@ -14,11 +14,17 @@ export async function render(config, allowed=[]) { const result=await build({entryPoints:[root+'html-room/file.js'],bundle:true,write:false,format:'iife',target:['es2020'],minify:true,legalComments:'none'}); // The script embeds the room HTML (template.js) to re-emit itself; tag openers // occur only inside string literals, where \x3C is the same character. - const script=result.outputFiles[0].text.trimEnd().replace(/<(\\?\/?)(script|style)/gi,'\\x3C$1$2'); - if(/<\/?(script|style)/i.test(script))throw Error('可执行脚本含不安全标签'); + // Likewise comment delimiters, which would end the file's HTML or Node comments + // early; they too occur only inside literals (see html-room/template.js). + const script=result.outputFiles[0].text.trimEnd().replace(/<(\\?\/?)(script|style)/gi,'\\x3C$1$2') + .replace(/ A["空白文件 · 浏览器
                                                  创建器(可离线)"] + F --> B["房间文件 · 浏览器
                                                  加密聊天室"] + F --> C["node index.html
                                                  中继服务器"] + A --> G["生成房间文件
                                                  新钥匙,可设口令"] + G -- 发给朋友 --> B + B -. "↻ 每个房间文件也能再生新房间" .-> G + B <--> R["中继(只转发密文)
                                                  wss:// 经典 · bc+ / nostr+ / mqtt+ 去中心化"] + C --> R + + classDef file fill:#F1EFE8,stroke:#5F5E5A,color:#2C2C2A + classDef browser fill:#EEEDFE,stroke:#534AB7,color:#26215C + classDef relay fill:#E1F5EE,stroke:#0F6E56,color:#04342C + class F file + class A,B,G browser + class C,R relay +``` + +- **浏览器打开空白文件**:创建器,断网也能生成房间文件。 +- **浏览器打开房间文件**:加密聊天室;房间文件里也带着创建器,可以继续生成新房间。 +- **`node index.html`**:中继服务器;空白文件运行时还会提供创建页面,房间文件运行时不公开自己。 +- 聊天要经过中继,中继只看得到密文。 + ## 三步开始聊天 **第 1 步:创建房间** From 261fd394b05d7daa9367c277b66f6811a9268dfc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 18:53:36 -0400 Subject: [PATCH 09/13] feat: rooms of up to 50, fixed in code, with traffic that scales - src/limits.js: MAX_MEMBERS = 50, the single source for every relay (Node, Worker, the file's own server, /bc/) and every page; not configurable. - Key wraps go only to their members: key frames carry `to`, relays hand them to those members (hellos still go to all); old relays ignore it. - A page asks again (hello) only the members whose chains are missing, never the whole room on a timer: every hello makes all receivers re-send. - Owed keys are batched for 300 ms; decentralized numbers settle 1.5 s before keys are made, and claims spread over the lowest free numbers. - lib/group.js: learning a member's ML-KEM key for the first time no longer resets a pair secret already received (stalled pairs under targeting). - Relay limits sized for a newcomer's key burst in a full room. Measured with 50 real clients in one process: classic relay, all ready in ~7 s, ~265 KB each (was: relay disconnects at ~600 MB); decentralized /bc/ 20 members ~12 s, ~450 KB each. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PCUwNSXQdqfDSugmFo4qmM --- README.md | 2 +- dist/_headers | 2 +- dist/hr-template.html | 36 +++---- dist/hr.html | 36 +++---- docs/DEVELOPER.md | 2 +- docs/html-room/protocol.md | 14 +++ docs/html-room/release-integrity.json | 12 +-- docs/html-room/security-model.md | 4 + html-room/client.js | 2 +- html-room/mesh.js | 51 +++++---- html-room/relay-connection.js | 3 +- html-room/serve.js | 8 +- html-room/session.js | 63 +++++++++-- lib/group.js | 10 +- lib/state.js | 5 +- relay/go/main.go | 4 +- server/server.mjs | 10 +- src/limits.js | 19 ++++ src/rules.js | 23 ++-- src/worker.js | 5 +- test/fifty.test.mjs | 145 ++++++++++++++++++++++++++ test/html-room.test.mjs | 2 +- test/mesh.test.mjs | 15 +-- 23 files changed, 364 insertions(+), 109 deletions(-) create mode 100644 src/limits.js create mode 100644 test/fifty.test.mjs diff --git a/README.md b/README.md index b852950..9bbf074 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ node index.html --help # 全部选项 | ✅ 可以 | ❌ 不可以 | | --- | --- | | 发文字消息 | 发图片、文件、语音 | -| 一个房间最多 **25 人** | 保存聊天记录 | +| 一个房间最多 **50 人**(写死在代码里) | 保存聊天记录 | | 每条消息最多 **500 字** | 不联网使用(聊天时必须有网) | | 断网后自动重连 | 找回已关闭页面里的消息 | diff --git a/dist/_headers b/dist/_headers index f255079..d3fd26d 100644 --- a/dist/_headers +++ b/dist/_headers @@ -1,5 +1,5 @@ /* - Content-Security-Policy: default-src 'none'; script-src 'sha256-132cHb6St5eGcvEL/1E4x1v4FhIOR8q/cSGgPUvsHXQ='; style-src 'sha256-+cH4mX6DQpXWnU8EWkUQE30KyFp1e6rCFFdGd+i+pzA='; connect-src 'self' wss: ws://localhost:* ws://127.0.0.1:*; img-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none' + Content-Security-Policy: default-src 'none'; script-src 'sha256-Kg0nJGG+xHIa3LK+UZGnnux+Lsj/oUbwhnGEPDnbc7U='; style-src 'sha256-+cH4mX6DQpXWnU8EWkUQE30KyFp1e6rCFFdGd+i+pzA='; connect-src 'self' wss: ws://localhost:* ws://127.0.0.1:*; img-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none' Cache-Control: no-store NEL: {"max_age":0} Referrer-Policy: no-referrer diff --git a/dist/hr-template.html b/dist/hr-template.html index a55968b..14b376e 100644 --- a/dist/hr-template.html +++ b/dist/hr-template.html @@ -1,35 +1,35 @@ -加密聊天房间

                                                  加密聊天房间

                                                  正在启动……如果一直停在这里,请看下面的说明。

                                                  页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                                                    +加密聊天房间

                                                    加密聊天房间

                                                    正在启动……如果一直停在这里,请看下面的说明。

                                                    页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                                                      diff --git a/dist/hr.html b/dist/hr.html index a55968b..14b376e 100644 --- a/dist/hr.html +++ b/dist/hr.html @@ -1,35 +1,35 @@ -加密聊天房间

                                                      加密聊天房间

                                                      正在启动……如果一直停在这里,请看下面的说明。

                                                      页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                                                        +加密聊天房间

                                                        加密聊天房间

                                                        正在启动……如果一直停在这里,请看下面的说明。

                                                        页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                                                          diff --git a/docs/DEVELOPER.md b/docs/DEVELOPER.md index 41f6ef3..c735e53 100644 --- a/docs/DEVELOPER.md +++ b/docs/DEVELOPER.md @@ -11,7 +11,7 @@ - **内存会话**:客户端不使用 Cookie、localStorage 或 IndexedDB 保存聊天状态;中继不持久化聊天记录。 - **独立部署**:提供 Node.js、Docker 和 Cloudflare Workers 中继,以及 GitHub Pages 静态前端。 -只支持文字聊天,单房间最多 **25 人**,单条消息最多 **500 字**。不包含大厅、图片、文件传输或桌游。 +只支持文字聊天,单房间最多 **50 人**(`src/limits.js` 写死,所有中继和页面执行同一个数,不可配置),单条消息最多 **500 字**。不包含大厅、图片、文件传输或桌游。 ## 前端与中继如何配合 diff --git a/docs/html-room/protocol.md b/docs/html-room/protocol.md index 6780223..d46a90e 100644 --- a/docs/html-room/protocol.md +++ b/docs/html-room/protocol.md @@ -55,3 +55,17 @@ V2/V3 配置可选字段 `custom`,只能为 `true`(缺省表示不允许) ## 文件即服务器 文件结构:`\n` + 页面 HTML + `\n`。浏览器把开头部分视为文档首个注释节点;Node(CommonJS)把首行视为 HTML 式行注释,把页面视为块注释。客户端与服务端代码都不得含 `*/`、`-->`、``、`{e.preventDefault();if(busy||Dat } session.tick=setInterval(()=>session.run(async()=>{ if(session.ws?.readyState===1){ - if(Date.now()-(session.lastAnnounce??0)>10000){await session.announce(true);session.lastAnnounce=Date.now();} + {const need=session.helloDue();if(need){await session.announce(true,need);session.lastAnnounce=Date.now();}} await session.flush();await session.retryPending(); } }),1600); diff --git a/html-room/mesh.js b/html-room/mesh.js index fa6651f..6dbdd96 100644 --- a/html-room/mesh.js +++ b/html-room/mesh.js @@ -16,7 +16,7 @@ // admission every frame is AES-GCM under a key derived from the room master // (meshKey), bound to the topic; anything else is dropped unread // numbers a newcomer announces itself, members answer, and after a few -// seconds it claims the lowest free number 1-25; on a clash +// seconds it claims the lowest free number 1-50; on a clash // the earlier claim keeps it (lower node id on a tie), so a // late joiner never displaces an established member // presence heartbeats every 4 s; silent for 14 s means gone @@ -26,10 +26,15 @@ // relay; a malicious member can also claim a taken one. The safety code covers // every present identity, so re-check it whenever the member list changes. import { hex, random, relayKind, relayUrl } from './crypto.js'; +import { MAX_MEMBERS } from '../src/limits.js'; import { schnorr } from '@noble/curves/secp256k1'; import { sha256 } from '@noble/hashes/sha256'; -export const HB_MS = 4000, ALIVE_MS = 14000, CLAIM_MS = 3000, MAX_WIRE = 16000, SKEW = 600000, MAX_MEMBERS = 25; +// A claimed number is held this long, clash-free, before keys are made for it: +// clashes in a crowd then cost a new number, not a new set of keys for everyone. +export const SETTLE_MS = 1500; +export const HB_MS = 4000, ALIVE_MS = 14000, CLAIM_MS = 3000, MAX_WIRE = 16000, SKEW = 600000; +export { MAX_MEMBERS }; export const NOSTR_KIND = 20111; const enc = new TextEncoder(), dec = new TextDecoder(); const b64 = u8 => { let s = ''; for (let i = 0; i < u8.length; i += 4096) s += String.fromCharCode(...u8.subarray(i, i + 4096)); return btoa(s); }; @@ -170,12 +175,12 @@ export class MeshConnection { constructor(relays, { derived, onhello, onmembers, onframe, onstatus = () => {}, onnotice = () => {}, onfatal = () => {}, createSocket = (url, list) => list ? new WebSocket(url, list) : new WebSocket(url), now = () => Date.now(), claimDelay = CLAIM_MS + Math.random() * 1000 }) { Object.assign(this, { d: derived, onhello, onmembers, onframe, onstatus, onnotice, onfatal, createSocket, now, claimDelay }); - this.node = hex(random(8)); this.me = 0; this.peers = new Map(); this.known = new Set(); this.seenIvs = new Map(); this.owners = new Map(); this.lastHb = 0; + this.node = hex(random(8)); this.me = 0; this.settled = false; this.peers = new Map(); this.known = new Set(); this.pending = new Map(); this.seenIvs = new Map(); this.owners = new Map(); this.lastHb = 0; this.nostrKey = schnorr.utils.randomPrivateKey(); this.links = relays.map(entry => new LINKS[relayKind(entry)](this, entry)); const mesh = this; // What Session expects of a socket. - this.socket = { get readyState() { return mesh.me && mesh.openCount() ? 1 : 0; }, send: s => mesh.sendSession(s), close() {} }; + this.socket = { get readyState() { return mesh.me && mesh.settled && mesh.openCount() ? 1 : 0; }, send: s => mesh.sendSession(s), close() {} }; } start() { for (const link of this.links) link.connect(); @@ -186,7 +191,7 @@ export class MeshConnection { stop() { if (this.stopped) return; if (this.me) this.broadcast({ k: 'bye' }).catch(() => {}); - this.stopped = true; clearTimeout(this.claimTimer); clearTimeout(this.answer); clearInterval(this.timer); + this.stopped = true; clearTimeout(this.claimTimer); clearTimeout(this.settleTimer); clearTimeout(this.answer); clearInterval(this.timer); setTimeout(() => this.links.forEach(l => l.stop()), 50); } openCount() { return this.links.filter(l => l.open).length; } @@ -212,7 +217,7 @@ export class MeshConnection { } sendSession(text) { let frame; try { frame = JSON.parse(text); } catch { return; } - if (!FRAME_TYPES.includes(frame?.type) || !this.me) return; + if (!FRAME_TYPES.includes(frame?.type) || !this.me || !this.settled) return; const me = this.me; this.broadcast({ k: 'f', f: frame }).then(sent => { // Relays need not echo to the sender; "submitted" means at least one took it. @@ -227,11 +232,13 @@ export class MeshConnection { const inner = await openFrame(this.d, wire, this.now()); if (!inner || inner.d === this.node || this.stopped) return; if (!this.known.has(inner.d) && inner.k !== 'bye') { this.known.add(inner.d); this.answerSoon(); } - if (inner.k === 'bye') this.peers.delete(inner.d); - else if (inner.n > 0) this.peers.set(inner.d, { n: inner.n, c: inner.c ?? inner.t, seen: this.now() }); + if (inner.k === 'bye') { this.peers.delete(inner.d); this.pending.delete(inner.d); } + else if (inner.n > 0) { this.peers.set(inner.d, { n: inner.n, c: inner.c ?? inner.t, seen: this.now() }); this.pending.delete(inner.d); } + else this.pending.set(inner.d, this.now()); this.reconcile(); // at: when it arrived here, the role a classic relay's timestamp plays (lib/state.js). - if (inner.k === 'f' && inner.n > 0 && this.owners.get(inner.n) === inner.d) this.onframe({ ...inner.f, at: this.now() }, inner.n); + // Broadcast relays cannot address; frames meant for others (to) are skipped unopened. + if (inner.k === 'f' && inner.n > 0 && this.owners.get(inner.n) === inner.d && !(Array.isArray(inner.f.to) && !inner.f.to.includes(this.me))) this.onframe({ ...inner.f, at: this.now() }, inner.n); } answerSoon() { if (!this.me || this.answer) return; @@ -239,16 +246,24 @@ export class MeshConnection { } claim() { if (this.stopped) return; - const used = new Set([...this.live()].map(([, p]) => p.n)); - let n = 1; while (used.has(n)) n++; - if (n > MAX_MEMBERS) { this.stop(); this.onfatal('房间已满(25 人)。'); return; } - this.me = n; this.claimedAt = this.now(); this.lastHb = 0; - this.onhello(n); + const used = new Set([...this.live()].map(([, p]) => p.n)), now = this.now(); + // Others still unnumbered are claiming too: spread over the lowest few + // free numbers instead of all taking the lowest. + const crowd = [...this.pending.values()].filter(t => now - t <= ALIVE_MS).length; + const free = []; for (let n = 1; n <= MAX_MEMBERS && free.length < Math.min(5, 1 + crowd); n++) if (!used.has(n)) free.push(n); + if (!free.length) { this.stop(); this.onfatal(`房间已满(${MAX_MEMBERS} 人)。`); return; } + const n = free[Math.floor(Math.random() * free.length)]; + this.me = n; this.claimedAt = now; this.lastHb = 0; this.settled = false; + clearTimeout(this.settleTimer); + this.settleTimer = setTimeout(() => { + if (this.stopped || this.me !== n) return; + this.settled = true; this.onhello(n); this.reconcile(true); + }, SETTLE_MS); this.sweep(); } *live() { const now = this.now(); for (const e of this.peers) if (now - e[1].seen <= ALIVE_MS) yield e; } // Owners: for each number, the live node that claimed it first (then lowest id). - reconcile() { + reconcile(force = false) { const owners = new Map(), claims = new Map(); const consider = (n, node, c) => { const cur = claims.get(n); if (!cur || c < cur.c || (c === cur.c && node < cur.node)) { claims.set(n, { node, c }); owners.set(n, node); } }; if (this.me) consider(this.me, this.node, this.claimedAt); @@ -258,12 +273,12 @@ export class MeshConnection { } const changed = [...owners].filter(([n, node]) => this.owners.has(n) && this.owners.get(n) !== node).map(([n]) => n); const same = owners.size === this.owners.size && [...owners].every(([n, node]) => this.owners.get(n) === node); - if (same) return; + if (same && !force) return; // A number passing to another node is a departure then an arrival, so the // session forgets the old identity before it hears the new one. - if (changed.length) this.onmembers([...this.owners.keys()].filter(n => !changed.includes(n)).sort((a, b) => a - b)); + if (changed.length && this.settled) this.onmembers([...this.owners.keys()].filter(n => !changed.includes(n)).sort((a, b) => a - b)); this.owners = owners; - if (this.me) this.onmembers([...owners.keys()].sort((a, b) => a - b)); + if (this.me && this.settled) this.onmembers([...owners.keys()].sort((a, b) => a - b)); } sweep() { if (this.stopped) return; diff --git a/html-room/relay-connection.js b/html-room/relay-connection.js index 2fca0ac..3eaeed2 100644 --- a/html-room/relay-connection.js +++ b/html-room/relay-connection.js @@ -1,3 +1,4 @@ +import { MAX_MEMBERS } from '../src/limits.js'; // Availability and priority only. This layer never logs room URLs or credentials. export class RelayConnection { constructor(relays, { roomId, protocols, onactive, onmessage, onnotice, onfatal, ondisconnect, @@ -42,7 +43,7 @@ export class RelayConnection { } if (r.frames.length >= 50) { this.fail(r); return; } r.frames.push(m); - if (m.type === 'hello' && !r.activating && Number.isInteger(m.you) && m.you >= 1 && m.you <= 25 + if (m.type === 'hello' && !r.activating && Number.isInteger(m.you) && m.you >= 1 && m.you <= MAX_MEMBERS && typeof m.resume === 'string' && /^[0-9a-f]{32}$/.test(m.resume)) { r.activating = true; this.activate(r).catch(() => this.fail(r)); } diff --git a/html-room/serve.js b/html-room/serve.js index 8c3dd27..88cd177 100644 --- a/html-room/serve.js +++ b/html-room/serve.js @@ -16,7 +16,7 @@ import { readFileSync } from 'node:fs'; import { createHash } from 'node:crypto'; import { spawn } from 'node:child_process'; import { - MAX_MEMBERS, ROOM_ID, PONG, CLOSE_BUSY, CLOSE_LEFT, CLOSE_FULL, CLOSE_CAPACITY, CLOSE_DENIED, PLAIN_HEADERS, JOINS_PER_MINUTE, + MAX_MEMBERS, MAX_FRAME, ROOM_ID, PONG, CLOSE_BUSY, CLOSE_LEFT, CLOSE_FULL, CLOSE_CAPACITY, CLOSE_DENIED, PLAIN_HEADERS, JOINS_PER_MINUTE, PROTOCOL, WIRE_VERSION, BC_TOPIC, BC_MAX_FRAME, BC_MAX_PEERS, broadcastFrameBad, emptyBook, admit, depart, awayNumbers, token, hostOf, parseProtocols, accessMatches } from '../src/rules.js'; @@ -138,7 +138,7 @@ export function serve(self, filename, argv = process.argv.slice(2)) { setInterval(() => { const now = Date.now(); for (const [ip, l] of joins) if (!l.some(t => now - t < 60000)) joins.delete(ip); }, 60000).unref(); const sendTo = (ws, obj) => ws.send(typeof obj === 'string' ? obj : JSON.stringify(obj)); - const broadcast = (room, obj) => { const out = JSON.stringify(obj); for (const p of room.peers) p.send(out); }; + const broadcast = (room, obj, to) => { const out = JSON.stringify(obj); for (const p of room.peers) if (!to || to.includes(p.state.n)) p.send(out); }; const members = room => { const host = hostOf([...room.peers].map(p => [p.state.n, p.state.seq])); broadcast(room, { type: 'members', members: [...room.peers].map(p => p.state.n).sort((a, b) => a - b), host, max: MAX_MEMBERS, locked: room.book.locked, away: awayNumbers(room.book) }); @@ -163,7 +163,7 @@ export function serve(self, filename, argv = process.argv.slice(2)) { if (f.close) return ws.close(...f.close); if (f.pong) return sendTo(ws, PONG); if (f.wait) return sendTo(ws, { type: 'wait', ms: f.wait }); - if (f.key) return broadcast(room, { type: 'key', from: n, ...f.key }); + if (f.key) return broadcast(room, { type: 'key', from: n, ...f.key }, f.to); if (f.seen) return broadcast(room, { type: 'seen', from: n, ...f.seen }); broadcast(room, { type: 'msg', from: n, at: f.at, ...f.msg }); }; @@ -203,7 +203,7 @@ export function serve(self, filename, argv = process.argv.slice(2)) { const proto = parseProtocols(req.headers['sec-websocket-protocol']); accessMatches(m[1], proto.access).then(ok => { if (!proto.ok || !ok) return reject('403 Forbidden'); - accept(PROTOCOL, 5600, ws => enter(ws, `hr:${m[1]}`, ip, proto.resume)); + accept(PROTOCOL, MAX_FRAME, ws => enter(ws, `hr:${m[1]}`, ip, proto.resume)); }, () => reject('403 Forbidden')); }); // Drop connections that stopped answering. diff --git a/html-room/session.js b/html-room/session.js index cb01ed4..079df6f 100644 --- a/html-room/session.js +++ b/html-room/session.js @@ -1,20 +1,56 @@ import { Group } from '../lib/group.js'; import { checkKeys, checkEnvelope, checkMessage, envelope } from '../lib/state.js'; import { hex, random, sealAnnouncement, openAnnouncement } from './crypto.js'; +import { MAX_MEMBERS } from '../src/limits.js'; +// Receipts per seen frame: a count of messages, not of members. +const RECEIPTS_PER_FRAME = 25; export const memoryStorage = () => new Map(); // Explicit injection for tests only; no browser persistence API is used. export class Session { - constructor(derived, config, {storage=memoryStorage(),emit=()=>{}}={}) { - Object.assign(this,{d:derived,config,store:storage,emit,deviceId:hex(random(16)),present:[],pins:new Map(),seen:new Map(),own:new Map(),queued:[],receipts:[],received:new Map(),group:null,ws:null,serial:Promise.resolve()}); + // batchMs: how long owed keys are gathered before sending (0: at once). + constructor(derived, config, {storage=memoryStorage(),emit=()=>{},batchMs=300}={}) { + Object.assign(this,{d:derived,config,store:storage,emit,batchMs,deviceId:hex(random(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(fn) { this.serial=this.serial.then(()=>{if(!this.stopped)return fn();}).catch(()=>this.emit('notice','收到一条无法验证的数据,已忽略。'));return this.serial; } send(frame) { if(!this.stopped && this.ws?.readyState===1)this.ws.send(JSON.stringify(frame)); } - async announce(hello=false) { + // to: with hello, ask only these members (see helloDue); without, ask all. + async announce(hello=false, to=null) { if(!this.group)return; const owed=this.group.owed(this.present); if(!hello && !owed.length)return; - for(const text of await this.group.announceAll(owed,hello))this.send({type:'key',...await sealAnnouncement(this.d,text)}); + const texts=await this.group.announceAll(owed,hello); + for(const [i,text] of texts.entries()){ + // Key wraps are for the members listed in w: the relay hands such a frame + // to them only (to), instead of copying ~1.5 KB of ML-KEM to the whole + // room. Only a hello (the first frame) is for everyone. + const wraps=Object.keys(JSON.parse(text).w).filter(k=>/^\d+$/.test(k)).map(Number); + const dest=hello&&i===0?(to?[...new Set([...to,...wraps])]:null):wraps; + if(dest&&!dest.length)continue; + this.send({type:'key',...await sealAnnouncement(this.d,text),...(dest&&{to:dest})}); + } + } + // Keys owed because someone new was just heard from: gathered for a moment + // so a burst of arrivals goes out in a few frames, not one per arrival. + scheduleAnnounce(){ + if(this.announceTimer||this.stopped)return; + this.announceTimer=setTimeout(()=>{this.announceTimer=null;this.run(async()=>{await this.announce();await this.update();});},this.batchMs); + } + // A hello (ask everyone to answer) is due: soon while someone present is + // still unknown, otherwise at the slow background pace. + // Who to ask again, if anyone: members present whose chain (with ML-KEM) + // has not arrived. A hello makes every receiver hand its chain again, so it + // goes only to them — a hello to the whole room at intervals would make all + // n members re-send to each asker, O(n²) frames per round. Once every chain + // is in, nothing is sent on a timer. + helloDue(now=Date.now()) { + if(!this.group || now-(this.lastAnnounce??0) < Math.max(5000, 200*this.present.length))return null; + const g=this.group, need=this.present.filter(n=>{ + if(n===g.me)return false; + const p=g.peers.get(n); + return !p || !p.recv || (p.v===32 && !(p.ss && p.pqIn)); + }); + return need.length ? need : null; } ready() { return !this.stopped && this.group && this.present.length>1 && this.group.ready(this.present,true) && this.group.quantumSafe(this.present); } async incoming(m) { @@ -24,16 +60,20 @@ export class Session { if(m.type==='hello') { this.resume=m.resume; if(!this.group || this.group.me!==m.you){this.group=await Group.create(this.d.key,this.d.bind,m.you);this.own.clear();this.received.clear();this.receipts=[];this.present=[];} - await this.announce(true);return; + await this.announce(true);this.lastAnnounce=Date.now();return; } if(!this.group)return; if(m.type==='members') { - if(!Array.isArray(m.members) || m.members.some(n=>!Number.isInteger(n)||n<1||n>25))return; + if(!Array.isArray(m.members) || m.members.some(n=>!Number.isInteger(n)||n<1||n>MAX_MEMBERS))return; const left=this.present.filter(n=>!m.members.includes(n)); for(const n of left){this.group.forget(n);if(this.config.version===3)this.pins.delete(n);for(const key of this.received.keys())if(key.startsWith(`${n}|`))this.received.delete(key);} this.present=m.members; + // Only what is owed: a newcomer's own hello asks everyone for their keys, + // so a hello from every member at every arrival would only multiply + // traffic (O(n²) frames, each copied to n). Missing keys are asked for by + // the tick (helloDue). if(left.length)await this.group.rekey(); - await this.announce(true);this.emit('presence',this.present.length);await this.update();return; + await this.announce();this.emit('presence',this.present.length);await this.update();return; } if(m.from===this.group.me) { const own=this.own.get(m.h); @@ -50,7 +90,8 @@ export class Session { if(pinned && pinned!==p.spk){this.halt();this.emit('fatal','有成员的身份变了,可能有人冒充。请打电话等方式确认后,重新打开房间。');return;} this.pins.set(m.from,p.spk); if(p?.lost)p.sent=-1; - await this.announce();await this.update();return; + if(this.batchMs)this.scheduleAnnounce();else await this.announce(); + await this.update();return; } if(!['msg','seen'].includes(m.type) || !m.h)return; const receiptKey=`${m.from}|${m.h}`, prior=this.received.get(receiptKey); @@ -59,7 +100,7 @@ export class Session { if(!opened.signed || this.group.peers.get(m.from)?.v!==32)return; if(m.type==='seen') { const a=checkKeys(opened.text,m,this.seen,Date.now(),'seen'); - if(a && Array.isArray(a.l) && a.l.length<=25)for(const item of a.l){ + if(a && Array.isArray(a.l) && a.l.length<=RECEIPTS_PER_FRAME)for(const item of a.l){ if(!Array.isArray(item) || item[0]!==this.group.me)continue; const own=this.own.get(item[1]); if(own && own.targets.includes(m.from)){own.confirmed.add(m.from);this.emit('delivery',{id:own.id,text:`已送达 ${own.confirmed.size}/${own.targets.length}`});} @@ -87,7 +128,7 @@ export class Session { if(this.group.due()){await this.group.rekey();await this.announce();} if(!this.ready())return; if(this.receipts.length){ - const plain=JSON.stringify({kind:'seen',id:hex(random(8)),from:this.group.me,t:Date.now(),l:this.receipts.splice(0,25)}); + const plain=JSON.stringify({kind:'seen',id:hex(random(8)),from:this.group.me,t:Date.now(),l:this.receipts.splice(0,RECEIPTS_PER_FRAME)}); this.send({type:'seen',...await this.group.seal(plain)}); } // Match the existing relay's message spacing, keeping a bounded memory queue. @@ -113,5 +154,5 @@ export class Session { this.own.clear();this.received.clear();this.receipts=[];this.nextSend=0;this.lastAnnounce=0; this.emit('presence',0);this.emit('relay-change'); } - halt(){this.stopped=true;clearTimeout(this.retry);clearInterval(this.tick);this.connection?.stop();this.ws?.close(4000);} + halt(){this.stopped=true;clearTimeout(this.retry);clearTimeout(this.announceTimer);clearInterval(this.tick);this.connection?.stop();this.ws?.close(4000);} } diff --git a/lib/group.js b/lib/group.js index 652711f..0b37c85 100644 --- a/lib/group.js +++ b/lib/group.js @@ -293,8 +293,14 @@ export class Group { // the chain again, with it. if (v31 && a.w.q === 1 && p.v !== 32) { p.v = 32; p.sent = -1; } if (v31 && typeof a.w.k === 'string' && a.w.k.length === KPK && B64.test(a.w.k) && a.w.k !== p.kpk) { - Object.assign(p, { kpk: a.w.k, ss: null, kc: null, kcIn: null, v: 32 }); - p.wraps = new Map(); + // A different key than before starts the pair over. Learning it for the + // first time does not: with key wraps addressed only to their members, a + // higher number can hold the pair's secret (from the lower one's + // ciphertext, made against this page's key) before it ever sees the + // lower one's key, and resetting then would leave the pair waiting on a + // ciphertext the other side already sent. + if (p.kpk) { Object.assign(p, { ss: null, kc: null, kcIn: null }); p.wraps = new Map(); } + Object.assign(p, { kpk: a.w.k, v: 32 }); } const wrapped = a.w[this.me]; if (wrapped && typeof wrapped.iv === 'string' && typeof wrapped.ct === 'string') { diff --git a/lib/state.js b/lib/state.js index cca1d60..ce0f396 100644 --- a/lib/state.js +++ b/lib/state.js @@ -1,5 +1,6 @@ // Rules shared by the page and the tests. -export const MAX_MEMBERS = 25; +export { MAX_MEMBERS } from '../src/limits.js'; +import { MAX_MEMBERS } from '../src/limits.js'; export const MAX_TEXT_CHARS = 500; export const MAX_TEXT_BYTES = 2048; export const SHORT_PASSPHRASE = 8; // shorter ones work, with a warning @@ -57,7 +58,7 @@ export function checkMessage(text) { return clean; } -// Visitors are numbered 1️⃣ to 🔟, then #11 to #25: there are no keycap emoji +// Visitors are numbered 1️⃣ to 🔟, then #11 to #50: there are no keycap emoji // past ten, and circled numbers are too small to read in running text. export const BADGES = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟']; diff --git a/relay/go/main.go b/relay/go/main.go index b7e7c31..d9d8c3b 100644 --- a/relay/go/main.go +++ b/relay/go/main.go @@ -44,9 +44,9 @@ var ( tlsKey = flag.String("tls-key", "", "TLS key file") allowOrigin = flag.String("allow-origin", "", "comma-separated Origins browsers may connect from (default: any; data is end-to-end encrypted)") maxConns = flag.Int("max-conns", 2000, "maximum simultaneous connections") - maxPeers = flag.Int("max-peers", 50, "maximum connections per topic") + maxPeers = flag.Int("max-peers", 64, "maximum connections per topic (a full room of 50 plus reconnects)") maxFrame = flag.Int("max-frame", 16000, "maximum frame size in bytes") - ratePer10s = flag.Int("rate", 60, "frames per connection per 10 seconds") + ratePer10s = flag.Int("rate", 100, "frames per connection per 10 seconds") tunnel = flag.Bool("tunnel", false, "start a Cloudflare quick tunnel (needs cloudflared) and print the public relay address") cloudflared = flag.String("cloudflared", "cloudflared", "path to the cloudflared binary") showVersion = flag.Bool("version", false, "print version and exit") diff --git a/server/server.mjs b/server/server.mjs index 5cc5688..c92581a 100644 --- a/server/server.mjs +++ b/server/server.mjs @@ -24,7 +24,7 @@ import { join, extname, normalize } from 'node:path'; import { fileURLToPath } from 'node:url'; import { WebSocketServer } from 'ws'; import { - MAX_MEMBERS, ROOM_ID, PONG, CLOSE_BUSY, CLOSE_CAPACITY, CLOSE_FULL, CLOSE_LEFT, CLOSE_DENIED, PLAIN_HEADERS, BYE_HEADERS, JOINS_PER_MINUTE, + MAX_MEMBERS, MAX_FRAME, ROOM_ID, PONG, CLOSE_BUSY, CLOSE_CAPACITY, CLOSE_FULL, CLOSE_LEFT, CLOSE_DENIED, PLAIN_HEADERS, BYE_HEADERS, JOINS_PER_MINUTE, PROTOCOL, WIRE_VERSION, COVER, inside, BC_TOPIC, BC_MAX_FRAME, BC_MAX_PEERS, broadcastFrameBad, emptyBook, admit, depart, awayNumbers, token, hostOf, parseProtocols, accessMatches } from '../src/rules.js'; @@ -178,9 +178,9 @@ function send(ws, obj) { const hostIn = (room) => hostOf([...room.peers].map((p) => [p.state.n, p.state.seq])); -function broadcast(room, obj) { +function broadcast(room, obj, to) { const out = JSON.stringify(obj); - for (const peer of room.peers) send(peer, out); + for (const peer of room.peers) if (!to || to.includes(peer.state.n)) send(peer, out); } function members(room) { @@ -190,7 +190,7 @@ function members(room) { const wss = new WebSocketServer({ noServer: true, - maxPayload: 5600, + maxPayload: MAX_FRAME, // The page asks for pt2 alongside its access value and resume token; only // pt2 is answered. handleProtocols: (protocols) => (protocols.has(PROTOCOL) ? PROTOCOL : false) @@ -249,7 +249,7 @@ function enter(ws, roomId, ip, resume) { if (r.close) return ws.close(...r.close); if (r.pong) return send(ws, PONG); if (r.wait) return send(ws, { type: 'wait', ms: r.wait }); - if (r.key) return broadcast(room, { type: 'key', from: n, ...r.key }); + if (r.key) return broadcast(room, { type: 'key', from: n, ...r.key }, r.to); if (r.seen) return broadcast(room, { type: 'seen', from: n, ...r.seen }); broadcast(room, { type: 'msg', from: n, at: r.at, ...r.msg }); }); diff --git a/src/limits.js b/src/limits.js new file mode 100644 index 0000000..c34d3f7 --- /dev/null +++ b/src/limits.js @@ -0,0 +1,19 @@ +// The room size, fixed in code: every relay (Node, Worker, the room file's own +// server, /bc/) and every page enforce the same number. Nothing reads it from +// configuration, environment or a room file. +// +// Why 50: the cost that grows with the room is the key exchange, not chat. +// Every pair of members runs a hybrid ECDH + ML-KEM-768 exchange once (about +// 1.5 KB of ML-KEM per pair, sent by the lower number), so a full room holds +// 50 × 49 / 2 = 1225 pairs; later rekeys carry only small wraps (about 20 +// members per 4 KB announcement). The worst moment is a newcomer that gets a +// low number and must encapsulate for all 49 others: about 25 announcement +// frames at once, which the relay limits in rules.js leave room for. Beyond +// this the right design is a tree of group keys (MLS-style), not a bigger +// number here. +export const MAX_MEMBERS = 50; + +// Traffic at this size is kept down by three rules elsewhere: key wraps go only +// to the members they are for (session.js, relays), a page asks again only the +// members whose keys it lacks (Session.helloDue), and in decentralized rooms a +// number settles before keys are made for it (mesh.js SETTLE_MS). diff --git a/src/rules.js b/src/rules.js index 0f98738..7b87dbe 100644 --- a/src/rules.js +++ b/src/rules.js @@ -3,13 +3,15 @@ // Rules shared by both relays: the Cloudflare Worker (src/worker.js) and the // Docker server (server/server.mjs). Each relay keeps its own room state and // calls these functions, so the two behave the same. -export const MAX_MEMBERS = 25; // visitors are numbered 1️⃣ to 🔟, then #11 to #25 -export const RATE_PER_10S = 60; // frames of any kind per connection per 10 seconds +// The room size lives in limits.js (fixed at 50, see there). +import { MAX_MEMBERS } from './limits.js'; +export { MAX_MEMBERS }; +export const RATE_PER_10S = 100; // frames of any kind per connection per 10 seconds (a newcomer to a full room sends ~25 key frames at once) export const SEND_GAP = 6000; // one message per this long per person, on average export const SEND_BURST = 3; // up to this many in a row first (a quick reply, a correction) export const GAP_SLACK = 250; // allowance for network jitter export const LOCK_GAP = 2000; // at least this long between one person's lock changes -export const KEYS_PER_10S = 40; // v3 key announcements per connection per 10 seconds (also count above). A page sends one or two a second, but one joining a full room as a low number sends up to 13 at once (ML-KEM secrets are large) +export const KEYS_PER_10S = 60; // v3 key announcements per connection per 10 seconds (also count above). A page sends one or two a second, but one joining a full room of 50 as a low number sends about 25 at once (ML-KEM secrets are large) export const BAD_FRAMES = 5; // malformed frames tolerated before the connection is closed // Delivery receipts (sealed lists of messages a page has read; see app.js): // a page sends at most one every 1.5 seconds; more are dropped, not punished. @@ -19,7 +21,7 @@ export const SEEN_PER_10S = 10; // per person per minute; more are dropped, not punished. export const FILES_PER_MINUTE = 6; export const PINGS_PER_10S = 4; // heartbeats per connection per 10 seconds (the page sends one every 25) -export const JOINS_PER_MINUTE = 60; // per address (a 25-person room behind one NAT or VPN exit must fit) +export const JOINS_PER_MINUTE = 120; // per address (a 50-person room behind one NAT or VPN exit must fit, with reconnects) export const GRACE = 2 * 60 * 1000; // how long a dropped member may come back, even to a locked room export const ROOM_ID = /^[0-9a-f]{64}$/; export const RESUME = /^[0-9a-f]{32}$/; @@ -68,7 +70,7 @@ export async function accessMatches(roomId, access) { // ASCII, so a frame's length in characters is its length in bytes. const IV = /^[A-Za-z0-9+/]{16}$/; const CT = /^[A-Za-z0-9+/]{24,5484}={0,2}$/; -export const MAX_FRAME = 5600; +export const MAX_FRAME = 5800; // a full key frame plus its recipient list (to) const CHAIN = /^\d{1,9}\.\d{1,9}$/; const ASCII = /^[\x20-\x7e]*$/; @@ -212,7 +214,12 @@ export function checkFrame(raw, state, now = Date.now()) { if (msg.type === 'key') { state.keys = state.keysWindow === window ? (state.keys ?? 0) + 1 : 1; state.keysWindow = window; - return state.keys > KEYS_PER_10S ? { close: [1008, 'too many keys'] } : { key: { iv: msg.iv, ct: msg.ct } }; + if (state.keys > KEYS_PER_10S) return { close: [1008, 'too many keys'] }; + // to: the members whose key wraps this frame carries; the relay hands it to + // them only. Without it (a hello) the frame goes to everyone. + if (msg.to === undefined) return { key: { iv: msg.iv, ct: msg.ct } }; + if (!Array.isArray(msg.to) || !msg.to.length || msg.to.length > MAX_MEMBERS || msg.to.some((n) => !Number.isInteger(n) || n < 1 || n > MAX_MEMBERS)) return bad(); + return { key: { iv: msg.iv, ct: msg.ct }, to: [...new Set(msg.to)] }; } if (msg.h !== undefined && (typeof msg.h !== 'string' || !CHAIN.test(msg.h))) return bad(); // A leaky bucket (GCRA): sent is the time the person's allowance is spent @@ -249,8 +256,8 @@ export const COVER = { status: 404, headers: { 'Content-Type': 'text/plain; char // same topic. Any server doing just that is compatible (see relay/go). export const BC_TOPIC = /^[0-9a-f]{32}$/; export const BC_MAX_FRAME = 16000; -export const BC_MAX_PEERS = 50; -export const BC_RATE_PER_10S = 60; +export const BC_MAX_PEERS = MAX_MEMBERS + 14; // a full room plus reconnects still being noticed +export const BC_RATE_PER_10S = 100; // like RATE_PER_10S: room for a newcomer's key burst // state { window, count } is updated in place; true means close the connection. export function broadcastFrameBad(raw, state, now = Date.now()) { const window = Math.floor(now / 10000); diff --git a/src/worker.js b/src/worker.js index 79329a9..4eef142 100644 --- a/src/worker.js +++ b/src/worker.js @@ -146,7 +146,7 @@ export class Room extends DurableObject { if (r.close) return ws.close(...r.close); if (r.pong) return ws.send(PONG); if (r.wait) return ws.send(JSON.stringify({ type: 'wait', ms: r.wait })); - if (r.key) return this.broadcast({ type: 'key', from: a.n, ...r.key }); + if (r.key) return this.broadcast({ type: 'key', from: a.n, ...r.key }, undefined, r.to); if (r.seen) return this.broadcast({ type: 'seen', from: a.n, ...r.seen }); this.broadcast({ type: 'msg', from: a.n, at: r.at, ...r.msg }); } @@ -174,9 +174,10 @@ export class Room extends DurableObject { this.broadcast({ type: 'members', members: list, host: this.host(peers), max: MAX_MEMBERS, locked: book.locked, away: awayNumbers(book) }, skip); } - broadcast(obj, skip) { + broadcast(obj, skip, to) { const out = JSON.stringify(obj); for (const peer of this.peers(skip)) { + if (to && !to.includes(peer.deserializeAttachment().n)) continue; try { peer.send(out); } catch {} } } diff --git a/test/fifty.test.mjs b/test/fifty.test.mjs new file mode 100644 index 0000000..2fcd77a --- /dev/null +++ b/test/fifty.test.mjs @@ -0,0 +1,145 @@ +// A full room: 50 real pages (full signed hybrid encryption, ML-KEM) on a real +// Node relay join at once, agree on one safety code, chat, survive a departure, +// and the 51st is refused. Also checks the limit cannot be raised from outside. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import WebSocket from 'ws'; +import { makeConfig, unlock, hex } from '../html-room/crypto.js'; +import { Session } from '../html-room/session.js'; +import { RelayConnection } from '../html-room/relay-connection.js'; +import { MeshConnection } from '../html-room/mesh.js'; +import { MAX_MEMBERS } from '../src/limits.js'; +import { MAX_MEMBERS as RELAY_MAX, admit, emptyBook } from '../src/rules.js'; +import { MAX_MEMBERS as STATE_MAX } from '../lib/state.js'; +import { MAX_MEMBERS as MESH_MAX } from '../html-room/mesh.js'; +import { nodeRelay } from './fixtures/mesh-relays.mjs'; + +test('the room size is 50 everywhere and fixed in code', () => { + assert.equal(MAX_MEMBERS, 50); + assert.equal(RELAY_MAX, 50); assert.equal(STATE_MAX, 50); assert.equal(MESH_MAX, 50); + let book = emptyBook(); const inUse = new Map(); + for (let i = 1; i <= 50; i++) { const r = admit(book, inUse, null); assert.equal(r.n, i); book = r.book; inUse.set(r.n, hex(crypto.getRandomValues(new Uint8Array(16)))); } + assert.equal(admit(book, inUse, null).refuse, 4001, 'the 51st is refused'); +}); + +function member(config, derived, stats) { + const events = { ready: null, messages: [], presence: 0, fatal: null }; + const session = new Session(derived, config, { emit: (kind, value) => { + if (kind === 'ready') events.ready = value; + if (kind === 'message' && value.from !== '我') events.messages.push(value.text); + if (kind === 'presence') events.presence = value; + } }); + const connection = new RelayConnection(config.relays, { + roomId: config.roomId, + createSocket: (url, list) => { + const ws = new WebSocket(url, list, { origin: 'null' }); + ws.on('message', d => { stats.frames++; stats.bytes += d.length; }); + return ws; + }, + protocols: index => ['pt2', `a${hex(session.d.access)}`, ...(session.resume && connection.lastIndex === index ? [`r${session.resume}`] : [])], + onactive: (ws, index, changed) => session.run(() => { if (changed) session.resetRelay(); session.ws = ws; }), + onmessage: m => { const ws = session.ws; session.run(() => { if (ws === session.ws) return session.incoming(m); }); }, + ondisconnect: () => { session.ws = null; }, + onnotice: () => {}, + onfatal: () => { events.fatal = true; session.halt(); }, + }); + session.connection = connection; connection.start(); + session.tick = setInterval(() => session.run(async () => { + if (session.ws?.readyState === 1) { + { const need = session.helloDue(); if (need) { await session.announce(true, need); session.lastAnnounce = Date.now(); } } + await session.flush(); await session.retryPending(); + } + }), 1600); + return { session, events, leave: () => session.halt() }; +} +const wait = ms => new Promise(r => setTimeout(r, ms)); +async function until(fn, ms, what) { const end = Date.now() + ms; while (Date.now() < end) { if (fn()) return; await wait(100); } assert.fail('timed out: ' + what); } + +test('50 members join a room at once, agree on one safety code, chat, and the 51st is refused', { timeout: 240000 }, async (t) => { + const relay = await nodeRelay(); t.after(relay.stop); + const config = await makeConfig(relay.url, 'fifty'); + const derived = await unlock(config, 'fifty'); + const stats = { frames: 0, bytes: 0 }; + const started = Date.now(); + const members = []; + for (let i = 0; i < MAX_MEMBERS; i++) { members.push(member(config, derived, stats)); await wait(20); } + t.after(() => members.forEach(m => m.leave())); + const progress = setInterval(() => process.env.HR_DEBUG && console.log(' ready', members.filter(m => m.events.ready).length, 'presence', [...new Set(members.map(m => m.events.presence))].join('/'), 'fatal', members.filter(m => m.events.fatal).length, 'frames', stats.frames, 'MB', (stats.bytes/1e6).toFixed(1)), 5000); + if (process.env.HR_DEBUG) setTimeout(() => { const m = members.find(x => !x.events.ready); if (!m) return; const g = m.session.group, pr = m.session.present; + const peers = pr.filter(n => n !== g.me).map(n => [n, g.peers.get(n)]); + console.log(' me', g.me, 'present', pr.length, 'known', peers.filter(([, p]) => p).length, + 'owed', peers.filter(([, p]) => p && p.sent !== g.mine.epoch).map(([n, p]) => `${n}(kpk:${!!p.kpk},ss:${!!p.ss})`).join(' '), + 'noRecv', peers.filter(([, p]) => p && !p.recv).map(([n]) => n).join(','), 'notPQ', peers.filter(([, p]) => p && !(p.v === 32 && p.ss && p.pqIn)).map(([n, p]) => `${n}(v${p.v},ss:${!!p.ss},pqIn:${p.pqIn})`).join(' ')); }, 40000); + await until(() => members.every(m => m.events.ready && m.events.presence === 50), 180000, 'all 50 ready'); + clearInterval(progress); + const joinMs = Date.now() - started, joinBytes = stats.bytes, joinFrames = stats.frames; + const codes = new Set(members.map(m => m.events.ready)); + assert.equal(codes.size, 1, 'one safety code for all 50'); + + // The 51st is turned away by the relay. + const extra = member(config, derived, { frames: 0, bytes: 0 }); + await until(() => extra.events.fatal, 15000, '51st refused'); extra.leave(); + + // Three members talk; everyone else receives each line once. + for (const i of [0, 24, 49]) members[i].session.queue(`来自第 ${i + 1} 位`); + await until(() => members.every((m, i) => [0, 24, 49].filter(j => j !== i).every(j => m.events.messages.includes(`来自第 ${j + 1} 位`))), 60000, 'messages delivered to all'); + assert.ok(members.every(m => new Set(m.events.messages).size === m.events.messages.length), 'no duplicates'); + + // One leaves: the others rekey without them and keep talking. + members[10].leave(); const rest = members.filter((_, i) => i !== 10); + await until(() => rest.every(m => m.events.presence === 49), 30000, 'departure seen'); + rest[0].session.queue('有人离开之后'); + await until(() => rest.slice(1).every(m => m.events.messages.includes('有人离开之后')), 60000, 'after departure'); + console.log(` 50 members: all ready in ${(joinMs / 1000).toFixed(1)} s; join traffic ${(joinBytes / 1e6).toFixed(1)} MB in ${joinFrames} frames over all 50 (${(joinBytes / 50 / 1e3).toFixed(0)} KB each)`); +}); + +function meshMember(config, derived, stats) { + const events = { ready: null, messages: [], presence: 0, fatal: null }; + const session = new Session(derived, config, { emit: (kind, value) => { + if (kind === 'ready') events.ready = value; + if (kind === 'message' && value.from !== '我') events.messages.push(value.text); + if (kind === 'presence') events.presence = value; + } }); + const connection = new MeshConnection(config.relays, { + derived, + createSocket: (url, list) => { const ws = new WebSocket(url, list, { origin: 'null' }); ws.on('message', d => { stats.frames++; stats.bytes += d.length; }); return ws; }, + onhello: n => session.run(() => { session.ws = connection.socket; return session.incoming({ type: 'hello', you: n, resume: '' }); }), + onmembers: list => session.run(() => session.incoming({ type: 'members', members: list })), + onframe: (frame, from) => session.run(() => session.incoming({ ...frame, from })), + onfatal: () => { events.fatal = true; session.halt(); }, + }); + const orig = connection.broadcast.bind(connection); + connection.broadcast = inner => { const kind = inner.k === 'f' ? `f-${inner.f.type}${inner.f.to ? '-to' : ''}` : inner.k; stats.sent[kind] = (stats.sent[kind] ?? 0) + 1; return orig(inner); }; + const origReclaim = connection.claim.bind(connection); connection.claim = () => { stats.sent.claim = (stats.sent.claim ?? 0) + 1; return origReclaim(); }; + session.connection = connection; connection.start(); + session.tick = setInterval(() => session.run(async () => { + if (session.ws?.readyState === 1) { { const need = session.helloDue(); if (need) { await session.announce(true, need); session.lastAnnounce = Date.now(); } } await session.flush(); await session.retryPending(); } + }), 1600); + return { session, events, connection, leave: () => session.halt() }; +} + +// Broadcast relays cannot address frames, so this room costs O(n³) bytes to set +// up. CI runs 20 members; HR_MESH_N=50 runs a full room (minutes, in one process). +test('a decentralized room over /bc/ (broadcast only) settles', { timeout: 400000 }, async (t) => { + const relay = await nodeRelay(); t.after(relay.stop); + const config = await makeConfig(`bc+${relay.url}/bc/`, 'fifty mesh'); + const derived = await unlock(config, 'fifty mesh'); + const stats = { frames: 0, bytes: 0, sent: {} }, started = Date.now(), members = []; + const { monitorEventLoopDelay } = await import('node:perf_hooks'); const lag = monitorEventLoopDelay({ resolution: 20 }); lag.enable(); + const N = Math.min(MAX_MEMBERS, Number(process.env.HR_MESH_N || 20)); + for (let i = 0; i < N; i++) { members.push(meshMember(config, derived, stats)); await wait(20); } + t.after(() => members.forEach(m => m.leave())); + const progress = setInterval(() => process.env.HR_DEBUG && console.log(' mesh ready', members.filter(m => m.events.ready).length, 'numbers', new Set(members.map(m => m.connection.me)).size, 'presence', [...new Set(members.map(m => m.events.presence))].sort((a,b)=>a-b).join('/'), 'fatal', members.filter(m => m.events.fatal).length, 'open', members.filter(m=>m.connection.openCount()).length, 'MB', (stats.bytes / 1e6).toFixed(1), 'lag p99 ms', (lag.percentile(99) / 1e6).toFixed(0), 'max', (lag.max / 1e6).toFixed(0)), 5000); + t.after(() => clearInterval(progress)); + if (process.env.HR_DEBUG) setTimeout(() => { for (const m of members.filter(x => !x.events.ready).slice(0, 2)) { const g = m.session.group, pr = m.session.present; if (!g) { console.log(' no group', m.connection.me, m.connection.settled); continue; } + const peers = pr.filter(n => n !== g.me).map(n => [n, g.peers.get(n)]); + console.log(' me', g.me, 'present', pr.length, 'known', peers.filter(([, p]) => p).length, 'owed', peers.filter(([, p]) => p && p.sent !== g.mine.epoch).length, + 'noRecv', peers.filter(([, p]) => p && !p.recv).length, 'notPQ', peers.filter(([, p]) => p && !(p.v === 32 && p.ss && p.pqIn)).map(([n, p]) => `${n}(ss:${!!p.ss},pqIn:${p.pqIn},kpk:${!!p.kpk})`).slice(0, 6).join(' '), 'unknown', peers.filter(([, p]) => !p).map(([n]) => n).slice(0, 10).join(',')); } }, 45000); + await until(() => members.every(m => m.events.ready && m.events.presence === N), 240000, 'all ready (mesh)'); + const joinMs = Date.now() - started; + assert.equal(new Set(members.map(m => m.events.ready)).size, 1, 'one safety code'); + assert.deepEqual([...new Set(members.map(m => m.connection.me))].length, N, 'distinct numbers'); + members[7].session.queue('去中心化五十人'); + await until(() => members.every((m, i) => i === 7 || m.events.messages.includes('去中心化五十人')), 60000, 'mesh message delivered'); + console.log(` ${N} members over /bc/: all ready in ${(joinMs / 1000).toFixed(1)} s; join traffic ${(stats.bytes / 1e6).toFixed(1)} MB (${(stats.bytes / 50 / 1e3).toFixed(0)} KB each)`); +}); diff --git a/test/html-room.test.mjs b/test/html-room.test.mjs index 7b35e6e..1cc6b95 100644 --- a/test/html-room.test.mjs +++ b/test/html-room.test.mjs @@ -43,7 +43,7 @@ test('HTML Room announcement nonce uniqueness, padding, tamper and room AAD',asy }); async function pair(){ const config=await makeConfig('wss://relay.example'),d=await unlock(config); - const events=[[],[]], a=new Session(d,config,{emit:(...e)=>events[0].push(e)}),b=new Session(d,config,{emit:(...e)=>events[1].push(e)}); + const events=[[],[]], a=new Session(d,config,{emit:(...e)=>events[0].push(e),batchMs:0}),b=new Session(d,config,{emit:(...e)=>events[1].push(e),batchMs:0}); a.group=await Group.create(d.key,d.bind,1);b.group=await Group.create(d.key,d.bind,2);a.present=b.present=[1,2]; // Transport drives the exact Session handlers, preserving the serialized frame order. const frames=[]; diff --git a/test/mesh.test.mjs b/test/mesh.test.mjs index 496c096..81b7095 100644 --- a/test/mesh.test.mjs +++ b/test/mesh.test.mjs @@ -87,12 +87,13 @@ test('members number themselves, settle clashes and hear each other', async (t) const d = await derive(random(32)), sock = bus(); const a = node(d, sock, 0), b = node(d, sock, 0); // both claim 1 at once t.after(() => { a.m.stop(); b.m.stop(); c.m.stop(); }); - await wait(1500); + await wait(3000); const nums = [a.m.me, b.m.me].sort(); assert.deepEqual(nums, [1, 2], 'a clash is settled'); - assert.deepEqual([a.seen.hello, b.seen.hello].sort((x, y) => x.length - y.length), [[1], [1, 2]]); + assert.deepEqual([a.seen.hello, b.seen.hello].map(h => h.length), [1, 1], 'keys are made once, after the number settled'); + assert.deepEqual([...a.seen.hello, ...b.seen.hello].sort(), [1, 2]); const c = node(d, sock, 200); - await wait(1500); + await wait(2500); assert.equal(c.m.me, 3); assert.deepEqual([a.m.me, b.m.me].sort(), [1, 2], 'established members keep their numbers'); assert.deepEqual(a.seen.members.at(-1), [1, 2, 3]); @@ -100,7 +101,7 @@ test('members number themselves, settle clashes and hear each other', async (t) await wait(100); assert.deepEqual(a.seen.frames.at(-1), [3, 'msg']); assert.deepEqual(c.seen.frames.at(-1), [3, 'msg'], 'local echo once a relay took it'); - c.m.stop(); await wait(200); + c.m.stop(); await wait(300); assert.deepEqual(a.seen.members.at(-1), [1, 2], 'bye removes a member at once'); }); @@ -112,9 +113,9 @@ test('a late joiner that claims a taken number gives it up, whatever its node id const late = { seen: { hello: [], members: [], frames: [] } }; late.m = new MeshConnection(['bc+wss://relay.example/bc/'], { derived: d, createSocket: sock, claimDelay: 0, onhello: n => late.seen.hello.push(n), onmembers: () => {}, onframe: () => {} }); late.m.node = '0000000000000000'; late.m.start(); - await wait(5000); + await wait(6000); assert.equal(a.m.me, 1); assert.deepEqual(a.seen.hello, [1]); - assert.equal(late.m.me, 2); assert.deepEqual(late.seen.hello, [1, 2]); + assert.equal(late.m.me, 2); assert.deepEqual(late.seen.hello, [2], 'gave up 1 before making keys for it'); }); test('frames reach every member through any one surviving relay, without duplicates', async (t) => { @@ -122,7 +123,7 @@ test('frames reach every member through any one surviving relay, without duplica const relays = ['bc+wss://one.example/bc/', 'bc+wss://two.example/bc/']; const a = node(d, sock, 0, relays), b = node(d, sock, 300, relays); t.after(() => { a.m.stop(); b.m.stop(); }); - await wait(800); + await wait(2500); a.m.links[0].ws.close(); a.m.links[0].open = false; a.m.links[0].stop(); a.m.socket.send(JSON.stringify({ type: 'key', iv: 'a'.repeat(16), ct: 'b'.repeat(24) })); await wait(100); From 9a627c991777d0af89de4e64435be4608cb3b652 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 19:21:06 -0400 Subject: [PATCH 10/13] fix(installer): check and compare dist/hr.html, not the removed hr.js The single-file site no longer ships hr.js/hr.css; the installer's source check and its CI test now use dist/hr.html. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PCUwNSXQdqfDSugmFo4qmM --- scripts/install.sh | 4 ++-- scripts/test-install.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index 0d0ed1c..e73dfc9 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -177,8 +177,8 @@ if [ "$build" -eq 1 ]; then root mkdir -m 0755 "$source_dir" root tar -xzf "$work/source.tgz" --strip-components=1 -C "$source_dir" fi - if ! { [ -f "$source_dir/Dockerfile" ] && [ -f "$source_dir/dist/hr.js" ]; }; then - fail '源码不完整,缺少 Dockerfile 或 dist/hr.js' + if ! { [ -f "$source_dir/Dockerfile" ] && [ -f "$source_dir/dist/hr.html" ]; }; then + fail '源码不完整,缺少 Dockerfile 或 dist/hr.html' fi build_line=" build: $source_dir" fi diff --git a/scripts/test-install.sh b/scripts/test-install.sh index 91c458b..c25426d 100755 --- a/scripts/test-install.sh +++ b/scripts/test-install.sh @@ -65,8 +65,8 @@ install_once [ "$initial_caddy" = "$(docker compose -p "$project" --project-directory "$work/install" -f "$work/install/compose.yaml" ps -q caddy)" ] url=http://localhost:${HR_TEST_HTTP_PORT:-18089} curl -fsS "$url/healthz" -curl -fsS "$url/hr.js" -o "$work/hr.js" -cmp "$work/hr.js" "$repository/dist/hr.js" +curl -fsS "$url/hr.html" -o "$work/hr.html" +cmp "$work/hr.html" "$repository/dist/hr.html" # Load an old site rule, then prove rerunning the installer replaces the active config. docker run --rm -v "$work:$work" htmlroom-installer:test sh -c ' sed "/reverse_proxy/i\\ respond /installer-stale-config stale" "$1" >"$1.next" From 5cdab09d1a5acffdae0251b02dd253bbd689f7d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 19:34:14 -0400 Subject: [PATCH 11/13] =?UTF-8?q?test:=20never=20hang=20=E2=80=94=20per-te?= =?UTF-8?q?st=20timeout,=20unref'd=20background=20timers,=20safe=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Windows unit job has stayed in progress since the decentralized relay commit. Background intervals (mesh sweep, relay heartbeat) kept test processes alive when a timing assertion failed before cleanup was wired. Timers are now unref'd (no-op in browsers), test cleanup tolerates early failures, awaited closes time out, and npm test sets --test-timeout. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PCUwNSXQdqfDSugmFo4qmM --- dist/_headers | 2 +- dist/hr-template.html | 6 +++--- dist/hr.html | 6 +++--- docs/html-room/release-integrity.json | 8 ++++---- html-room/mesh.js | 18 ++++++++++-------- html-room/relay-connection.js | 14 ++++++++------ html-room/session.js | 1 + package.json | 4 ++-- test/fixtures/hr-relay-suite.mjs | 2 +- test/mesh-relays.test.mjs | 2 +- test/mesh.test.mjs | 10 ++++++---- 11 files changed, 40 insertions(+), 33 deletions(-) diff --git a/dist/_headers b/dist/_headers index d3fd26d..8b6c6ab 100644 --- a/dist/_headers +++ b/dist/_headers @@ -1,5 +1,5 @@ /* - Content-Security-Policy: default-src 'none'; script-src 'sha256-Kg0nJGG+xHIa3LK+UZGnnux+Lsj/oUbwhnGEPDnbc7U='; style-src 'sha256-+cH4mX6DQpXWnU8EWkUQE30KyFp1e6rCFFdGd+i+pzA='; connect-src 'self' wss: ws://localhost:* ws://127.0.0.1:*; img-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none' + Content-Security-Policy: default-src 'none'; script-src 'sha256-t+UQwWWn39gEDqed1s6sSnhEiZjNGlRobivE0cpid5M='; style-src 'sha256-+cH4mX6DQpXWnU8EWkUQE30KyFp1e6rCFFdGd+i+pzA='; connect-src 'self' wss: ws://localhost:* ws://127.0.0.1:*; img-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none' Cache-Control: no-store NEL: {"max_age":0} Referrer-Policy: no-referrer diff --git a/dist/hr-template.html b/dist/hr-template.html index 14b376e..30a645a 100644 --- a/dist/hr-template.html +++ b/dist/hr-template.html @@ -28,8 +28,8 @@ `)}});for(let f of["exit","SIGINT","SIGTERM"])process.once(f,()=>c.kill())}let Z=!1;for(let t of["SIGTERM","SIGINT"])process.on(t,()=>{Z&&process.exit(0),Z=!0,m.close();for(let c of S)c.close(1012,"restart");setTimeout(()=>process.exit(0),S.size?500:0)});return m}typeof require<"u"&&require.main===module&&ke((0,L.readFileSync)(__filename,"utf8"),require("node:path").basename(__filename));0&&(module.exports={serve}); /* --> -加密聊天房间

                                                          加密聊天房间

                                                          正在启动……如果一直停在这里,请看下面的说明。

                                                          页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                                                            +`:""}`,csp:i}}function jr(e,t,r=e.custom===!0){tt(e);let{custom:n,...s}=e;e=r?{...s,custom:!0}:s;let o=Tt(t);return tt({...e,version:Qt(o),relays:o})}var Ds=async e=>btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(e)))));async function qs(e,t,r,n){let s=jr(e,t,n);return{config:s,html:await pe(s,r)}}function js(e){if(typeof e!="string"||e.length>4e5)throw Error("\u4E0D\u662F\u6709\u6548\u7684\u623F\u95F4\u6587\u4EF6");let t=e.match(/\x3Cscript id="pt-room-config" type="application\/json">([\s\S]*?)\x3C\/script>/);if(!t)throw Error("\u4E0D\u662F\u6709\u6548\u7684\u623F\u95F4\u6587\u4EF6");return tt(JSON.parse(t[1]))}async function pe(e,{script:t,style:r,comment:n=""}){if(typeof t!="string"||typeof r!="string"||/\x3C\/script/i.test(t)||/\x3C\/style/i.test(r))throw Error("\u811A\u672C\u6216\u6837\u5F0F\u4E0D\u53EF\u7528");return e.version?tt(e):be(e),Zi(e,t,r,await Ds(t),await Ds(r),n).html}var Ps=document.currentScript?.textContent,Vs=document.querySelector("style")?.textContent,Xi=document.firstChild?.nodeType===Node.COMMENT_NODE?document.firstChild.data:"",tr={script:Ps,style:Vs,comment:Xi},T=e=>document.getElementById(e),Gr=/^https?:$/.test(location.protocol),zr=e=>/[一-鿿]/.test(e?.message??""),er=new Map;function rr(e,t){er.has(e)&&URL.revokeObjectURL(er.get(e));let r=URL.createObjectURL(new Blob([t],{type:"text/html"}));er.set(e,r),T(e).href=r,T(e).hidden=!1}addEventListener("pagehide",()=>{for(let e of er.values())URL.revokeObjectURL(e)});var pt;try{pt=JSON.parse(T("pt-room-config").textContent)}catch{pt=null}function Fi(e){let t=r=>{T("rt-status").textContent=r};T("retarget").hidden=!1,T("rt-relay").value=e.relays.join(" "),T("rt-make").addEventListener("click",async()=>{T("rt-download").hidden=!0,T("rt-make").disabled=!0,t("");try{let r=await qs(e,T("rt-relay").value,tr);if(JSON.stringify(r.config.relays)===JSON.stringify(e.relays)){t("\u5730\u5740\u548C\u73B0\u5728\u4E00\u6837\uFF0C\u4E0D\u7528\u6362\u3002");return}rr("rt-download",r.html),t(`\u65B0\u6587\u4EF6\u5DF2\u751F\u6210\uFF08\u4E2D\u7EE7\uFF1A${r.config.relays.join("\u3001")}\uFF09\u3002\u53E3\u4EE4\u4E0D\u53D8\u3002\u8BF7\u628A\u5B83\u53D1\u7ED9\u6240\u6709\u4EBA\uFF0C\u5927\u5BB6\u90FD\u7528\u65B0\u6587\u4EF6\u6253\u5F00\uFF0C\u518D\u6838\u5BF9\u4E00\u6B21\u5B89\u5168\u7801\u3002`)}catch(r){t(zr(r)?`\u751F\u6210\u5931\u8D25\uFF1A${r.message}\u3002`:"\u751F\u6210\u5931\u8D25\uFF1A\u5730\u5740\u683C\u5F0F\u4E0D\u5BF9\u3002\u4F8B\u5B50\uFF1Awss://relay.example.com")}finally{T("rt-make").disabled=!1}})}function Pr(e,{open:t}){let r=n=>{T("mk-error").textContent=n};T("new-room").hidden=!1,t&&(T("new-room").open=!0,T("new-room").querySelector("summary").hidden=!0),T("mk-relay").value=e.join(" "),e.length||(T("mk-advanced").open=!0),T("mk-gen").addEventListener("click",()=>{T("mk-pass").value=G(Q(32))}),T("mk-mesh").addEventListener("click",()=>{let n=T("mk-relay").value.trim().split(/[\s,]+/).filter(Boolean).map(s=>/^wss?:\/\//.test(s)?`bc+${s.replace(/\/+$/,"")}/bc/`:s);T("mk-relay").value=n.join(" "),r(n.length?"\u5DF2\u6362\u6210\u53BB\u4E2D\u5FC3\u5316\u4E2D\u7EE7\u3002\u8FD8\u53EF\u4EE5\u5728\u540E\u9762\u52A0\u4E0A nostr+wss://\u2026 \u6216 mqtt+wss://\u2026 \u5F53\u5907\u4EFD\u3002":"\u5148\u586B\u4E00\u4E2A\u4E2D\u7EE7\u5730\u5740\uFF0C\u4F8B\u5982 bc+wss://relay.example.com")}),T("maker").addEventListener("submit",async n=>{n.preventDefault(),T("mk-make").disabled=!0,r(""),T("mk-out").hidden=!0;try{let s=Tt(T("mk-relay").value),o=T("mk-pass").value;if(o&&new TextEncoder().encode(o).length>1024)throw Error("\u53E3\u4EE4\u592A\u957F\u4E86");let i=await rn(s,o||void 0,{custom:T("mk-custom").checked});rr("mk-download",await pe(i,tr)),T("mk-out").hidden=!1,T("mk-pass").value="",Gr?(T("mk-link").value=location.origin+location.pathname+qr(i),T("mk-link-row").hidden=!1):(T("mk-link").value="",T("mk-link-row").hidden=!0),r(o?"\u2705 \u623F\u95F4\u5EFA\u597D\u4E86\u3002\u53E3\u4EE4\u8BF7\u6362\u4E00\u79CD\u65B9\u5F0F\u544A\u8BC9\u670B\u53CB\uFF08\u6BD4\u5982\u6253\u7535\u8BDD\uFF09\uFF0C\u522B\u548C\u6587\u4EF6\u4E00\u8D77\u53D1\u3002":"\u2705 \u623F\u95F4\u5EFA\u597D\u4E86\u3002\u6587\u4EF6\u5C31\u662F\u94A5\u5319\uFF0C\u8BF7\u53EA\u53D1\u7ED9\u8981\u804A\u5929\u7684\u4EBA\u3002")}catch(s){r(zr(s)?`\u521B\u5EFA\u5931\u8D25\uFF1A${s.message}`:"\u521B\u5EFA\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u4E2D\u7EE7\u5730\u5740\u540E\u518D\u8BD5\u3002"),T("mk-advanced").open=!0}finally{T("mk-make").disabled=!1}}),T("mk-copy").addEventListener("click",()=>navigator.clipboard?.writeText(T("mk-link").value).then(()=>r("\u5DF2\u590D\u5236\u3002\u94FE\u63A5\u5C31\u662F\u94A5\u5319\uFF0C\u8BF7\u53EA\u53D1\u7ED9\u8981\u804A\u5929\u7684\u4EBA\u3002"),()=>r("\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u4E2D\u94FE\u63A5\u590D\u5236\u3002"))),T("cv-make").addEventListener("click",async()=>{let n=s=>{T("cv-error").textContent=s};T("cv-download").hidden=!0,T("cv-make").disabled=!0,n("");try{let s=T("cv-file").files?.[0];if(!s)throw Error("\u8BF7\u5148\u9009\u62E9\u623F\u95F4\u6587\u4EF6");if(s.size>4e5)throw Error("\u8FD9\u4E2A\u6587\u4EF6\u592A\u5927\u4E86\uFF0C\u4E0D\u662F\u623F\u95F4\u6587\u4EF6");let o;try{o=js(await s.text())}catch{throw Error("\u8BFB\u4E0D\u51FA\u623F\u95F4\u4FE1\u606F\u3002\u5982\u679C\u662F\u5F88\u65E9\u7684\u65E7\u7248\u6587\u4EF6\uFF0C\u8BF7\u91CD\u65B0\u521B\u5EFA\u623F\u95F4")}let i=jr(o,T("cv-relay").value,T("cv-custom").checked);rr("cv-download",await pe(i,tr)),n(`\u65B0\u6587\u4EF6\u5DF2\u751F\u6210\uFF08\u4E2D\u7EE7\uFF1A${i.relays.join("\u3001")}\uFF09\u3002${i.encrypted?"\u53E3\u4EE4\u4E0D\u53D8\u3002":""}\u8BF7\u628A\u5B83\u53D1\u7ED9\u6240\u6709\u4EBA\uFF0C\u5927\u5BB6\u90FD\u7528\u65B0\u6587\u4EF6\u6253\u5F00\uFF0C\u518D\u6838\u5BF9\u4E00\u6B21\u5B89\u5168\u7801\u3002`)}catch(s){n(zr(s)?s.message:"\u751F\u6210\u5931\u8D25\uFF1A\u8BF7\u68C0\u67E5\u4E2D\u7EE7\u5730\u5740\u3002")}finally{T("cv-make").disabled=!1}}),Gr&&pe(e.length?{relays:e}:{},tr).then(n=>{rr("mk-self",n),T("mk-self-row").hidden=!1}).catch(()=>{})}function Yi(){for(let e of["risk","status","preview-help","unlock","retarget","relay-status","presence","safety","messages","chat"])T(e).hidden=!0;document.querySelector("h1").hidden=!0,document.title="HTML Room \xB7 \u52A0\u5BC6\u804A\u5929\u5BA4"}function Wi(e){history.replaceState(null,"",location.pathname+location.search);let t;try{t=Ms(e)}catch{return T("status").hidden=!1,T("status").textContent="\u623F\u95F4\u94FE\u63A5\u6253\u4E0D\u5F00\uFF1A\u94FE\u63A5\u4E0D\u5B8C\u6574\u6216\u5DF2\u635F\u574F\u3002\u8BF7\u8BA9\u5BF9\u65B9\u91CD\u53D1\u94FE\u63A5\uFF0C\u6216\u76F4\u63A5\u7528\u623F\u95F4\u6587\u4EF6\u3002",T("enter").disabled=!0,!1}return Dr(t),!0}var Vr=Ps&&Vs;try{if(pt?.version)tt(pt),Dr(pt),Vr&&(Fi(pt),Pr(pt.relays,{open:!1}));else if(pt){be(pt);let e=pt.relays??(Gr?[location.origin.replace(/^http/,"ws")]:[]);location.hash.startsWith("#hr1=")?(T("new-room").hidden=!0,Wi(location.hash)&&Vr&&Pr(e,{open:!1})):(Yi(),Vr&&Pr(e,{open:!0}),addEventListener("hashchange",()=>{location.hash.startsWith("#hr1=")&&location.reload()}))}else throw Error("bad config")}catch{T("status").hidden=!1,T("status").textContent="\u623F\u95F4\u6587\u4EF6\u6709\u95EE\u9898\uFF0C\u8BF7\u8BA9\u5BF9\u65B9\u91CD\u65B0\u53D1\u4E00\u4EFD\u3002",T("enter").disabled=!0}})(); diff --git a/dist/hr.html b/dist/hr.html index 14b376e..30a645a 100644 --- a/dist/hr.html +++ b/dist/hr.html @@ -28,8 +28,8 @@ `)}});for(let f of["exit","SIGINT","SIGTERM"])process.once(f,()=>c.kill())}let Z=!1;for(let t of["SIGTERM","SIGINT"])process.on(t,()=>{Z&&process.exit(0),Z=!0,m.close();for(let c of S)c.close(1012,"restart");setTimeout(()=>process.exit(0),S.size?500:0)});return m}typeof require<"u"&&require.main===module&&ke((0,L.readFileSync)(__filename,"utf8"),require("node:path").basename(__filename));0&&(module.exports={serve}); /* --> -加密聊天房间

                                                            加密聊天房间

                                                            正在启动……如果一直停在这里,请看下面的说明。

                                                            页面没有启动?微信、QQ 或手机「文件」App 的预览打不开聊天室,请用 Safari、Chrome 等浏览器打开这个文件。

                                                              +`:""}`,csp:i}}function jr(e,t,r=e.custom===!0){tt(e);let{custom:n,...s}=e;e=r?{...s,custom:!0}:s;let o=Tt(t);return tt({...e,version:Qt(o),relays:o})}var Ds=async e=>btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(e)))));async function qs(e,t,r,n){let s=jr(e,t,n);return{config:s,html:await pe(s,r)}}function js(e){if(typeof e!="string"||e.length>4e5)throw Error("\u4E0D\u662F\u6709\u6548\u7684\u623F\u95F4\u6587\u4EF6");let t=e.match(/\x3Cscript id="pt-room-config" type="application\/json">([\s\S]*?)\x3C\/script>/);if(!t)throw Error("\u4E0D\u662F\u6709\u6548\u7684\u623F\u95F4\u6587\u4EF6");return tt(JSON.parse(t[1]))}async function pe(e,{script:t,style:r,comment:n=""}){if(typeof t!="string"||typeof r!="string"||/\x3C\/script/i.test(t)||/\x3C\/style/i.test(r))throw Error("\u811A\u672C\u6216\u6837\u5F0F\u4E0D\u53EF\u7528");return e.version?tt(e):be(e),Zi(e,t,r,await Ds(t),await Ds(r),n).html}var Ps=document.currentScript?.textContent,Vs=document.querySelector("style")?.textContent,Xi=document.firstChild?.nodeType===Node.COMMENT_NODE?document.firstChild.data:"",tr={script:Ps,style:Vs,comment:Xi},T=e=>document.getElementById(e),Gr=/^https?:$/.test(location.protocol),zr=e=>/[一-鿿]/.test(e?.message??""),er=new Map;function rr(e,t){er.has(e)&&URL.revokeObjectURL(er.get(e));let r=URL.createObjectURL(new Blob([t],{type:"text/html"}));er.set(e,r),T(e).href=r,T(e).hidden=!1}addEventListener("pagehide",()=>{for(let e of er.values())URL.revokeObjectURL(e)});var pt;try{pt=JSON.parse(T("pt-room-config").textContent)}catch{pt=null}function Fi(e){let t=r=>{T("rt-status").textContent=r};T("retarget").hidden=!1,T("rt-relay").value=e.relays.join(" "),T("rt-make").addEventListener("click",async()=>{T("rt-download").hidden=!0,T("rt-make").disabled=!0,t("");try{let r=await qs(e,T("rt-relay").value,tr);if(JSON.stringify(r.config.relays)===JSON.stringify(e.relays)){t("\u5730\u5740\u548C\u73B0\u5728\u4E00\u6837\uFF0C\u4E0D\u7528\u6362\u3002");return}rr("rt-download",r.html),t(`\u65B0\u6587\u4EF6\u5DF2\u751F\u6210\uFF08\u4E2D\u7EE7\uFF1A${r.config.relays.join("\u3001")}\uFF09\u3002\u53E3\u4EE4\u4E0D\u53D8\u3002\u8BF7\u628A\u5B83\u53D1\u7ED9\u6240\u6709\u4EBA\uFF0C\u5927\u5BB6\u90FD\u7528\u65B0\u6587\u4EF6\u6253\u5F00\uFF0C\u518D\u6838\u5BF9\u4E00\u6B21\u5B89\u5168\u7801\u3002`)}catch(r){t(zr(r)?`\u751F\u6210\u5931\u8D25\uFF1A${r.message}\u3002`:"\u751F\u6210\u5931\u8D25\uFF1A\u5730\u5740\u683C\u5F0F\u4E0D\u5BF9\u3002\u4F8B\u5B50\uFF1Awss://relay.example.com")}finally{T("rt-make").disabled=!1}})}function Pr(e,{open:t}){let r=n=>{T("mk-error").textContent=n};T("new-room").hidden=!1,t&&(T("new-room").open=!0,T("new-room").querySelector("summary").hidden=!0),T("mk-relay").value=e.join(" "),e.length||(T("mk-advanced").open=!0),T("mk-gen").addEventListener("click",()=>{T("mk-pass").value=G(Q(32))}),T("mk-mesh").addEventListener("click",()=>{let n=T("mk-relay").value.trim().split(/[\s,]+/).filter(Boolean).map(s=>/^wss?:\/\//.test(s)?`bc+${s.replace(/\/+$/,"")}/bc/`:s);T("mk-relay").value=n.join(" "),r(n.length?"\u5DF2\u6362\u6210\u53BB\u4E2D\u5FC3\u5316\u4E2D\u7EE7\u3002\u8FD8\u53EF\u4EE5\u5728\u540E\u9762\u52A0\u4E0A nostr+wss://\u2026 \u6216 mqtt+wss://\u2026 \u5F53\u5907\u4EFD\u3002":"\u5148\u586B\u4E00\u4E2A\u4E2D\u7EE7\u5730\u5740\uFF0C\u4F8B\u5982 bc+wss://relay.example.com")}),T("maker").addEventListener("submit",async n=>{n.preventDefault(),T("mk-make").disabled=!0,r(""),T("mk-out").hidden=!0;try{let s=Tt(T("mk-relay").value),o=T("mk-pass").value;if(o&&new TextEncoder().encode(o).length>1024)throw Error("\u53E3\u4EE4\u592A\u957F\u4E86");let i=await rn(s,o||void 0,{custom:T("mk-custom").checked});rr("mk-download",await pe(i,tr)),T("mk-out").hidden=!1,T("mk-pass").value="",Gr?(T("mk-link").value=location.origin+location.pathname+qr(i),T("mk-link-row").hidden=!1):(T("mk-link").value="",T("mk-link-row").hidden=!0),r(o?"\u2705 \u623F\u95F4\u5EFA\u597D\u4E86\u3002\u53E3\u4EE4\u8BF7\u6362\u4E00\u79CD\u65B9\u5F0F\u544A\u8BC9\u670B\u53CB\uFF08\u6BD4\u5982\u6253\u7535\u8BDD\uFF09\uFF0C\u522B\u548C\u6587\u4EF6\u4E00\u8D77\u53D1\u3002":"\u2705 \u623F\u95F4\u5EFA\u597D\u4E86\u3002\u6587\u4EF6\u5C31\u662F\u94A5\u5319\uFF0C\u8BF7\u53EA\u53D1\u7ED9\u8981\u804A\u5929\u7684\u4EBA\u3002")}catch(s){r(zr(s)?`\u521B\u5EFA\u5931\u8D25\uFF1A${s.message}`:"\u521B\u5EFA\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u4E2D\u7EE7\u5730\u5740\u540E\u518D\u8BD5\u3002"),T("mk-advanced").open=!0}finally{T("mk-make").disabled=!1}}),T("mk-copy").addEventListener("click",()=>navigator.clipboard?.writeText(T("mk-link").value).then(()=>r("\u5DF2\u590D\u5236\u3002\u94FE\u63A5\u5C31\u662F\u94A5\u5319\uFF0C\u8BF7\u53EA\u53D1\u7ED9\u8981\u804A\u5929\u7684\u4EBA\u3002"),()=>r("\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u4E2D\u94FE\u63A5\u590D\u5236\u3002"))),T("cv-make").addEventListener("click",async()=>{let n=s=>{T("cv-error").textContent=s};T("cv-download").hidden=!0,T("cv-make").disabled=!0,n("");try{let s=T("cv-file").files?.[0];if(!s)throw Error("\u8BF7\u5148\u9009\u62E9\u623F\u95F4\u6587\u4EF6");if(s.size>4e5)throw Error("\u8FD9\u4E2A\u6587\u4EF6\u592A\u5927\u4E86\uFF0C\u4E0D\u662F\u623F\u95F4\u6587\u4EF6");let o;try{o=js(await s.text())}catch{throw Error("\u8BFB\u4E0D\u51FA\u623F\u95F4\u4FE1\u606F\u3002\u5982\u679C\u662F\u5F88\u65E9\u7684\u65E7\u7248\u6587\u4EF6\uFF0C\u8BF7\u91CD\u65B0\u521B\u5EFA\u623F\u95F4")}let i=jr(o,T("cv-relay").value,T("cv-custom").checked);rr("cv-download",await pe(i,tr)),n(`\u65B0\u6587\u4EF6\u5DF2\u751F\u6210\uFF08\u4E2D\u7EE7\uFF1A${i.relays.join("\u3001")}\uFF09\u3002${i.encrypted?"\u53E3\u4EE4\u4E0D\u53D8\u3002":""}\u8BF7\u628A\u5B83\u53D1\u7ED9\u6240\u6709\u4EBA\uFF0C\u5927\u5BB6\u90FD\u7528\u65B0\u6587\u4EF6\u6253\u5F00\uFF0C\u518D\u6838\u5BF9\u4E00\u6B21\u5B89\u5168\u7801\u3002`)}catch(s){n(zr(s)?s.message:"\u751F\u6210\u5931\u8D25\uFF1A\u8BF7\u68C0\u67E5\u4E2D\u7EE7\u5730\u5740\u3002")}finally{T("cv-make").disabled=!1}}),Gr&&pe(e.length?{relays:e}:{},tr).then(n=>{rr("mk-self",n),T("mk-self-row").hidden=!1}).catch(()=>{})}function Yi(){for(let e of["risk","status","preview-help","unlock","retarget","relay-status","presence","safety","messages","chat"])T(e).hidden=!0;document.querySelector("h1").hidden=!0,document.title="HTML Room \xB7 \u52A0\u5BC6\u804A\u5929\u5BA4"}function Wi(e){history.replaceState(null,"",location.pathname+location.search);let t;try{t=Ms(e)}catch{return T("status").hidden=!1,T("status").textContent="\u623F\u95F4\u94FE\u63A5\u6253\u4E0D\u5F00\uFF1A\u94FE\u63A5\u4E0D\u5B8C\u6574\u6216\u5DF2\u635F\u574F\u3002\u8BF7\u8BA9\u5BF9\u65B9\u91CD\u53D1\u94FE\u63A5\uFF0C\u6216\u76F4\u63A5\u7528\u623F\u95F4\u6587\u4EF6\u3002",T("enter").disabled=!0,!1}return Dr(t),!0}var Vr=Ps&&Vs;try{if(pt?.version)tt(pt),Dr(pt),Vr&&(Fi(pt),Pr(pt.relays,{open:!1}));else if(pt){be(pt);let e=pt.relays??(Gr?[location.origin.replace(/^http/,"ws")]:[]);location.hash.startsWith("#hr1=")?(T("new-room").hidden=!0,Wi(location.hash)&&Vr&&Pr(e,{open:!1})):(Yi(),Vr&&Pr(e,{open:!0}),addEventListener("hashchange",()=>{location.hash.startsWith("#hr1=")&&location.reload()}))}else throw Error("bad config")}catch{T("status").hidden=!1,T("status").textContent="\u623F\u95F4\u6587\u4EF6\u6709\u95EE\u9898\uFF0C\u8BF7\u8BA9\u5BF9\u65B9\u91CD\u65B0\u53D1\u4E00\u4EFD\u3002",T("enter").disabled=!0}})(); diff --git a/docs/html-room/release-integrity.json b/docs/html-room/release-integrity.json index 8c39141..9a868b2 100644 --- a/docs/html-room/release-integrity.json +++ b/docs/html-room/release-integrity.json @@ -4,13 +4,13 @@ "esbuild": "0.28.2", "artifacts": { "html-room-template.html": { - "bytes": 139506, - "sha256": "47bd9dbbe2ce796bd3d03cf11200a865983524c4ae875d3525c03ca59105739c", + "bytes": 139626, + "sha256": "2fd303b6275ff62aec19746e94c718548d025f8795d409d0b5068d070fc3e713", "csp": "([^" } }, - "scriptHashBase64": "Kg0nJGG+xHIa3LK+UZGnnux+Lsj/oUbwhnGEPDnbc7U=", - "scriptHashHex": "2a0d272461bec4721adcb2be5191a79eec7e2ec8ffa146f08671843c39db73b5", + "scriptHashBase64": "t+UQwWWn39gEDqed1s6sSnhEiZjNGlRobivE0cpid5M=", + "scriptHashHex": "b7e510c165a7dfd8040ea79dd6ceac4a78448998cd1a54686e2bc4d1ca627793", "styleHashBase64": "+cH4mX6DQpXWnU8EWkUQE30KyFp1e6rCFFdGd+i+pzA=", "serverCommentSha256": "971a532473d0c7d039dd0617391a736bd4057fab488e2249d612188a340195fe" } diff --git a/html-room/mesh.js b/html-room/mesh.js index 6dbdd96..25b253d 100644 --- a/html-room/mesh.js +++ b/html-room/mesh.js @@ -41,6 +41,8 @@ const b64 = u8 => { let s = ''; for (let i = 0; i < u8.length; i += 4096) s += S const unb64 = s => Uint8Array.from(atob(s), c => c.charCodeAt(0)); const aad = topic => enc.encode(`private-talk/html-room/v3/mesh|${topic}`); const FRAME_TYPES = ['key', 'msg', 'seen']; +// Background timers must not keep a Node process (tests, tools) alive; browsers ignore this. +const bg = t => (t?.unref?.(), t); export async function sealFrame(d, inner) { const iv = random(12); @@ -72,7 +74,7 @@ class Link { let ws; try { ws = this.ws = this.mesh.createSocket(this.target(), this.protocols()); } catch { this.fail(); return; } ws.binaryType = 'arraybuffer'; - this.timeout = setTimeout(() => this.fail(ws), 10000); + this.timeout = bg(setTimeout(() => this.fail(ws), 10000)); ws.onopen = () => { if (ws === this.ws) this.opened(); }; ws.onmessage = e => { if (ws === this.ws && !this.mesh.stopped) this.data(e.data); }; ws.onclose = ws.onerror = () => this.fail(ws); @@ -85,7 +87,7 @@ class Link { try { ws?.close(); } catch {} if (was) this.mesh.linkChanged(); clearTimeout(this.retry); - this.retry = setTimeout(() => this.connect(), this.backoff); + this.retry = bg(setTimeout(() => this.connect(), this.backoff)); this.backoff = Math.min(this.backoff * 2, 30000); } stop() { clearTimeout(this.timeout); clearTimeout(this.retry); clearInterval(this.keepalive); const ws = this.ws; this.ws = null; this.open = false; try { ws?.close(1000); } catch {} } @@ -159,7 +161,7 @@ class MqttLink extends Link { } else if (p.type === 9) { // SUBACK if (p.body[2] === 0x80) { this.notice('这个 MQTT 服务器拒绝了订阅,可换一个'); this.fail(); return; } this.ready(); - this.keepalive = setInterval(() => { try { this.ws?.send(Uint8Array.of(0xc0, 0)); } catch {} }, 30000); + this.keepalive = bg(setInterval(() => { try { this.ws?.send(Uint8Array.of(0xc0, 0)); } catch {} }, 30000)); } else if (p.type === 3) { // PUBLISH const tl = (p.body[0] << 8) | p.body[1], skip = 2 + tl + ((p.flags >> 1) & 3 ? 2 : 0); if (dec.decode(p.body.subarray(2, 2 + tl)) === this.topic()) this.mesh.receive(dec.decode(p.body.subarray(skip))); @@ -184,8 +186,8 @@ export class MeshConnection { } start() { for (const link of this.links) link.connect(); - this.claimTimer = setTimeout(() => this.claim(), this.claimDelay); - this.timer = setInterval(() => this.sweep(), 1000); + this.claimTimer = bg(setTimeout(() => this.claim(), this.claimDelay)); + this.timer = bg(setInterval(() => this.sweep(), 1000)); this.linkChanged(); } stop() { @@ -242,7 +244,7 @@ export class MeshConnection { } answerSoon() { if (!this.me || this.answer) return; - this.answer = setTimeout(() => { this.answer = null; this.lastHb = 0; this.sweep(); }, 50 + Math.random() * 250); + this.answer = bg(setTimeout(() => { this.answer = null; this.lastHb = 0; this.sweep(); }, 50 + Math.random() * 250)); } claim() { if (this.stopped) return; @@ -255,10 +257,10 @@ export class MeshConnection { const n = free[Math.floor(Math.random() * free.length)]; this.me = n; this.claimedAt = now; this.lastHb = 0; this.settled = false; clearTimeout(this.settleTimer); - this.settleTimer = setTimeout(() => { + this.settleTimer = bg(setTimeout(() => { if (this.stopped || this.me !== n) return; this.settled = true; this.onhello(n); this.reconcile(true); - }, SETTLE_MS); + }, SETTLE_MS)); this.sweep(); } *live() { const now = this.now(); for (const e of this.peers) if (now - e[1].seen <= ALIVE_MS) yield e; } diff --git a/html-room/relay-connection.js b/html-room/relay-connection.js index 3eaeed2..dc35423 100644 --- a/html-room/relay-connection.js +++ b/html-room/relay-connection.js @@ -1,4 +1,6 @@ import { MAX_MEMBERS } from '../src/limits.js'; +// Background timers must not keep a Node process (tests, tools) alive; browsers ignore this. +const bg = t => (t?.unref?.(), t); // Availability and priority only. This layer never logs room URLs or credentials. export class RelayConnection { constructor(relays, { roomId, protocols, onactive, onmessage, onnotice, onfatal, ondisconnect, @@ -8,10 +10,10 @@ export class RelayConnection { } start() { this.connect(0); - this.probeTimer = setInterval(() => { + this.probeTimer = bg(setInterval(() => { if (!this.stopped && this.active?.ready && this.active.index > 0 && !this.probe) this.connect(0, true); - }, 60000); - this.heartbeat = setInterval(() => { + }, 60000)); + this.heartbeat = bg(setInterval(() => { const r = this.active; if (!r?.ready) return; if (Date.now() - r.lastPong > 65000) { this.fail(r); return; } @@ -19,7 +21,7 @@ export class RelayConnection { r.lastPing = Date.now(); try { r.ws.send(JSON.stringify({ type: 'ping' })); } catch { this.fail(r); } } - }, 1000); + }, 1000)); } connect(index, probe = false) { if (this.stopped) return; @@ -27,7 +29,7 @@ export class RelayConnection { if (probe) this.probe = r; else this.pending = r; try { r.ws = this.createSocket(`${this.relays[index]}/hr/${this.roomId}`, this.protocols(index)); } catch { this.fail(r); return; } - r.timeout = setTimeout(() => this.fail(r), 10000); + r.timeout = bg(setTimeout(() => this.fail(r), 10000)); r.ws.onerror = () => { if (!r.retired && !r.probe) this.onnotice('连不上中继(可能是网络、证书或权限问题),正在重试……'); }; @@ -81,7 +83,7 @@ export class RelayConnection { // An in-flight probe must not compete with the next ordered retry. this.retire(this.probe); this.probe = null; clearTimeout(this.retry); - this.retry = setTimeout(() => this.connect((r.index + 1) % this.relays.length), this.backoff); + this.retry = bg(setTimeout(() => this.connect((r.index + 1) % this.relays.length), this.backoff)); this.backoff = Math.min(this.backoff * 2, 30000); if (code !== 1006) this.onnotice('连接断了,正在换一个中继重试……'); } diff --git a/html-room/session.js b/html-room/session.js index 079df6f..64f41da 100644 --- a/html-room/session.js +++ b/html-room/session.js @@ -35,6 +35,7 @@ export class Session { scheduleAnnounce(){ if(this.announceTimer||this.stopped)return; this.announceTimer=setTimeout(()=>{this.announceTimer=null;this.run(async()=>{await this.announce();await this.update();});},this.batchMs); + this.announceTimer?.unref?.(); } // A hello (ask everyone to answer) is due: soon while someone present is // still unknown, otherwise at the slow background pace. diff --git a/package.json b/package.json index 4ba8b05..dbb2e6a 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,13 @@ "build": "node scripts/build-html-room.mjs template", "build:pages": "node scripts/build-html-room.mjs pages", "build:check": "node scripts/build-html-room.mjs template --check", - "test": "node --test", + "test": "node --test --test-timeout=300000", "test:browser": "node browser-tests/htmlroom.mjs && node browser-tests/html-room-preview.mjs && node browser-tests/multi-relay.mjs && node browser-tests/retarget.mjs && node browser-tests/mesh.mjs && node browser-tests/custom-relay.mjs && node browser-tests/generations.mjs && node browser-tests/file-server.mjs", "test:retarget": "node browser-tests/retarget.mjs", "test:failover": "node browser-tests/multi-relay.mjs", "test:pages": "npm run build:pages && node browser-tests/pages.mjs", "test:preview": "node browser-tests/html-room-preview.mjs", - "test:worker": "HR_WORKER_TEST=1 node --test test/hr-worker.test.mjs", + "test:worker": "HR_WORKER_TEST=1 node --test --test-timeout=300000 test/hr-worker.test.mjs", "html-room:generate": "node scripts/build-html-room.mjs", "html-room:check": "node scripts/check-html-room.mjs", "deploy": "npm run build:check && wrangler deploy", diff --git a/test/fixtures/hr-relay-suite.mjs b/test/fixtures/hr-relay-suite.mjs index 3994834..6bfbe34 100644 --- a/test/fixtures/hr-relay-suite.mjs +++ b/test/fixtures/hr-relay-suite.mjs @@ -160,7 +160,7 @@ test('/bc/ forwards text frames to the other members of the topic only', async ( assert.deepEqual(got, ['sealed-frame', 'sealed-frame']); await new Promise((r) => setTimeout(r, 150)); assert.deepEqual(wrong, []); - const closed = new Promise((r) => b.on('close', r)); b.send('x'.repeat(16001)); + const closed = new Promise((r, f) => { b.on('close', r); setTimeout(() => f(new Error('no close within 5 s')), 5000); }); b.send('x'.repeat(16001)); await closed; await assert.rejects(open(topic, 'https://evil.example'), (e) => e.status === 403); for (const ws of [a, c, d]) ws.close(); diff --git a/test/mesh-relays.test.mjs b/test/mesh-relays.test.mjs index b07ef7f..1dee5fb 100644 --- a/test/mesh-relays.test.mjs +++ b/test/mesh-relays.test.mjs @@ -12,7 +12,7 @@ const wait = ms => new Promise(r => setTimeout(r, ms)); const createSocket = (url, protocols) => new WebSocket(url, protocols, { origin: 'null' }); const open = (url, opts = {}) => new Promise((resolve, reject) => { const ws = new WebSocket(url, opts); ws.once('open', () => resolve(ws)); ws.once('error', reject); ws.once('unexpected-response', (_, res) => reject(Object.assign(Error('http ' + res.statusCode), { status: res.statusCode }))); }); const next = ws => new Promise(r => ws.once('message', (d, bin) => r(bin ? d : d.toString()))); -const closed = ws => new Promise(r => ws.once('close', code => r(code))); +const closed = ws => Promise.race([new Promise(r => ws.once('close', code => r(code))), new Promise((_, f) => setTimeout(() => f(new Error('no close within 5 s')), 5000))]); const TOPIC = 'ab'.repeat(16); async function broadcastContract(base, label) { diff --git a/test/mesh.test.mjs b/test/mesh.test.mjs index 81b7095..7c218f0 100644 --- a/test/mesh.test.mjs +++ b/test/mesh.test.mjs @@ -85,14 +85,15 @@ function node(d, createSocket, claimDelay, relays = ['bc+wss://relay.example/bc/ test('members number themselves, settle clashes and hear each other', async (t) => { const d = await derive(random(32)), sock = bus(); + const started = []; t.after(() => started.forEach(x => x.m.stop())); const a = node(d, sock, 0), b = node(d, sock, 0); // both claim 1 at once - t.after(() => { a.m.stop(); b.m.stop(); c.m.stop(); }); + started.push(a, b); await wait(3000); const nums = [a.m.me, b.m.me].sort(); assert.deepEqual(nums, [1, 2], 'a clash is settled'); assert.deepEqual([a.seen.hello, b.seen.hello].map(h => h.length), [1, 1], 'keys are made once, after the number settled'); assert.deepEqual([...a.seen.hello, ...b.seen.hello].sort(), [1, 2]); - const c = node(d, sock, 200); + const c = node(d, sock, 200); started.push(c); await wait(2500); assert.equal(c.m.me, 3); assert.deepEqual([a.m.me, b.m.me].sort(), [1, 2], 'established members keep their numbers'); @@ -107,12 +108,13 @@ test('members number themselves, settle clashes and hear each other', async (t) test('a late joiner that claims a taken number gives it up, whatever its node id', async (t) => { const d = await derive(random(32)), sock = bus(); - const a = node(d, sock, 0); t.after(() => { a.m.stop(); late.m.stop(); }); + const stops = []; t.after(() => stops.forEach(f => f())); + const a = node(d, sock, 0); stops.push(() => a.m.stop()); await wait(300); // Joins deaf: claims at once, before hearing anyone, and with the lowest possible node id. const late = { seen: { hello: [], members: [], frames: [] } }; late.m = new MeshConnection(['bc+wss://relay.example/bc/'], { derived: d, createSocket: sock, claimDelay: 0, onhello: n => late.seen.hello.push(n), onmembers: () => {}, onframe: () => {} }); - late.m.node = '0000000000000000'; late.m.start(); + late.m.node = '0000000000000000'; late.m.start(); stops.push(() => late.m.stop()); await wait(6000); assert.equal(a.m.me, 1); assert.deepEqual(a.seen.hello, [1]); assert.equal(late.m.me, 2); assert.deepEqual(late.seen.hello, [2], 'gave up 1 before making keys for it'); From a8b5b2c1b99d492af66a0b137be9eb043ad59d48 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 19:44:06 -0400 Subject: [PATCH 12/13] ci: report failing unit tests as an annotation Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PCUwNSXQdqfDSugmFo4qmM --- .github/workflows/test.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55dd57a..3cb06ea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,17 @@ jobs: node-version: 22 cache: npm - run: npm ci --ignore-scripts - - run: npm test + - name: npm test + shell: bash + run: npm test 2>&1 | tee test-output.txt; exit ${PIPESTATUS[0]} + # Failing test names and errors as an annotation, readable from the API + # without downloading logs. + - name: Report failing tests + if: failure() + shell: bash + run: | + msg=$(grep -nE "^not ok|^# Subtest|^ +(error|expected|actual|location|code):" test-output.txt | grep -B1 -A6 "not ok" | head -80 | sed 's/%/%25/g' | awk '{printf "%s%%0A", $0}') + echo "::error title=npm test failures::${msg}" - run: npm run build:check browser: runs-on: ubuntu-latest From b934a0390729e81233ee336e202acddab7c94302 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 19:52:25 -0400 Subject: [PATCH 13/13] test: Windows-safe fixture paths URL.pathname gives /D:/... on Windows, so the relay fixtures never started (and the Go build ran in a missing directory). Use fileURLToPath/join and os.tmpdir(); skip the POSIX fake-cloudflared test on Windows. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PCUwNSXQdqfDSugmFo4qmM --- test/fixtures/mesh-relays.mjs | 11 +++++++---- test/mesh-relays.test.mjs | 5 +++-- test/quickstart.test.mjs | 3 ++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/test/fixtures/mesh-relays.mjs b/test/fixtures/mesh-relays.mjs index bcffb6b..ff53496 100644 --- a/test/fixtures/mesh-relays.mjs +++ b/test/fixtures/mesh-relays.mjs @@ -2,13 +2,16 @@ import { spawn, execFileSync } from 'node:child_process'; import { createServer } from 'node:http'; import { existsSync, mkdtempSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { WebSocketServer, createWebSocketStream } from 'ws'; import { schnorr } from '@noble/curves/secp256k1'; import { sha256 } from '@noble/hashes/sha256'; import { Aedes } from 'aedes'; -const root = new URL('../../', import.meta.url).pathname; +// fileURLToPath, not .pathname: on Windows that would be /D:/... and nothing starts. +const root = fileURLToPath(new URL('../../', import.meta.url)); const hex = u8 => Buffer.from(u8).toString('hex'); async function healthy(url) { for (let i = 0; i < 80; i++) { try { if ((await fetch(url)).ok) return; } catch {} await new Promise(r => setTimeout(r, 100)); } @@ -19,7 +22,7 @@ const listen = server => new Promise(r => server.listen(0, '127.0.0.1', () => r( // This project's Node server (classic /hr/ and broadcast /bc/). export async function nodeRelay(env = {}) { const port = 20000 + Math.floor(Math.random() * 20000); - const child = spawn(process.execPath, [root + 'server/server.mjs'], { env: { ...process.env, PORT: String(port), HOST: '127.0.0.1', ...env }, stdio: 'ignore' }); + const child = spawn(process.execPath, [join(root, 'server', 'server.mjs')], { env: { ...process.env, PORT: String(port), HOST: '127.0.0.1', ...env }, stdio: 'ignore' }); await healthy(`http://127.0.0.1:${port}/healthz`); return { port, url: `ws://127.0.0.1:${port}`, stop: () => new Promise(r => { if (child.exitCode !== null || child.signalCode) return r(); child.once('exit', r); child.kill('SIGTERM'); }) }; } @@ -28,8 +31,8 @@ let goBinary; export function goAvailable() { try { execFileSync('go', ['version'], { stdio: 'ignore' }); return true; } catch { return false; } } export async function goRelay(args = []) { if (!goBinary) { - goBinary = mkdtempSync(tmpdir() + '/hr-go-') + '/htmlroom-relay' + (process.platform === 'win32' ? '.exe' : ''); - execFileSync('go', ['build', '-o', goBinary, '.'], { cwd: root + 'relay/go', stdio: 'ignore', env: { ...process.env, CGO_ENABLED: '0' } }); + goBinary = join(mkdtempSync(join(tmpdir(), 'hr-go-')), 'htmlroom-relay' + (process.platform === 'win32' ? '.exe' : '')); + execFileSync('go', ['build', '-o', goBinary, '.'], { cwd: join(root, 'relay', 'go'), stdio: 'ignore', env: { ...process.env, CGO_ENABLED: '0' } }); } const port = 20000 + Math.floor(Math.random() * 20000); const child = spawn(goBinary, ['-listen', `127.0.0.1:${port}`, ...args], { stdio: ['ignore', 'pipe', 'pipe'] }); diff --git a/test/mesh-relays.test.mjs b/test/mesh-relays.test.mjs index 1dee5fb..a536b32 100644 --- a/test/mesh-relays.test.mjs +++ b/test/mesh-relays.test.mjs @@ -47,9 +47,10 @@ test('Go relay forwards the same way, with optional origin allowlist', { skip: ! (await open(`${strict.url}/bc/${TOPIC}`, { origin: 'null' })).close(); }); -test('Go relay -tunnel prints the relay line from cloudflared output', { skip: !goAvailable() && 'go not installed' }, async (t) => { +test('Go relay -tunnel prints the relay line from cloudflared output', { skip: (!goAvailable() && 'go not installed') || (process.platform === 'win32' && 'POSIX fake cloudflared') }, async (t) => { const { writeFileSync, chmodSync, mkdtempSync } = await import('node:fs'); - const dir = mkdtempSync('/tmp/hr-cf-'), fake = dir + '/cloudflared'; + const { tmpdir } = await import('node:os'); + const dir = mkdtempSync(tmpdir() + '/hr-cf-'), fake = dir + '/cloudflared'; writeFileSync(fake, '#!/bin/sh\necho "INF | https://quiet-river-1234.trycloudflare.com |" >&2\nsleep 30\n'); chmodSync(fake, 0o755); const relay = await goRelay(['-tunnel', '-cloudflared', fake]); t.after(relay.stop); for (let i = 0; i < 50 && !relay.output().includes('bc+wss://'); i++) await wait(100); diff --git a/test/quickstart.test.mjs b/test/quickstart.test.mjs index 840388d..48d3099 100644 --- a/test/quickstart.test.mjs +++ b/test/quickstart.test.mjs @@ -6,8 +6,9 @@ import { createServer } from 'node:http'; import { createHash } from 'node:crypto'; import { mkdtempSync } from 'node:fs'; import { tmpdir } from 'node:os'; +import { fileURLToPath } from 'node:url'; -const script = new URL('../scripts/relay-quickstart.sh', import.meta.url).pathname; +const script = fileURLToPath(new URL('../scripts/relay-quickstart.sh', import.meta.url)); // A POSIX sh script (Linux, macOS, BSD, WSL, Termux, OpenWrt); Windows users get the .exe. const skip = process.platform === 'win32' && 'POSIX shell script'; // Async: the fake release server below runs in this same process.