From dfd496c1a95953e8677d025f480ddee906fb509a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 15:43:27 -0400 Subject: [PATCH 1/5] 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 2/5] 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 4/5] 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 5/5] 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());