filenymous 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/filenymous.js +14 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,8 @@ filenymous upload <fichier...> [--ttl 1h] [--max-downloads 1] [--password X] \
|
|
|
30
30
|
[--webhook URL] [--webhook-no-link] [--allow-empty] [--json] [-q]
|
|
31
31
|
```
|
|
32
32
|
- Le lien est écrit sur la **sortie standard**, la progression sur stderr. Avec `--json`, la sortie standard reçoit un événement JSON par ligne.
|
|
33
|
+
- `--stream` : le destinataire web peut lire la vidéo ou l'audio pendant le transfert (« Partage streamé »).
|
|
34
|
+
- `--expires <date ISO>` : le lien expire à cette date (liée à la clé et au lien) ; l'expéditeur cesse alors de servir. Nécessite un destinataire récent (web à jour ou CLI ≥ 0.3).
|
|
33
35
|
- `--password` : un mot de passe est demandé au destinataire. Il n'est jamais envoyé : c'est une preuve HMAC, et une dérivation PBKDF2 est mêlée à la clé AES. Préférez la variable d'environnement `FILENYMOUS_PASSWORD`, qui n'apparaît pas dans l'historique du shell ni dans les logs de CI.
|
|
34
36
|
- Codes de sortie :
|
|
35
37
|
- `0` : au moins un téléchargement terminé (ou `--allow-empty`)
|
|
@@ -74,6 +76,8 @@ filenymous upload <file...> [--ttl 1h] [--max-downloads 1] [--password X] \
|
|
|
74
76
|
[--webhook URL] [--webhook-no-link] [--allow-empty] [--json] [-q]
|
|
75
77
|
```
|
|
76
78
|
- The link goes to **stdout** and progress to stderr. With `--json`, stdout carries JSON lines events.
|
|
79
|
+
- `--stream`: web recipients can play audio/video while it downloads ("Streamed sharing").
|
|
80
|
+
- `--expires <ISO date>`: the link expires at that date (bound into the key and the link); the sender stops serving afterwards. Requires an up-to-date receiver (current web app or CLI ≥ 0.3).
|
|
77
81
|
- Prefer `FILENYMOUS_PASSWORD` over `--password`, so the password stays out of shell history and CI logs.
|
|
78
82
|
- Exit codes:
|
|
79
83
|
- `0`: at least one completed download (or `--allow-empty`)
|
package/dist/filenymous.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import{createRequire as __cr}from'node:module';const require=__cr(import.meta.url);
|
|
3
|
-
import{parseArgs as et}from"node:util";import{createInterface as tt}from"node:readline";import{resolve as Ae}from"node:path";import{RTCPeerConnection as xe,RTCSessionDescription as Ce,RTCIceCandidate as Re,RTCDataChannel as _e}from"node-datachannel/polyfill";import Ie from"ws";function De(i){return i.replace(/^a=/,"")}var B=class extends xe{pendingLocal=0;remoteSet=!1;held=[];userHandler=null;constructor(e){super(e);let t=r=>{let n=r.candidate,s={candidate:n?{candidate:De(n.candidate),sdpMid:n.sdpMid??"0",sdpMLineIndex:n.sdpMLineIndex??0}:null};this.pendingLocal>0||!this.remoteSet?this.held.push(s):this.userHandler?.(s)};Object.defineProperty(this,"onicecandidate",{configurable:!0,get:()=>this.userHandler?t:null,set:r=>{this.userHandler=r}})}async createOffer(e){return this.pendingLocal=1,super.createOffer()}async createAnswer(e){return this.pendingLocal=1,super.createAnswer()}async setLocalDescription(e){this.pendingLocal=1;try{await super.setLocalDescription(e)}finally{setTimeout(()=>{this.pendingLocal=0,this.flush()},0)}}async setRemoteDescription(e){await super.setRemoteDescription(e),this.remoteSet=!0,this.flush()}flush(){if(!(this.pendingLocal>0||!this.remoteSet))for(let e of this.held.splice(0))this.userHandler?.(e)}};function V(i){let e=globalThis;e.RTCPeerConnection=B,e.RTCSessionDescription??=Ce,e.RTCIceCandidate??=Re,e.RTCDataChannel??=_e,e.WebSocket||(e.WebSocket=Ie),e.__FILENYMOUS_SIGNAL_URL__=i}import{open as Le,stat as Be}from"node:fs/promises";import{basename as Oe,extname as Me}from"node:path";var Fe={".zip":"application/zip",".gz":"application/gzip",".tgz":"application/gzip",".tar":"application/x-tar",".txt":"text/plain",".log":"text/plain",".json":"application/json",".pdf":"application/pdf",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".mp4":"video/mp4",".html":"text/html"};async function Z(i){let e=await Be(i);if(!e.isFile())throw new Error(`not a regular file: ${i}`);let t=await Le(i,"r"),r=e.size;return{name:Oe(i),size:r,type:Fe[Me(i).toLowerCase()]??"application/octet-stream",slice(n,s){let a=Math.max(0,Math.min(r,n)),o=Math.max(a,Math.min(r,s));return{async arrayBuffer(){let l=Buffer.alloc(o-a),f=0;for(;f<l.length;){let{bytesRead:m}=await t.read(l,f,l.length-f,a+f);if(m===0)throw new Error("file changed while sending");f+=m}return l.buffer.slice(l.byteOffset,l.byteOffset+l.length)}}},async close(){await t?.close(),t=null}}}import{createWriteStream as $e,existsSync as Ke,mkdirSync as He,rmSync as We}from"node:fs";import{extname as ze,join as he,basename as Ge}from"node:path";var Ue=/^(\d{3})(\d{3})([A-Z]{3})$/,X="ABCDEFGHJKLMNPQRSTUVWXYZ";function w(i){let e="";for(let t=0;t<i.length;t+=32768)e+=String.fromCharCode(...i.subarray(t,t+32768));return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function S(i){let e=i.replace(/-/g,"+").replace(/_/g,"/").padEnd(Math.ceil(i.length/4)*4,"="),t=atob(e),r=new Uint8Array(t.length);for(let n=0;n<t.length;n++)r[n]=t.charCodeAt(n);return r}function u(i){return i.slice().buffer}function Q(){let i=crypto.getRandomValues(new Uint32Array(4)),e=String(i[0]%1e6).padStart(6,"0"),t=[i[1],i[2],i[3]].map(r=>X[r%X.length]).join("");return`${e.slice(0,3)}-${e.slice(3)}-${t}`}function Ne(i){let t=String(i||"").toUpperCase().replace(/[^0-9A-Z]/g,"").match(Ue);return t?`${t[1]}-${t[2]}-${t[3]}`:null}function ee(){return w(crypto.getRandomValues(new Uint8Array(16)))}function te(i,e,t){return`${i.split("#")[0]}#p2p:${e}${t?`/${t}`:""}`}function ne(i){let e=String(i||"").trim();if(!e)return null;let t=e.indexOf("#");t>=0&&(e=e.slice(t+1));try{e=decodeURIComponent(e)}catch{}e.startsWith("p2p:")&&(e=e.slice(4));let[r,n]=e.split("/"),s=Ne(r??"");if(!s)return null;let a=n&&/^[A-Za-z0-9_-]{16,64}$/.test(n)?n:void 0;return a?{code:s,secret:a}:{code:s}}function O(i=16){return w(crypto.getRandomValues(new Uint8Array(i)))}async function M(i,e,t=31e4){let r=await crypto.subtle.importKey("raw",u(new TextEncoder().encode(i.normalize("NFC"))),"PBKDF2",!1,["deriveBits"]),n=new Uint8Array(await crypto.subtle.deriveBits({name:"PBKDF2",salt:u(S(e)),iterations:t,hash:"SHA-256"},r,512));return{auth:await crypto.subtle.importKey("raw",u(n.slice(0,32)),{name:"HMAC",hash:"SHA-256"},!1,["sign","verify"]),mix:n.slice(32)}}function re(i,e){return u(new TextEncoder().encode(`filenymous-p2p-auth-v3:${i}:${e}`))}async function ie(i,e,t){return w(new Uint8Array(await crypto.subtle.sign("HMAC",i.auth,re(e,t))))}async function se(i,e,t,r){try{return await crypto.subtle.verify("HMAC",i.auth,u(S(r)),re(e,t))}catch{return!1}}async function F(i,e){let t=S(i),r=t;e&&(r=new Uint8Array(t.length+e.length),r.set(t),r.set(e,t.length));let n=await crypto.subtle.importKey("raw",u(r),"HKDF",!1,["deriveKey"]);return crypto.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:u(new TextEncoder().encode("filenymous-p2p-v2")),info:u(new TextEncoder().encode(e?"file-key+password":"file-key"))},n,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}async function ae(i){let e=await crypto.subtle.importKey("raw",u(new TextEncoder().encode(`filenymous-p2p-code-v2:${i}`)),"PBKDF2",!1,["deriveKey"]);return crypto.subtle.deriveKey({name:"PBKDF2",salt:u(new TextEncoder().encode("filenymous-p2p-secret-wrap")),iterations:12e4,hash:"SHA-256"},e,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}async function oe(i,e){let t=crypto.getRandomValues(new Uint8Array(12)),r=await crypto.subtle.encrypt({name:"AES-GCM",iv:t},await ae(i),u(S(e)));return`${w(t)}.${w(new Uint8Array(r))}`}async function ce(i,e){let[t,r]=String(e||"").split(".");if(!t||!r)throw new Error("invalid wrapped secret");let n=await crypto.subtle.decrypt({name:"AES-GCM",iv:u(S(t))},await ae(i),u(S(r)));return w(new Uint8Array(n))}function le(i,e){let t=new Uint8Array(12),r=new DataView(t.buffer);return r.setUint32(0,i),r.setUint32(4,Math.floor(e/2**32)),r.setUint32(8,e>>>0),t}async function de(i,e,t,r){let n=new Uint8Array(8),s=new DataView(n.buffer);s.setUint32(0,e),s.setUint32(4,t);let a=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:u(le(e,t)),additionalData:u(n)},i,u(r))),o=new Uint8Array(8+a.byteLength);return o.set(n),o.set(a,8),o.buffer}async function pe(i,e){let t=new Uint8Array(e);if(t.byteLength<24)throw new Error("short frame");let r=t.subarray(0,8),n=new DataView(t.buffer,t.byteOffset,8),s=n.getUint32(0),a=n.getUint32(4),o=new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:u(le(s,a)),additionalData:u(r)},i,u(t.subarray(8))));return{fileIndex:s,chunkIndex:a,plain:o}}function U(i,e=65536){return i===0?0:Math.ceil(i/e)}function N(i){try{let e=JSON.parse(i);if(!e||typeof e!="object"||typeof e.type!="string")return null;if(e.type==="manifest"){if(!Array.isArray(e.files)||e.files.length===0||e.files.length>1e3||!e.files.every(t=>typeof t?.name=="string"&&Number.isSafeInteger(t.size)&&t.size>=0)||typeof e.wrappedSecret!="string"||!Number.isSafeInteger(e.chunkSize))return null;if(e.auth!==void 0){let t=e.auth;if(!t||typeof t.salt!="string"||typeof t.nonce!="string"||!Number.isSafeInteger(t.iter)||t.iter<1e5||t.iter>5e6)return null}}if("device"in e&&e.device!==void 0){if(typeof e.device!="string")return null;e.device=e.device.replace(/[\u0000-\u001f\u007f]/g,"").slice(0,80)}return e}catch{return null}}function ue(i){return String(i||"").replace(/[\u0000-\u001f\u007f]/g,"").replace(/[\\/:*?"<>|]/g,"_").replace(/^\.+/,"").trim().slice(0,200)||"file"}function Ye(i,e,t=Ke){let r=ue(e),n=ze(r),s=n?r.slice(0,-n.length):r,a=he(i,r);for(let o=1;t(a);o++)a=he(i,`${s} (${o})${n}`);return a}var x=class{constructor(e){this.dir=e;He(e,{recursive:!0})}kind="fs-dir";written=[];stream=null;path="";async open(e,t){this.path=Ye(this.dir,Ge(t.name)),this.stream=$e(this.path,{flags:"wx"}),await new Promise((r,n)=>this.stream.once("open",()=>r()).once("error",n))}async write(e){let t=this.stream;t.write(e)||await new Promise(r=>t.once("drain",()=>r()))}async close(){let e=this.stream;e&&(await new Promise((t,r)=>e.end(n=>n?r(n):t())),this.written.push(this.path),this.stream=null)}async abort(){let e=this.stream;if(this.stream=null,e){e.destroy();try{We(this.path,{force:!0})}catch{}}}};function $(i){let e=String(i).trim().match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/i);if(!e)throw new Error(`invalid duration: ${i} (examples: 90s, 15m, 1h, 2d)`);let t=Number(e[1]),r=(e[2]??"s").toLowerCase(),n=t*{ms:1,s:1e3,m:6e4,h:36e5,d:864e5}[r];if(!(n>=1e3)||n>7*864e5)throw new Error(`duration out of range (1s..7d): ${i}`);return Math.round(n)}function fe(i,e){if(!e)return i;let{link:t,code:r,...n}=i;return n}function b(i){let e=["B","KB","MB","GB","TB"],t=0,r=i;for(;r>=1024&&t<e.length-1;)r/=1024,t++;return`${t?r.toFixed(1):r} ${e[t]}`}var K="https://filenymous.eu/app/",H="wss://www.opendpe.net/filenymous-signal/";import{platform as me}from"node:os";function C(){return`Filenymous CLI \xB7 ${{linux:"Linux",darwin:"macOS",win32:"Windows"}[me()]??me()}`}var qe="wss://www.opendpe.net/filenymous-signal/",ye=[{urls:"stun:stun.l.google.com:19302"}];function we(i){let e=import.meta.env?.[i];return e&&e.trim()?e.trim():void 0}function z(){let i=globalThis.__FILENYMOUS_SIGNAL_URL__;if(i)return i;let e=we("VITE_P2P_SIGNAL_URL");if(e)return e;let t=typeof location<"u"?location.hostname:"";return t==="localhost"||t==="127.0.0.1"?"ws://127.0.0.1:8789":qe}function Je(i=z()){return i.replace(/^ws(s?):\/\//,"http$1://").replace(/\/+$/,"")}var W=null,ge=0;async function G(){let i=we("VITE_P2P_ICE_SERVERS");if(i)try{let e=JSON.parse(i);if(Array.isArray(e))return e}catch{}return(!W||Date.now()-ge>30*6e4)&&(ge=Date.now(),W=(async()=>{try{let e=new AbortController,t=setTimeout(()=>e.abort(),4e3),r=await fetch(`${Je()}/ice`,{signal:e.signal,cache:"no-store"});if(clearTimeout(t),!r.ok)throw new Error(String(r.status));let n=await r.json();return n.iceServers?.length?n.iceServers:ye}catch{return ye}})()),W}var P=class i{constructor(e,t,r,n=z()){this.code=e;this.role=t;this.handlers=r;this.url=n}ws=null;closed=!1;attempt=0;queue=[];connect(){return new Promise((e,t)=>{let r=!1,n=new WebSocket(this.url);this.ws=n;let s=setTimeout(()=>{if(!r){r=!0;try{n.close()}catch{}t(new Error("signal-timeout"))}},1e4);n.onopen=()=>n.send(JSON.stringify({type:"join",code:this.code,role:this.role})),n.onmessage=a=>{let o;try{o=JSON.parse(String(a.data))}catch{return}switch(o.type){case"joined":for(this.attempt=0,r||(r=!0,clearTimeout(s),e());this.queue.length;)n.send(this.queue.shift());break;case"peer-joined":this.handlers.onPeerJoined?.();break;case"peer-left":this.handlers.onPeerLeft?.();break;case"signal":o.payload&&this.handlers.onSignal(o.payload);break;case"error":r?this.handlers.onError?.(o.error||"signal-error"):(r=!0,clearTimeout(s),this.closed=!0,n.close(),t(new Error(o.error||"signal-error")));break;default:break}},n.onerror=()=>{r||(r=!0,clearTimeout(s),t(new Error("signal-unreachable")))},n.onclose=()=>{if(this.ws!==n||this.closed||!r)return;this.handlers.onReconnecting?.();let a=Math.min(15e3,500*2**this.attempt++);setTimeout(()=>{this.closed||this.connect().catch(o=>this.handlers.onError?.(String(o.message||o)))},a)}})}clone(){return new i(this.code,this.role,this.handlers,this.url)}send(e){let t=JSON.stringify({type:"signal",code:this.code,payload:e});this.ws?.readyState===WebSocket.OPEN?this.ws.send(t):this.queue.push(t)}close(){this.closed=!0;try{this.ws?.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify({type:"leave"})),this.ws?.close(1e3,"done")}catch{}}};function Y(i){let e={candidate:i.candidate,sdpMid:i.sdpMid,sdpMLineIndex:i.sdpMLineIndex};return i.usernameFragment&&(e.usernameFragment=i.usernameFragment),e}var Ve=4*1024*1024,be=1*1024*1024,Pe=45e3,Ze=3e4,Xe=300;function g(i,e){i.readyState==="open"&&i.send(JSON.stringify(e))}function Qe(i){return i.bufferedAmount<=Ve?Promise.resolve():new Promise((e,t)=>{let r=setInterval(()=>{i.readyState!=="open"?s():i.bufferedAmount<=be&&n()},200),n=()=>{a(),e()},s=()=>{a(),t(new Error("connection-lost"))},a=()=>{clearInterval(r),i.removeEventListener("bufferedamountlow",n),i.removeEventListener("close",s)};i.addEventListener("bufferedamountlow",n),i.addEventListener("close",s)})}async function Te(i,e,t){if(e.kind==="ice"){if(!i.remoteDescription){t.push(e.candidate);return}try{await i.addIceCandidate(e.candidate)}catch{}}}var _=class{constructor(e,t,r={}){this.files=e;this.onState=t;r.password&&(this.pwSalt=O(16),this.pwKeys=M(r.password,this.pwSalt)),this.code=r.code??Q(),this.secret=r.secret??ee(),this.message=r.message,this.state={status:"starting",code:this.code,secret:this.secret,sentBytes:0,confirmedBytes:0,totalBytes:e.reduce((n,s)=>n+s.size,0),speed:0,fileStates:e.map(()=>"pending"),downloads:0,log:[],protected:!!r.password,authFailures:0},this.signal=new P(this.code,"sender",{onSignal:n=>void this.onSignal(n),onPeerJoined:()=>void this.offer(),onPeerLeft:()=>{this.attempt&&!this.attempt.open&&this.dropAttempt(this.idleStatus())},onReconnecting:()=>{this.attempt?.open||this.update({status:"reconnecting"})},onError:n=>this.update({status:"error",error:n})},r.signalUrl)}code;secret;message;pwKeys;pwSalt;authFailures=0;signal;attempt=null;pendingIce=[];state;stopped=!1;nextId=1;offerGen=0;connectTimer;update(e){this.state={...this.state,...e},this.onState(this.state)}getState(){return this.state}idleStatus(){if(this.pwKeys&&this.authFailures>=5)return"failed";let e=this.state.log[0];return e?e.result==="complete"?"complete":"failed":"waiting"}async start(){this.update({status:"starting"}),await this.signal.connect(),this.update({status:"waiting"}),await this.offer()}dropAttempt(e){clearTimeout(this.connectTimer);let t=this.attempt;if(this.attempt=null,this.pendingIce=[],t){t.finished=!0;try{t.pc.close()}catch{}}this.stopped||this.update({status:e})}record(e,t,r){if(e.finished)return;e.finished=!0;let n={id:e.id,startedAt:e.startedAt,endedAt:Date.now(),bytes:t==="complete"?this.state.totalBytes:this.state.confirmedBytes,totalBytes:this.state.totalBytes,result:t,...r?{reason:r}:{}};this.update({log:[n,...this.state.log].slice(0,50),downloads:this.state.downloads+(t==="complete"?1:0),status:t==="complete"?"complete":"failed",error:r,...t==="complete"?{confirmedBytes:this.state.totalBytes,fileStates:this.files.map(()=>"delivered")}:{fileStates:this.state.fileStates.map(s=>s==="delivered"?s:"failed")}})}fail(e,t){this.attempt===e&&(e.started&&!e.finished&&this.record(e,"failed",t),this.dropAttempt(this.idleStatus()),e.started||this.update({error:t}))}async offer(){if(this.stopped||this.attempt?.open&&!this.attempt.finished)return;let e=++this.offerGen;this.dropAttempt(this.idleStatus());let t=await G();if(e!==this.offerGen||this.stopped)return;let r=new RTCPeerConnection({iceServers:t}),n=r.createDataChannel("filenymous-p2p-v2",{ordered:!0});n.binaryType="arraybuffer",n.bufferedAmountLowThreshold=be;let s={id:this.nextId++,pc:r,channel:n,open:!1,started:!1,authed:!1,nonce:"",finished:!1,startedAt:0,lastConfirm:{at:0,bytes:0}};this.attempt=s,r.onicecandidate=o=>{o.candidate&&this.attempt===s&&this.signal.send({kind:"ice",candidate:Y(o.candidate)})},r.onconnectionstatechange=()=>{this.attempt===s&&r.connectionState==="failed"&&this.fail(s,s.open?"connection-lost":"ice-failed")},n.onopen=()=>{this.attempt===s&&(s.open=!0,clearTimeout(this.connectTimer),this.serve(s))},n.onclose=()=>{this.attempt===s&&!s.finished&&this.fail(s,s.started?"connection-lost":"recipient-left")};let a=await r.createOffer();await r.setLocalDescription(a),this.attempt===s&&this.signal.send({kind:"offer",sdp:a.sdp??""})}async onSignal(e){let t=this.attempt;if(!t)return;let r=t.pc;if(e.kind==="answer"&&r.signalingState==="have-local-offer"){this.update({status:"connecting",error:void 0}),clearTimeout(this.connectTimer),this.connectTimer=setTimeout(()=>{this.attempt===t&&!t.open&&this.fail(t,"ice-timeout")},Pe);try{await r.setRemoteDescription({type:"answer",sdp:e.sdp})}catch{this.attempt===t&&this.offer();return}for(let n of this.pendingIce.splice(0))await r.addIceCandidate(n).catch(()=>{})}else e.kind==="ice"&&await Te(r,e,this.pendingIce)}async serve(e){let{channel:t}=e;if(t.onmessage=r=>{if(typeof r.data!="string"||this.attempt!==e)return;let n=N(r.data);if(n)switch(n.type){case"start":if(this.pwKeys&&!e.authed){this.fail(e,"auth-required");break}this.stream(e);break;case"auth":this.checkAuth(e,n.proof);break;case"progress":{let s=Date.now(),a=Math.min(this.state.totalBytes,Math.max(0,Number(n.bytes)||0)),o=(s-e.lastConfirm.at)/1e3,l=o>0?(a-e.lastConfirm.bytes)/o:0;e.lastConfirm={at:s,bytes:a},this.update({confirmedBytes:a,speed:this.state.speed?this.state.speed*.7+l*.3:l});break}case"file-ack":{let s=[...this.state.fileStates];n.index>=0&&n.index<s.length&&(s[n.index]="delivered"),this.update({fileStates:s});break}case"ack":this.record(e,"complete"),setTimeout(()=>{this.attempt===e&&this.dropAttempt("complete")},1500);break;case"cancel":this.fail(e,n.reason||"cancelled");break;case"hello":this.update({peerDevice:n.device});break;default:break}},this.pwKeys){if(this.authFailures>=5){g(t,{type:"cancel",reason:"too-many-attempts"}),this.fail(e,"too-many-attempts");return}e.nonce=O(16),g(t,{type:"manifest",v:3,chunkSize:65536,files:[{name:"locked",size:0,mime:""}],wrappedSecret:"",auth:{salt:this.pwSalt,iter:31e4,nonce:e.nonce},device:C()}),this.attempt===e&&this.update({status:"connected",error:void 0});return}await this.sendManifest(e)}async checkAuth(e,t){if(!this.pwKeys||e.authed||e.finished)return;let r=typeof t=="string"&&await se(await this.pwKeys,this.code,e.nonce,t);if(this.attempt!==e)return;if(r){e.authed=!0,g(e.channel,{type:"auth-ok"}),await this.sendManifest(e);return}this.authFailures+=1;let n=Math.max(0,5-this.authFailures);this.update({authFailures:this.authFailures}),g(e.channel,{type:"auth-fail",remaining:n}),n===0&&(g(e.channel,{type:"cancel",reason:"too-many-attempts"}),this.fail(e,"too-many-attempts"),setTimeout(()=>{this.stopped=!0,this.signal.close(),this.update({status:"failed",error:"too-many-attempts"})},1e3))}async sendManifest(e){let{channel:t}=e;g(t,{type:"manifest",v:2,chunkSize:65536,files:this.files.map(r=>({name:r.name,size:r.size,mime:r.type||"application/octet-stream"})),wrappedSecret:await oe(this.code,this.secret),...this.message?{message:this.message}:{},device:C(),...this.pwKeys?{protected:!0}:{}}),this.attempt===e&&this.update({status:"connected",error:void 0})}async stream(e){if(e.started)return;e.started=!0,e.startedAt=Date.now(),e.lastConfirm={at:e.startedAt,bytes:0};let{channel:t}=e;this.update({status:"transferring",sentBytes:0,confirmedBytes:0,speed:0,fileStates:this.files.map(()=>"pending"),error:void 0});try{let r=await F(this.secret,this.pwKeys?(await this.pwKeys).mix:void 0),n=0,s=0;for(let a=0;a<this.files.length;a++){let o=this.files[a],l=[...this.state.fileStates];l[a]="sending",this.update({fileStates:l});let f=U(o.size);for(let m=0;m<f;m++){if(this.attempt!==e||t.readyState!=="open")throw new Error("connection-lost");let y=new Uint8Array(await o.slice(m*65536,(m+1)*65536).arrayBuffer());await Qe(t),t.send(await de(r,a,m,y)),n+=y.byteLength,(Date.now()-s>150||n===this.state.totalBytes)&&(s=Date.now(),this.update({sentBytes:n}))}g(t,{type:"file-end",index:a,chunks:f})}g(t,{type:"done"}),this.update({sentBytes:n})}catch(r){this.fail(e,String(r.message||r))}}stop(){this.stopped=!0;let e=this.attempt;if(e?.started&&!e.finished&&this.record(e,"failed","sender-stopped"),clearTimeout(this.connectTimer),e){try{g(e.channel,{type:"cancel",reason:"sender-stopped"})}catch{}try{e.pc.close()}catch{}}this.attempt=null,this.signal.close(),this.update({status:"stopped"})}},I=class{constructor(e,t,r,n,s=Ze){this.code=e;this.secret=t;this.onState=r;this.stallTimeoutMs=s;this.signal=new P(e,"receiver",{onSignal:a=>void this.onSignal(a),onPeerLeft:()=>{!this.channel&&this.state.status!=="complete"&&this.state.status!=="error"&&this.update({status:"waiting-sender"})},onError:a=>{this.channel||this.update({status:"error",error:a})}},n)}signal;pc=null;channel=null;pendingIce=[];key=null;state={status:"connecting",files:[],receivedBytes:0,totalBytes:0,speed:0};sink=null;queue=Promise.resolve();expected={file:0,chunk:0};connectTimer;stallTimer;lastData=0;lastProgressSent=0;speedRef={at:0,bytes:0};closed=!1;offerGen=0;challenge=null;pwMix=null;update(e){this.state={...this.state,...e},this.onState(this.state)}fail(e){if(!(this.state.status==="complete"||this.state.status==="error")){if(clearInterval(this.stallTimer),this.sink?.abort(),this.channel?.readyState==="open")try{this.channel.send(JSON.stringify({type:"cancel",reason:e}))}catch{}this.update({status:"error",error:e})}}async start(){for(let e=0;!this.closed;e++)try{await this.signal.connect(),this.pc||this.update({status:"waiting-sender",error:void 0});return}catch(t){let r=String(t.message||t);if(r!=="role-taken"||e>200){this.update({status:"error",error:r});return}this.update({status:"waiting-sender",error:r}),await new Promise(n=>setTimeout(n,3e3)),this.signal=this.signal.clone()}}async onSignal(e){if(e.kind==="offer"){if(this.channel?.readyState==="open"||this.state.status==="receiving"||this.state.status==="complete")return;let t=++this.offerGen;try{this.pc?.close()}catch{}this.pc=null,this.pendingIce=[],this.channel=null;let r=await G();if(t!==this.offerGen||this.closed)return;let n=new RTCPeerConnection({iceServers:r});this.pc=n,this.update({status:"negotiating",error:void 0}),clearTimeout(this.connectTimer),this.connectTimer=setTimeout(()=>{this.pc===n&&this.state.status==="negotiating"&&this.update({status:"error",error:"ice-timeout"})},Pe),n.onicecandidate=a=>{a.candidate&&this.pc===n&&this.signal.send({kind:"ice",candidate:Y(a.candidate)})},n.onconnectionstatechange=()=>{this.pc===n&&n.connectionState==="failed"&&this.fail(this.channel?"connection-lost":"ice-failed")},n.ondatachannel=a=>this.attach(a.channel),await n.setRemoteDescription({type:"offer",sdp:e.sdp});for(let a of this.pendingIce.splice(0))await n.addIceCandidate(a).catch(()=>{});let s=await n.createAnswer();if(await n.setLocalDescription(s),t!==this.offerGen)return;this.signal.send({kind:"answer",sdp:s.sdp??""})}else e.kind==="ice"&&(this.pc?await Te(this.pc,e,this.pendingIce):this.pendingIce.push(e.candidate))}attach(e){this.channel=e,e.binaryType="arraybuffer",e.onmessage=t=>{let r=t.data;this.lastData=Date.now(),this.queue=this.queue.then(()=>this.handle(r)).catch(n=>this.fail(String(n.message||n)))},e.onclose=()=>{(this.state.status==="receiving"||this.state.status==="ready"||this.state.status==="password")&&this.fail("sender-disconnected")}}sendProgress(e=!1){let t=Date.now();if(!e&&t-this.lastProgressSent<Xe)return;this.lastProgressSent=t;let r=(t-this.speedRef.at)/1e3;if(r>=.5){let n=(this.state.receivedBytes-this.speedRef.bytes)/r;this.speedRef={at:t,bytes:this.state.receivedBytes},this.update({speed:this.state.speed?this.state.speed*.7+n*.3:n})}this.channel?.readyState==="open"&&this.channel.send(JSON.stringify({type:"progress",bytes:this.state.receivedBytes}))}async handle(e){if(this.state.status==="error")return;if(typeof e=="string"){let s=N(e);if(!s)return;if(s.type==="manifest"&&s.v===3&&s.auth)clearTimeout(this.connectTimer),this.challenge=s.auth,this.pwMix=null,this.update({status:"password",peerDevice:s.device,authChecking:!1,authRemaining:5});else if(s.type==="auth-fail")this.pwMix=null,this.update({authChecking:!1,authError:!0,authRemaining:Math.max(0,Number(s.remaining)||0)});else if(s.type==="auth-ok")this.update({authError:!1});else if(s.type==="manifest"){if(clearTimeout(this.connectTimer),s.protected&&!this.pwMix)throw new Error("auth-required");if(s.v!==2||s.chunkSize!==65536)throw new Error("unsupported-protocol");let a=this.secret??await ce(this.code,s.wrappedSecret);this.key=await F(a,s.protected?this.pwMix:void 0),this.update({status:"ready",authChecking:!1,authError:!1,files:s.files,message:s.message,peerDevice:s.device,totalBytes:s.files.reduce((o,l)=>o+l.size,0)}),this.channel?.send(JSON.stringify({type:"hello",device:C()}))}else if(s.type==="file-end"){let a=this.state.files[s.index];if(!a||s.index!==this.expected.file||s.chunks!==U(a.size)||this.expected.chunk!==s.chunks)throw new Error("integrity-error");s.chunks===0&&await this.sink.open(s.index,a),await this.sink.close(),this.expected={file:s.index+1,chunk:0},this.sendProgress(!0),this.channel?.send(JSON.stringify({type:"file-ack",index:s.index}))}else if(s.type==="done"){if(this.expected.file!==this.state.files.length)throw new Error("integrity-error");clearInterval(this.stallTimer),this.channel?.send(JSON.stringify({type:"ack",bytes:this.state.receivedBytes})),this.update({status:"complete",receivedBytes:this.state.totalBytes}),setTimeout(()=>this.close(),5e3)}else s.type==="cancel"&&this.fail(s.reason==="sender-stopped"?"sender-stopped":s.reason||"cancelled");return}if(!this.key||!this.sink)throw new Error("unexpected-data");let{fileIndex:t,chunkIndex:r,plain:n}=await pe(this.key,e).catch(()=>{throw new Error("decrypt-error")});if(t!==this.expected.file||r!==this.expected.chunk)throw new Error("integrity-error");r===0&&await this.sink.open(t,this.state.files[t]),await this.sink.write(n),this.expected.chunk+=1,this.state={...this.state,receivedBytes:this.state.receivedBytes+n.byteLength},this.onState(this.state),this.sendProgress()}async submitPassword(e){if(this.state.status!=="password"||!this.challenge||this.state.authChecking)return;if(!this.channel||this.channel.readyState!=="open"){this.fail("sender-disconnected");return}this.update({authChecking:!0,authError:!1});let t=this.challenge,r=await M(e,t.salt,t.iter),n=await ie(r,this.code,t.nonce);this.pwMix=r.mix,this.channel.send(JSON.stringify({type:"auth",proof:n}))}download(e){if(this.state.status!=="ready")throw new Error("not-ready");if(!this.channel||this.channel.readyState!=="open"){this.fail("sender-disconnected");return}this.sink=e,this.expected={file:0,chunk:0},this.lastData=Date.now(),this.speedRef={at:Date.now(),bytes:0},this.update({status:"receiving",receivedBytes:0,speed:0}),this.channel.send(JSON.stringify({type:"start"})),clearInterval(this.stallTimer),this.stallTimer=setInterval(()=>{if(this.state.status!=="receiving")return clearInterval(this.stallTimer);Date.now()-this.lastData>this.stallTimeoutMs&&this.fail("stalled")},1e3)}close(){this.closed=!0,clearTimeout(this.connectTimer),clearInterval(this.stallTimer),this.signal.close();try{this.pc?.close()}catch{}}};var Ee=`filenymous 0.1.0 \u2014 end-to-end encrypted P2P file sharing (no server storage)
|
|
3
|
+
import{parseArgs as ct}from"node:util";import{createInterface as lt}from"node:readline";import{resolve as _e}from"node:path";import{RTCPeerConnection as Me,RTCSessionDescription as De,RTCIceCandidate as Le,RTCDataChannel as Be}from"node-datachannel/polyfill";import Ne from"ws";function Ue(s){return s.replace(/^a=/,"")}var U=class extends Me{pendingLocal=0;remoteSet=!1;held=[];userHandler=null;constructor(e){super(e);let t=i=>{let r=i.candidate,n={candidate:r?{candidate:Ue(r.candidate),sdpMid:r.sdpMid??"0",sdpMLineIndex:r.sdpMLineIndex??0}:null};this.pendingLocal>0||!this.remoteSet?this.held.push(n):this.userHandler?.(n)};Object.defineProperty(this,"onicecandidate",{configurable:!0,get:()=>this.userHandler?t:null,set:i=>{this.userHandler=i}})}async createOffer(e){return this.pendingLocal=1,super.createOffer()}async createAnswer(e){return this.pendingLocal=1,super.createAnswer()}async setLocalDescription(e){this.pendingLocal=1;try{await super.setLocalDescription(e)}finally{setTimeout(()=>{this.pendingLocal=0,this.flush()},0)}}async setRemoteDescription(e){await super.setRemoteDescription(e),this.remoteSet=!0,this.flush()}flush(){if(!(this.pendingLocal>0||!this.remoteSet))for(let e of this.held.splice(0))this.userHandler?.(e)}};function Q(s){let e=globalThis;e.RTCPeerConnection=U,e.RTCSessionDescription??=De,e.RTCIceCandidate??=Le,e.RTCDataChannel??=Be,e.WebSocket||(e.WebSocket=Ne),e.__FILENYMOUS_SIGNAL_URL__=s}import{open as Fe,stat as $e}from"node:fs/promises";import{basename as Oe,extname as Ke}from"node:path";var He={".zip":"application/zip",".gz":"application/gzip",".tgz":"application/gzip",".tar":"application/x-tar",".txt":"text/plain",".log":"text/plain",".json":"application/json",".pdf":"application/pdf",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".mp4":"video/mp4",".html":"text/html"};async function ee(s){let e=await $e(s);if(!e.isFile())throw new Error(`not a regular file: ${s}`);let t=await Fe(s,"r"),i=e.size;return{name:Oe(s),size:i,type:He[Ke(s).toLowerCase()]??"application/octet-stream",slice(r,n){let a=Math.max(0,Math.min(i,r)),o=Math.max(a,Math.min(i,n));return{async arrayBuffer(){let c=Buffer.alloc(o-a),l=0;for(;l<c.length;){let{bytesRead:h}=await t.read(c,l,c.length-l,a+l);if(h===0)throw new Error("file changed while sending");l+=h}return c.buffer.slice(c.byteOffset,c.byteOffset+c.length)}}},async close(){await t?.close(),t=null}}}import{createWriteStream as qe,existsSync as ze,mkdirSync as Ye,rmSync as Je}from"node:fs";import{extname as Ve,join as fe,basename as je}from"node:path";var We=/^(\d{3})(\d{3})([A-Z]{3})$/,te="ABCDEFGHJKLMNPQRSTUVWXYZ";function b(s){let e="";for(let t=0;t<s.length;t+=32768)e+=String.fromCharCode(...s.subarray(t,t+32768));return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function x(s){let e=s.replace(/-/g,"+").replace(/_/g,"/").padEnd(Math.ceil(s.length/4)*4,"="),t=atob(e),i=new Uint8Array(t.length);for(let r=0;r<t.length;r++)i[r]=t.charCodeAt(r);return i}function f(s){return s.slice().buffer}function re(){let s=crypto.getRandomValues(new Uint32Array(4)),e=String(s[0]%1e6).padStart(6,"0"),t=[s[1],s[2],s[3]].map(i=>te[i%te.length]).join("");return`${e.slice(0,3)}-${e.slice(3)}-${t}`}function Ge(s){let t=String(s||"").toUpperCase().replace(/[^0-9A-Z]/g,"").match(We);return t?`${t[1]}-${t[2]}-${t[3]}`:null}function ie(){return b(crypto.getRandomValues(new Uint8Array(16)))}function ne(s,e,t,i){let r=s.split("#")[0],n=t&&i?`/e${i.toString(36)}`:"";return`${r}#p2p:${e}${t?`/${t}`:""}${n}`}function se(s){let e=String(s||"").trim();if(!e)return null;let t=e.indexOf("#");t>=0&&(e=e.slice(t+1));try{e=decodeURIComponent(e)}catch{}e.startsWith("p2p:")&&(e=e.slice(4));let[i,r,n]=e.split("/"),a=Ge(i??"");if(!a)return null;let o=r&&/^[A-Za-z0-9_-]{16,64}$/.test(r)?r:void 0;if(!o)return{code:a};let c=n&&/^e[0-9a-z]{1,10}$/.test(n)?parseInt(n.slice(1),36):void 0;return c?{code:a,secret:o,expires:c}:{code:a,secret:o}}function F(s=16){return b(crypto.getRandomValues(new Uint8Array(s)))}async function $(s,e,t=31e4){let i=await crypto.subtle.importKey("raw",f(new TextEncoder().encode(s.normalize("NFC"))),"PBKDF2",!1,["deriveBits"]),r=new Uint8Array(await crypto.subtle.deriveBits({name:"PBKDF2",salt:f(x(e)),iterations:t,hash:"SHA-256"},i,512));return{auth:await crypto.subtle.importKey("raw",f(r.slice(0,32)),{name:"HMAC",hash:"SHA-256"},!1,["sign","verify"]),mix:r.slice(32)}}function ae(s,e){return f(new TextEncoder().encode(`filenymous-p2p-auth-v3:${s}:${e}`))}async function oe(s,e,t){return b(new Uint8Array(await crypto.subtle.sign("HMAC",s.auth,ae(e,t))))}async function ce(s,e,t,i){try{return await crypto.subtle.verify("HMAC",s.auth,f(x(i)),ae(e,t))}catch{return!1}}async function O(s,e,t){let i=x(s),r=i;e&&(r=new Uint8Array(i.length+e.length),r.set(i),r.set(e,i.length));let n=await crypto.subtle.importKey("raw",f(r),"HKDF",!1,["deriveKey"]);return crypto.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:f(new TextEncoder().encode("filenymous-p2p-v2")),info:f(new TextEncoder().encode(`${e?"file-key+password":"file-key"}${t?`|exp=${t}`:""}`))},n,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}async function le(s){let e=await crypto.subtle.importKey("raw",f(new TextEncoder().encode(`filenymous-p2p-code-v2:${s}`)),"PBKDF2",!1,["deriveKey"]);return crypto.subtle.deriveKey({name:"PBKDF2",salt:f(new TextEncoder().encode("filenymous-p2p-secret-wrap")),iterations:12e4,hash:"SHA-256"},e,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}async function de(s,e){let t=crypto.getRandomValues(new Uint8Array(12)),i=await crypto.subtle.encrypt({name:"AES-GCM",iv:t},await le(s),f(x(e)));return`${b(t)}.${b(new Uint8Array(i))}`}async function pe(s,e){let[t,i]=String(e||"").split(".");if(!t||!i)throw new Error("invalid wrapped secret");let r=await crypto.subtle.decrypt({name:"AES-GCM",iv:f(x(t))},await le(s),f(x(i)));return b(new Uint8Array(r))}function ue(s,e){let t=new Uint8Array(12),i=new DataView(t.buffer);return i.setUint32(0,s),i.setUint32(4,Math.floor(e/2**32)),i.setUint32(8,e>>>0),t}async function K(s,e,t,i){let r=new Uint8Array(8),n=new DataView(r.buffer);n.setUint32(0,e),n.setUint32(4,t);let a=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:f(ue(e,t)),additionalData:f(r)},s,f(i))),o=new Uint8Array(8+a.byteLength);return o.set(r),o.set(a,8),o.buffer}async function A(s,e){let t=new Uint8Array(e);if(t.byteLength<24)throw new Error("short frame");let i=t.subarray(0,8),r=new DataView(t.buffer,t.byteOffset,8),n=r.getUint32(0),a=r.getUint32(4),o=new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:f(ue(n,a)),additionalData:f(i)},s,f(t.subarray(8))));return{fileIndex:n,chunkIndex:a,plain:o}}function R(s,e=65536){return s===0?0:Math.ceil(s/e)}function H(s){try{let e=JSON.parse(s);if(!e||typeof e!="object"||typeof e.type!="string")return null;if(e.type==="manifest"){if(!Array.isArray(e.files)||e.files.length===0||e.files.length>1e3||!e.files.every(t=>typeof t?.name=="string"&&Number.isSafeInteger(t.size)&&t.size>=0)||typeof e.wrappedSecret!="string"||!Number.isSafeInteger(e.chunkSize)||e.expires!==void 0&&!(Number.isSafeInteger(e.expires)&&e.expires>0))return null;if(e.auth!==void 0){let t=e.auth;if(!t||typeof t.salt!="string"||typeof t.nonce!="string"||!Number.isSafeInteger(t.iter)||t.iter<1e5||t.iter>5e6)return null}}if(e.type==="range"){let{index:t,from:i,to:r}=e;if(![t,i,r].every(n=>Number.isSafeInteger(n)&&n>=0)||r<i||r-i>=64)return null}if("device"in e&&e.device!==void 0){if(typeof e.device!="string")return null;e.device=e.device.replace(/[\u0000-\u001f\u007f]/g,"").slice(0,80)}return e}catch{return null}}function he(s){return String(s||"").replace(/[\u0000-\u001f\u007f]/g,"").replace(/[\\/:*?"<>|]/g,"_").replace(/^\.+/,"").trim().slice(0,200)||"file"}function Ze(s,e,t=ze){let i=he(e),r=Ve(i),n=r?i.slice(0,-r.length):i,a=fe(s,i);for(let o=1;t(a);o++)a=fe(s,`${n} (${o})${r}`);return a}var _=class{constructor(e){this.dir=e;Ye(e,{recursive:!0})}kind="fs-dir";written=[];stream=null;path="";async open(e,t){this.path=Ze(this.dir,je(t.name)),this.stream=qe(this.path,{flags:"wx"}),await new Promise((i,r)=>this.stream.once("open",()=>i()).once("error",r))}async write(e){let t=this.stream;t.write(e)||await new Promise(i=>t.once("drain",()=>i()))}async close(){let e=this.stream;e&&(await new Promise((t,i)=>e.end(r=>r?i(r):t())),this.written.push(this.path),this.stream=null)}async abort(){let e=this.stream;if(this.stream=null,e){e.destroy();try{Je(this.path,{force:!0})}catch{}}}};function W(s){let e=String(s).trim().match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/i);if(!e)throw new Error(`invalid duration: ${s} (examples: 90s, 15m, 1h, 2d)`);let t=Number(e[1]),i=(e[2]??"s").toLowerCase(),r=t*{ms:1,s:1e3,m:6e4,h:36e5,d:864e5}[i];if(!(r>=1e3)||r>7*864e5)throw new Error(`duration out of range (1s..7d): ${s}`);return Math.round(r)}function me(s,e){if(!e)return s;let{link:t,code:i,...r}=s;return r}function E(s){let e=["B","KB","MB","GB","TB"],t=0,i=s;for(;i>=1024&&t<e.length-1;)i/=1024,t++;return`${t?i.toFixed(1):i} ${e[t]}`}var G="https://filenymous.eu/app/",q="wss://www.opendpe.net/filenymous-signal/";import{platform as ye}from"node:os";function C(){return`Filenymous CLI \xB7 ${{linux:"Linux",darwin:"macOS",win32:"Windows"}[ye()]??ye()}`}var Xe="wss://www.opendpe.net/filenymous-signal/",ge=[{urls:"stun:stun.l.google.com:19302"}];function ve(s){let e=import.meta.env?.[s];return e&&e.trim()?e.trim():void 0}function Y(){let s=globalThis.__FILENYMOUS_SIGNAL_URL__;if(s)return s;let e=ve("VITE_P2P_SIGNAL_URL");if(e)return e;let t=typeof location<"u"?location.hostname:"";return t==="localhost"||t==="127.0.0.1"?"ws://127.0.0.1:8789":Xe}function Qe(s=Y()){return s.replace(/^ws(s?):\/\//,"http$1://").replace(/\/+$/,"")}var z=null,we=0;async function J(){let s=ve("VITE_P2P_ICE_SERVERS");if(s)try{let e=JSON.parse(s);if(Array.isArray(e))return e}catch{}return(!z||Date.now()-we>30*6e4)&&(we=Date.now(),z=(async()=>{try{let e=new AbortController,t=setTimeout(()=>e.abort(),4e3),i=await fetch(`${Qe()}/ice`,{signal:e.signal,cache:"no-store"});if(clearTimeout(t),!i.ok)throw new Error(String(i.status));let r=await i.json();return r.iceServers?.length?r.iceServers:ge}catch{return ge}})()),z}var T=class s{constructor(e,t,i,r=Y()){this.code=e;this.role=t;this.handlers=i;this.url=r}ws=null;closed=!1;attempt=0;queue=[];connect(){return new Promise((e,t)=>{let i=!1,r=new WebSocket(this.url);this.ws=r;let n=setTimeout(()=>{if(!i){i=!0;try{r.close()}catch{}t(new Error("signal-timeout"))}},1e4);r.onopen=()=>r.send(JSON.stringify({type:"join",code:this.code,role:this.role})),r.onmessage=a=>{let o;try{o=JSON.parse(String(a.data))}catch{return}switch(o.type){case"joined":for(this.attempt=0,i||(i=!0,clearTimeout(n),e());this.queue.length;)r.send(this.queue.shift());break;case"peer-joined":this.handlers.onPeerJoined?.();break;case"peer-left":this.handlers.onPeerLeft?.();break;case"signal":o.payload&&this.handlers.onSignal(o.payload);break;case"error":i?this.handlers.onError?.(o.error||"signal-error"):(i=!0,clearTimeout(n),this.closed=!0,r.close(),t(new Error(o.error||"signal-error")));break;default:break}},r.onerror=()=>{i||(i=!0,clearTimeout(n),t(new Error("signal-unreachable")))},r.onclose=()=>{if(this.ws!==r||this.closed||!i)return;this.handlers.onReconnecting?.();let a=Math.min(15e3,500*2**this.attempt++);setTimeout(()=>{this.closed||this.connect().catch(o=>this.handlers.onError?.(String(o.message||o)))},a)}})}clone(){return new s(this.code,this.role,this.handlers,this.url)}send(e){let t=JSON.stringify({type:"signal",code:this.code,payload:e});this.ws?.readyState===WebSocket.OPEN?this.ws.send(t):this.queue.push(t)}close(){this.closed=!0;try{this.ws?.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify({type:"leave"})),this.ws?.close(1e3,"done")}catch{}}};function V(s){let e={candidate:s.candidate,sdpMid:s.sdpMid,sdpMLineIndex:s.sdpMLineIndex};return s.usernameFragment&&(e.usernameFragment=s.usernameFragment),e}function Se(s,e,t,i=65536){let r=Math.max(0,t-1),n=Math.min(Math.max(0,s),r),a=Math.min(Math.max(n,e),r);return{from:Math.floor(n/i),to:Math.floor(a/i)}}var tt=256,rt=32;function it(s,e,t){let i=[],r=-1;for(let n=s;n<=e+1;n++){let a=n<=e&&!t(n);a&&r<0&&(r=n),r>=0&&(!a||n-r===64)&&(i.push([r,n-1]),r=a?n:-1)}return i}var I=class{constructor(e,t,i,r){this.key=e;this.files=t;this.send=i;this.onBytes=r}cache=new Map;waiters=new Map;requested=new Set;received=0;get receivedBytes(){return this.received}async onFrame(e){let{fileIndex:t,chunkIndex:i,plain:r}=await A(this.key,e),n=`${t}:${i}`;for(this.requested.delete(n),this.received+=r.byteLength,this.onBytes?.(this.received),this.cache.delete(n),this.cache.set(n,r);this.cache.size>tt;)this.cache.delete(this.cache.keys().next().value);let a=this.waiters.get(n);if(a){this.waiters.delete(n);for(let o of a)o(r)}}request(e,t,i,r=!1){let n=a=>this.cache.has(`${e}:${a}`)||this.requested.has(`${e}:${a}`);for(let[a,o]of it(t,i,n)){for(let c=a;c<=o;c++)this.requested.add(`${e}:${c}`);this.send({type:"range",index:e,from:a,to:o,...r?{prefetch:!0}:{}})}}chunk(e,t){let i=`${e}:${t}`,r=this.cache.get(i);return r?(this.cache.delete(i),this.cache.set(i,r),Promise.resolve(r)):new Promise(n=>{let a=this.waiters.get(i)??[];a.push(n),this.waiters.set(i,a)})}async read(e,t,i){let r=this.files[e];if(!r)throw new Error("bad-file");if(r.size===0)return new Uint8Array(0);let{from:n,to:a}=Se(t,i,r.size);this.request(e,n,a);let o=Math.ceil(r.size/65536)-1;a<o&&this.request(e,a+1,Math.min(o,a+rt),!0);let c=await Promise.all(Array.from({length:a-n+1},(g,u)=>this.chunk(e,n+u))),l=new Uint8Array(Math.min(i,r.size-1)-t+1),h=0,w=t-n*65536;for(let g of c){let u=g.subarray(w,w+(l.length-h));l.set(u,h),h+=u.length,w=0}return l}};var st=4*1024*1024,Te=1*1024*1024,ke=45e3,at=3e4,ot=300;function v(s,e){s.readyState==="open"&&s.send(JSON.stringify(e))}function be(s){return s.bufferedAmount<=st?Promise.resolve():new Promise((e,t)=>{let i=setInterval(()=>{s.readyState!=="open"?n():s.bufferedAmount<=Te&&r()},200),r=()=>{a(),e()},n=()=>{a(),t(new Error("connection-lost"))},a=()=>{clearInterval(i),s.removeEventListener("bufferedamountlow",r),s.removeEventListener("close",n)};s.addEventListener("bufferedamountlow",r),s.addEventListener("close",n)})}async function Ae(s,e,t){if(e.kind==="ice"){if(!s.remoteDescription){t.push(e.candidate);return}try{await s.addIceCandidate(e.candidate)}catch{}}}var D=class{constructor(e,t,i={}){this.files=e;this.onState=t;this.streamable=!!i.stream,this.expires=i.expires?Math.floor(i.expires):void 0,i.password&&(this.pwSalt=F(16),this.pwKeys=$(i.password,this.pwSalt)),this.code=i.code??re(),this.secret=i.secret??ie(),this.message=i.message,this.state={status:"starting",code:this.code,secret:this.secret,sentBytes:0,confirmedBytes:0,totalBytes:e.reduce((r,n)=>r+n.size,0),speed:0,fileStates:e.map(()=>"pending"),downloads:0,log:[],protected:!!i.password,authFailures:0,...this.expires?{expires:this.expires}:{}},this.signal=new T(this.code,"sender",{onSignal:r=>void this.onSignal(r),onPeerJoined:()=>void this.offer(),onPeerLeft:()=>{this.attempt&&!this.attempt.open&&this.dropAttempt(this.idleStatus())},onReconnecting:()=>{this.attempt?.open||this.update({status:"reconnecting"})},onError:r=>this.update({status:"error",error:r})},i.signalUrl)}code;secret;message;pwKeys;pwSalt;authFailures=0;signal;attempt=null;pendingIce=[];state;stopped=!1;nextId=1;offerGen=0;connectTimer;streamable;fileKey;expires;expiryTimer;update(e){this.state={...this.state,...e},this.onState(this.state)}getState(){return this.state}idleStatus(){if(this.pwKeys&&this.authFailures>=5)return"failed";let e=this.state.log[0];return e?e.result==="complete"?"complete":"failed":"waiting"}isExpired(){return!!this.expires&&Date.now()>=this.expires*1e3}expire(){if(this.stopped&&this.state.status==="expired")return;clearTimeout(this.expiryTimer),clearTimeout(this.connectTimer);let e=this.attempt;if(e){try{v(e.channel,{type:"cancel",reason:"expired"})}catch{}e.started&&!e.finished&&this.record(e,"failed","expired"),setTimeout(()=>{try{e.pc.close()}catch{}},300)}this.attempt=null,this.stopped=!0,this.signal.close(),this.update({status:"expired",error:"expired"})}armExpiry(){if(!this.expires)return;clearTimeout(this.expiryTimer);let e=this.expires*1e3-Date.now();if(e<=0)return this.expire();this.expiryTimer=setTimeout(()=>this.armExpiry(),Math.min(e,2**31-1))}async start(){if(this.isExpired())throw this.expire(),new Error("expired");this.armExpiry(),this.update({status:"starting"}),await this.signal.connect(),this.update({status:"waiting"}),await this.offer()}dropAttempt(e){clearTimeout(this.connectTimer);let t=this.attempt;if(this.attempt=null,this.pendingIce=[],t){t.finished=!0;try{t.pc.close()}catch{}}this.stopped||this.update({status:e})}record(e,t,i){if(e.finished)return;e.finished=!0;let r={id:e.id,startedAt:e.startedAt,endedAt:Date.now(),bytes:t==="complete"?this.state.totalBytes:this.state.confirmedBytes,totalBytes:this.state.totalBytes,result:t,...i?{reason:i}:{}};this.update({log:[r,...this.state.log].slice(0,50),downloads:this.state.downloads+(t==="complete"?1:0),status:t==="complete"?"complete":"failed",error:i,...t==="complete"?{confirmedBytes:this.state.totalBytes,fileStates:this.files.map(()=>"delivered")}:{fileStates:this.state.fileStates.map(n=>n==="delivered"?n:"failed")}})}fail(e,t){this.attempt===e&&(e.started&&!e.finished&&this.record(e,"failed",t),this.dropAttempt(this.idleStatus()),e.started||this.update({error:t}))}async offer(){if(this.stopped||this.attempt?.open&&!this.attempt.finished)return;let e=++this.offerGen;this.dropAttempt(this.idleStatus());let t=await J();if(e!==this.offerGen||this.stopped)return;let i=new RTCPeerConnection({iceServers:t}),r=i.createDataChannel("filenymous-p2p-v2",{ordered:!0});r.binaryType="arraybuffer",r.bufferedAmountLowThreshold=Te;let n={id:this.nextId++,pc:i,channel:r,open:!1,started:!1,authed:!1,nonce:"",finished:!1,startedAt:0,lastConfirm:{at:0,bytes:0}};this.attempt=n,i.onicecandidate=o=>{o.candidate&&this.attempt===n&&this.signal.send({kind:"ice",candidate:V(o.candidate)})},i.onconnectionstatechange=()=>{this.attempt===n&&i.connectionState==="failed"&&this.fail(n,n.open?"connection-lost":"ice-failed")},r.onopen=()=>{this.attempt===n&&(n.open=!0,clearTimeout(this.connectTimer),this.serve(n))},r.onclose=()=>{this.attempt===n&&!n.finished&&this.fail(n,n.started?"connection-lost":"recipient-left")};let a=await i.createOffer();await i.setLocalDescription(a),this.attempt===n&&this.signal.send({kind:"offer",sdp:a.sdp??""})}async onSignal(e){let t=this.attempt;if(!t)return;let i=t.pc;if(e.kind==="answer"&&i.signalingState==="have-local-offer"){this.update({status:"connecting",error:void 0}),clearTimeout(this.connectTimer),this.connectTimer=setTimeout(()=>{this.attempt===t&&!t.open&&this.fail(t,"ice-timeout")},ke);try{await i.setRemoteDescription({type:"answer",sdp:e.sdp})}catch{this.attempt===t&&this.offer();return}for(let r of this.pendingIce.splice(0))await i.addIceCandidate(r).catch(()=>{})}else e.kind==="ice"&&await Ae(i,e,this.pendingIce)}async serve(e){let{channel:t}=e;if(this.isExpired())return this.expire();if(t.onmessage=i=>{if(typeof i.data!="string"||this.attempt!==e)return;let r=H(i.data);if(r)switch(r.type){case"start":if(this.pwKeys&&!e.authed){this.fail(e,"auth-required");break}this.stream(e);break;case"auth":this.checkAuth(e,r.proof);break;case"range":if(!this.streamable)break;if(this.pwKeys&&!e.authed){this.fail(e,"auth-required");break}this.queueRange(e,r.index,r.from,r.to,r.prefetch===!0);break;case"progress":{let n=Date.now(),a=Math.min(this.state.totalBytes,Math.max(0,Number(r.bytes)||0)),o=(n-e.lastConfirm.at)/1e3,c=o>0?(a-e.lastConfirm.bytes)/o:0;e.lastConfirm={at:n,bytes:a},this.update({confirmedBytes:a,speed:this.state.speed?this.state.speed*.7+c*.3:c});break}case"file-ack":{let n=[...this.state.fileStates];r.index>=0&&r.index<n.length&&(n[r.index]="delivered"),this.update({fileStates:n});break}case"ack":this.record(e,"complete"),setTimeout(()=>{this.attempt===e&&this.dropAttempt("complete")},1500);break;case"cancel":this.fail(e,r.reason||"cancelled");break;case"hello":this.update({peerDevice:r.device});break;default:break}},this.pwKeys){if(this.authFailures>=5){v(t,{type:"cancel",reason:"too-many-attempts"}),this.fail(e,"too-many-attempts");return}e.nonce=F(16),v(t,{type:"manifest",v:3,chunkSize:65536,files:[{name:"locked",size:0,mime:""}],wrappedSecret:"",auth:{salt:this.pwSalt,iter:31e4,nonce:e.nonce},device:C()}),this.attempt===e&&this.update({status:"connected",error:void 0});return}await this.sendManifest(e)}async checkAuth(e,t){if(!this.pwKeys||e.authed||e.finished)return;let i=typeof t=="string"&&await ce(await this.pwKeys,this.code,e.nonce,t);if(this.attempt!==e)return;if(i){e.authed=!0,v(e.channel,{type:"auth-ok"}),await this.sendManifest(e);return}this.authFailures+=1;let r=Math.max(0,5-this.authFailures);this.update({authFailures:this.authFailures}),v(e.channel,{type:"auth-fail",remaining:r}),r===0&&(v(e.channel,{type:"cancel",reason:"too-many-attempts"}),this.fail(e,"too-many-attempts"),setTimeout(()=>{this.stopped=!0,this.signal.close(),this.update({status:"failed",error:"too-many-attempts"})},1e3))}async sendManifest(e){let{channel:t}=e;v(t,{type:"manifest",v:this.expires?4:2,...this.expires?{expires:this.expires}:{},chunkSize:65536,files:this.files.map(i=>({name:i.name,size:i.size,mime:i.type||"application/octet-stream"})),wrappedSecret:await de(this.code,this.secret),...this.message?{message:this.message}:{},device:C(),...this.pwKeys?{protected:!0}:{},...this.streamable?{streamable:!0}:{}}),this.attempt===e&&this.update({status:"connected",error:void 0})}key(){return this.fileKey??=(async()=>O(this.secret,this.pwKeys?(await this.pwKeys).mix:void 0,this.expires))(),this.fileKey}queueRange(e,t,i,r,n){let a=this.files[t];if(!a)return;let o=R(a.size)-1,c=n?e.prefetch??=[]:e.demand??=[];for(let l=i;l<=Math.min(r,o);l++)c.push([t,l]);e.pumping||this.pumpRanges(e).catch(l=>this.fail(e,String(l.message||l)))}async pumpRanges(e){e.pumping=!0;try{let t=await this.key(),i=Number(globalThis.__FILENYMOUS_TEST_CHUNK_DELAY_MS__)||0,r=new Set;for(;;){let n=e.demand?.shift()??e.prefetch?.shift();if(!n)break;let[a,o]=n,c=`${a}:${o}`;if(r.has(c))continue;if(r.add(c),r.size>4096&&r.clear(),this.isExpired())return this.expire();if(this.attempt!==e||e.channel.readyState!=="open")throw new Error("connection-lost");let l=new Uint8Array(await this.files[a].slice(o*65536,(o+1)*65536).arrayBuffer());await be(e.channel),i&&await new Promise(h=>setTimeout(h,i)),e.channel.send(await K(t,a,o,l)),this.update({status:"streaming",sentBytes:this.state.sentBytes+l.byteLength,error:void 0})}}finally{e.pumping=!1}}async stream(e){if(e.started)return;e.started=!0,e.startedAt=Date.now(),e.lastConfirm={at:e.startedAt,bytes:0};let{channel:t}=e;this.update({status:"transferring",sentBytes:0,confirmedBytes:0,speed:0,fileStates:this.files.map(()=>"pending"),error:void 0});try{let i=await this.key(),r=0,n=0;for(let a=0;a<this.files.length;a++){let o=this.files[a],c=[...this.state.fileStates];c[a]="sending",this.update({fileStates:c});let l=R(o.size);for(let h=0;h<l;h++){if(this.isExpired())return this.expire();if(this.attempt!==e||t.readyState!=="open")throw new Error("connection-lost");let w=new Uint8Array(await o.slice(h*65536,(h+1)*65536).arrayBuffer());await be(t),t.send(await K(i,a,h,w)),r+=w.byteLength,(Date.now()-n>150||r===this.state.totalBytes)&&(n=Date.now(),this.update({sentBytes:r}))}v(t,{type:"file-end",index:a,chunks:l})}v(t,{type:"done"}),this.update({sentBytes:r})}catch(i){this.fail(e,String(i.message||i))}}stop(){this.stopped=!0,clearTimeout(this.expiryTimer);let e=this.attempt;if(e?.started&&!e.finished&&this.record(e,"failed","sender-stopped"),clearTimeout(this.connectTimer),e){try{v(e.channel,{type:"cancel",reason:"sender-stopped"})}catch{}try{e.pc.close()}catch{}}this.attempt=null,this.signal.close(),this.update({status:"stopped"})}},L=class{constructor(e,t,i,r,n=at,a){this.code=e;this.secret=t;this.onState=i;this.stallTimeoutMs=n;this.linkExpires=a;this.signal=new T(e,"receiver",{onSignal:o=>void this.onSignal(o),onPeerLeft:()=>{!this.channel&&this.state.status!=="complete"&&this.state.status!=="error"&&this.update({status:"waiting-sender"})},onError:o=>{this.channel||this.update({status:"error",error:o})}},r)}signal;pc=null;channel=null;pendingIce=[];key=null;state={status:"connecting",files:[],receivedBytes:0,totalBytes:0,speed:0};sink=null;queue=Promise.resolve();expected={file:0,chunk:0};connectTimer;stallTimer;lastData=0;lastProgressSent=0;speedRef={at:0,bytes:0};closed=!1;offerGen=0;challenge=null;pwMix=null;streamReader=null;update(e){this.state={...this.state,...e},this.onState(this.state)}fail(e){if(!(this.state.status==="complete"||this.state.status==="error")){if(clearInterval(this.stallTimer),this.sink?.abort(),this.channel?.readyState==="open")try{this.channel.send(JSON.stringify({type:"cancel",reason:e}))}catch{}this.update({status:"error",error:e})}}expiryTimer;armExpiry(e){clearTimeout(this.expiryTimer);let t=e*1e3-Date.now();if(t<=0)return this.expireNow();this.expiryTimer=setTimeout(()=>this.armExpiry(e),Math.min(t,2**31-1))}expireNow(){this.fail("expired"),this.state.status!=="complete"&&this.close()}async start(){if(this.linkExpires){if(this.update({expires:this.linkExpires}),Date.now()>=this.linkExpires*1e3){this.update({status:"error",error:"expired"});return}this.armExpiry(this.linkExpires)}for(let e=0;!this.closed;e++)try{await this.signal.connect(),this.pc||this.update({status:"waiting-sender",error:void 0});return}catch(t){let i=String(t.message||t);if(i!=="role-taken"||e>200){this.update({status:"error",error:i});return}this.update({status:"waiting-sender",error:i}),await new Promise(r=>setTimeout(r,3e3)),this.signal=this.signal.clone()}}async onSignal(e){if(e.kind==="offer"){if(this.channel?.readyState==="open"||this.state.status==="receiving"||this.state.status==="complete")return;let t=++this.offerGen;try{this.pc?.close()}catch{}this.pc=null,this.pendingIce=[],this.channel=null;let i=await J();if(t!==this.offerGen||this.closed)return;let r=new RTCPeerConnection({iceServers:i});this.pc=r,this.update({status:"negotiating",error:void 0}),clearTimeout(this.connectTimer),this.connectTimer=setTimeout(()=>{this.pc===r&&this.state.status==="negotiating"&&this.update({status:"error",error:"ice-timeout"})},ke),r.onicecandidate=a=>{a.candidate&&this.pc===r&&this.signal.send({kind:"ice",candidate:V(a.candidate)})},r.onconnectionstatechange=()=>{this.pc===r&&r.connectionState==="failed"&&this.fail(this.channel?"connection-lost":"ice-failed")},r.ondatachannel=a=>this.attach(a.channel),await r.setRemoteDescription({type:"offer",sdp:e.sdp});for(let a of this.pendingIce.splice(0))await r.addIceCandidate(a).catch(()=>{});let n=await r.createAnswer();if(await r.setLocalDescription(n),t!==this.offerGen)return;this.signal.send({kind:"answer",sdp:n.sdp??""})}else e.kind==="ice"&&(this.pc?await Ae(this.pc,e,this.pendingIce):this.pendingIce.push(e.candidate))}attach(e){this.channel=e,e.binaryType="arraybuffer",e.onmessage=t=>{let i=t.data;this.lastData=Date.now(),this.queue=this.queue.then(()=>this.handle(i)).catch(r=>this.fail(String(r.message||r)))},e.onclose=()=>{["receiving","ready","password","streaming"].includes(this.state.status)&&this.fail("sender-disconnected")}}sendProgress(e=!1){let t=Date.now();if(!e&&t-this.lastProgressSent<ot)return;this.lastProgressSent=t;let i=(t-this.speedRef.at)/1e3;if(i>=.5){let r=(this.state.receivedBytes-this.speedRef.bytes)/i;this.speedRef={at:t,bytes:this.state.receivedBytes},this.update({speed:this.state.speed?this.state.speed*.7+r*.3:r})}this.channel?.readyState==="open"&&this.channel.send(JSON.stringify({type:"progress",bytes:this.state.receivedBytes}))}async handle(e){if(this.state.status==="error")return;if(typeof e=="string"){let n=H(e);if(!n)return;if(n.type==="manifest"&&n.v===3&&n.auth)clearTimeout(this.connectTimer),this.challenge=n.auth,this.pwMix=null,this.update({status:"password",peerDevice:n.device,authChecking:!1,authRemaining:5});else if(n.type==="auth-fail")this.pwMix=null,this.update({authChecking:!1,authError:!0,authRemaining:Math.max(0,Number(n.remaining)||0)});else if(n.type==="auth-ok")this.update({authError:!1});else if(n.type==="manifest"){if(clearTimeout(this.connectTimer),n.protected&&!this.pwMix)throw new Error("auth-required");let a=n.v===4;if(n.v!==2&&!a||n.chunkSize!==65536)throw new Error("unsupported-protocol");if(a&&!n.expires)throw new Error("unsupported-protocol");if(this.linkExpires&&this.linkExpires!==n.expires)throw new Error("integrity-error");if(!this.linkExpires&&this.secret&&a)throw new Error("integrity-error");if(a){if(Date.now()>=n.expires*1e3)throw new Error("expired");this.armExpiry(n.expires)}let o=this.secret??await pe(this.code,n.wrappedSecret);this.key=await O(o,n.protected?this.pwMix:void 0,a?n.expires:void 0),this.update({status:"ready",authChecking:!1,authError:!1,files:n.files,message:n.message,peerDevice:n.device,streamable:n.streamable===!0,...a?{expires:n.expires}:{},totalBytes:n.files.reduce((c,l)=>c+l.size,0)}),this.channel?.send(JSON.stringify({type:"hello",device:C()}))}else if(n.type==="file-end"){let a=this.state.files[n.index];if(!a||n.index!==this.expected.file||n.chunks!==R(a.size)||this.expected.chunk!==n.chunks)throw new Error("integrity-error");n.chunks===0&&await this.sink.open(n.index,a),await this.sink.close(),this.expected={file:n.index+1,chunk:0},this.sendProgress(!0),this.channel?.send(JSON.stringify({type:"file-ack",index:n.index}))}else if(n.type==="done"){if(this.expected.file!==this.state.files.length)throw new Error("integrity-error");clearInterval(this.stallTimer),this.channel?.send(JSON.stringify({type:"ack",bytes:this.state.receivedBytes})),this.update({status:"complete",receivedBytes:this.state.totalBytes}),setTimeout(()=>this.close(),5e3)}else n.type==="cancel"&&this.fail(n.reason==="sender-stopped"||n.reason==="expired"?n.reason:n.reason||"cancelled");return}if(this.streamReader){await this.streamReader.onFrame(e).catch(()=>{throw new Error("decrypt-error")});return}if(!this.key||!this.sink)throw new Error("unexpected-data");let{fileIndex:t,chunkIndex:i,plain:r}=await A(this.key,e).catch(()=>{throw new Error("decrypt-error")});if(t!==this.expected.file||i!==this.expected.chunk)throw new Error("integrity-error");i===0&&await this.sink.open(t,this.state.files[t]),await this.sink.write(r),this.expected.chunk+=1,this.state={...this.state,receivedBytes:this.state.receivedBytes+r.byteLength},this.onState(this.state),this.sendProgress()}async submitPassword(e){if(this.state.status!=="password"||!this.challenge||this.state.authChecking)return;if(!this.channel||this.channel.readyState!=="open"){this.fail("sender-disconnected");return}this.update({authChecking:!0,authError:!1});let t=this.challenge,i=await $(e,t.salt,t.iter),r=await oe(i,this.code,t.nonce);this.pwMix=i.mix,this.channel.send(JSON.stringify({type:"auth",proof:r}))}startStreaming(){if(this.streamReader)return this.streamReader;if(this.state.status!=="ready"||!this.state.streamable||!this.key)throw new Error("not-streamable");if(!this.channel||this.channel.readyState!=="open")throw new Error("sender-disconnected");let e=this.channel;return this.streamReader=new I(this.key,this.state.files,t=>e.readyState==="open"&&e.send(JSON.stringify(t)),t=>this.update({receivedBytes:t})),this.update({status:"streaming",receivedBytes:0}),this.streamReader}download(e){if(this.state.status!=="ready")throw new Error("not-ready");if(!this.channel||this.channel.readyState!=="open"){this.fail("sender-disconnected");return}this.sink=e,this.expected={file:0,chunk:0},this.lastData=Date.now(),this.speedRef={at:Date.now(),bytes:0},this.update({status:"receiving",receivedBytes:0,speed:0}),this.channel.send(JSON.stringify({type:"start"})),clearInterval(this.stallTimer),this.stallTimer=setInterval(()=>{if(this.state.status!=="receiving")return clearInterval(this.stallTimer);Date.now()-this.lastData>this.stallTimeoutMs&&this.fail("stalled")},1e3)}close(){this.closed=!0,clearTimeout(this.expiryTimer),clearTimeout(this.connectTimer),clearInterval(this.stallTimer),this.signal.close();try{this.pc?.close()}catch{}}};var Re=`filenymous 0.3.0 \u2014 end-to-end encrypted P2P file sharing (no server storage)
|
|
4
4
|
|
|
5
5
|
Usage:
|
|
6
6
|
filenymous upload <file...> [options] share files; stays online until done
|
|
@@ -12,9 +12,12 @@ upload options:
|
|
|
12
12
|
--password <pw> require a password (prefer env FILENYMOUS_PASSWORD)
|
|
13
13
|
--webhook <url> POST JSON events (share.created, download.completed, share.expired)
|
|
14
14
|
--webhook-no-link omit link and code from webhook payloads
|
|
15
|
+
--expires <iso-date> bind an expiry into the link (e.g. 2026-10-20T18:00:00Z);
|
|
16
|
+
recipients see a countdown, nobody can connect after it
|
|
17
|
+
--stream let web recipients play audio/video while it downloads
|
|
15
18
|
--allow-empty exit 0 even if the TTL expires with zero downloads (default exit 2)
|
|
16
19
|
--json machine-readable JSON lines on stdout
|
|
17
|
-
--app-url <url> web app base for links (default ${
|
|
20
|
+
--app-url <url> web app base for links (default ${G})
|
|
18
21
|
|
|
19
22
|
receive options:
|
|
20
23
|
-o, --output <dir> destination directory (default .)
|
|
@@ -23,14 +26,14 @@ receive options:
|
|
|
23
26
|
--json machine-readable JSON lines on stdout
|
|
24
27
|
|
|
25
28
|
common:
|
|
26
|
-
--signal <wss-url> signaling relay (default ${
|
|
29
|
+
--signal <wss-url> signaling relay (default ${q})
|
|
27
30
|
-q, --quiet no progress on stderr
|
|
28
31
|
-h, --help, -v, --version
|
|
29
|
-
`,{values:
|
|
30
|
-
`),
|
|
31
|
-
`),
|
|
32
|
-
`);async function
|
|
33
|
-
`),
|
|
34
|
-
`);
|
|
35
|
-
`),0;let[
|
|
36
|
-
`),process.stdout.write(
|
|
32
|
+
`,{values:d,positionals:dt}=ct({allowPositionals:!0,options:{ttl:{type:"string"},"max-downloads":{type:"string"},password:{type:"string"},webhook:{type:"string"},"webhook-no-link":{type:"boolean"},"allow-empty":{type:"boolean"},stream:{type:"boolean"},expires:{type:"string"},json:{type:"boolean"},"app-url":{type:"string"},signal:{type:"string"},output:{type:"string",short:"o"},timeout:{type:"string"},quiet:{type:"boolean",short:"q"},help:{type:"boolean",short:"h"},version:{type:"boolean",short:"v"}}}),m=s=>process.stderr.write(`${s}
|
|
33
|
+
`),B=s=>d.json&&process.stdout.write(`${JSON.stringify(s)}
|
|
34
|
+
`),P=0;function Ce(s,e,t){if(d.quiet||!process.stderr.isTTY&&Date.now()-P<2e3||Date.now()-P<250&&e<t)return;P=Date.now();let i=t?Math.floor(e/t*100):100,r=`${s} ${i}% (${E(e)} / ${E(t)})`;process.stderr.isTTY?process.stderr.write(`\r\x1B[2K${r}`):m(r)}var N=()=>process.stderr.isTTY&&!d.quiet&&P&&process.stderr.write(`
|
|
35
|
+
`);async function pt(s,e){try{let t=new AbortController,i=setTimeout(()=>t.abort(),5e3),r=await fetch(s,{method:"POST",headers:{"content-type":"application/json","user-agent":"filenymous-cli/0.3.0"},body:JSON.stringify(e),signal:t.signal});clearTimeout(i),r.ok||m(`warning: webhook answered HTTP ${r.status}`)}catch(t){m(`warning: webhook failed: ${t.message}`)}}async function ut(s){if(!s.length)throw new Error("upload: missing file (try: filenymous upload app.zip --ttl 1h)");let e;if(d.expires){let p=Date.parse(d.expires);if(!Number.isFinite(p)||p<=Date.now()+2e3)throw new Error("--expires must be an ISO date in the future");e=Math.floor(p/1e3)}let t=e&&!d.ttl?e*1e3-Date.now():Math.min(W(d.ttl??"1h"),e?e*1e3-Date.now():1/0),i=Number(d["max-downloads"]??1);if(!Number.isInteger(i)||i<1)throw new Error("--max-downloads must be a positive integer");let r=d.password??process.env.FILENYMOUS_PASSWORD??void 0,n=[];for(let p of s)n.push(await ee(_e(p)));let a=n.reduce((p,k)=>p+k.size,0),o=n.length===1?n[0].name:n.map(p=>p.name).join(", "),c=new Date(e?e*1e3:Date.now()+t).toISOString(),l=0,h=()=>{},w=new Promise(p=>h=p),g=new D(n,p=>{p.status==="transferring"&&Ce("sending",p.confirmedBytes,p.totalBytes),p.downloads>l?(l=p.downloads,N(),P=0,d.quiet||m(`\u2713 download ${l}/${i} completed${p.peerDevice?` (${p.peerDevice})`:""}`),S("download.completed").then(()=>l>=i&&setTimeout(()=>h(0),1600))):p.status==="failed"&&p.error==="too-many-attempts"?(m("\u2717 too many wrong passwords: share locked"),S("share.failed",p.error).then(()=>h(1))):p.status==="failed"&&p.error&&!d.quiet&&(N(),P=0,m(`\u2026 transfer interrupted (${p.error}); still waiting for a recipient`))},{password:r,stream:!!d.stream,expires:e}),u=ne(d["app-url"]??process.env.FILENYMOUS_APP_URL??G,g.code,g.secret,e);async function S(p,k){let X={event:p,link:u,code:g.code,file:o,size:a,expiresAt:c,downloads:l,maxDownloads:i,protected:!!r,...k?{error:k}:{}};B(X),d.webhook&&await pt(d.webhook,me(X,!!d["webhook-no-link"]))}await g.start(),d.json||process.stdout.write(`${u}
|
|
36
|
+
`),d.quiet||(m(`Filenymous \u2014 ${o} (${E(a)})${r?", password-protected":""}`),m(`code: ${g.code} expires: ${c} downloads: 0/${i}`),m("Keep this process running: files go directly to the recipient, nothing is stored on a server.")),await S("share.created");let Ie=setTimeout(()=>{S("share.expired").then(()=>h(l>0||d["allow-empty"]?0:2))},t),j=()=>h(130);process.once("SIGINT",j),process.once("SIGTERM",j);let Z=await w;clearTimeout(Ie),Z===2&&!d.quiet&&m("share expired with no download"),g.stop();for(let p of n)await p.close();return Z}function ht(s){let e=lt({input:process.stdin,output:process.stderr,terminal:!0});return new Promise(t=>e.question(s,i=>{e.close(),t(i)}))}async function ft(s){let e=se(s??"");if(!e)throw new Error("receive: give a Filenymous link (\u2026#p2p:123-456-ABC/secret) or a code");let t=_e(d.output??"."),i=W(d.timeout??"30m"),r=d.password??process.env.FILENYMOUS_PASSWORD??void 0,n=new _(t),a=()=>{},o=new Promise(u=>a=u),c=!1,l=!1,h=new L(e.code,e.secret,u=>{switch(u.status){case"waiting-sender":!c&&!d.quiet&&m(`waiting for the sender of ${e.code}\u2026`),c=!0;break;case"password":if(u.authChecking)break;if(u.authError&&l&&(d.password||process.env.FILENYMOUS_PASSWORD||!process.stdin.isTTY)){m(`\u2717 wrong password (${u.authRemaining??0} attempts left)`),a(1);break}(async()=>{if(!r||u.authError){if(!process.stdin.isTTY)return m("\u2717 this share needs a password (--password or FILENYMOUS_PASSWORD)"),a(1);r=await ht(u.authError?`wrong password (${u.authRemaining} left), try again: `:"password: ")}l=!0,await h.submitPassword(r)})();break;case"ready":d.quiet||m(`receiving ${u.files.length} file(s), ${E(u.totalBytes)}${u.peerDevice?` from ${u.peerDevice}`:""} \u2192 ${t}`),h.download(n);break;case"receiving":Ce("receiving",u.receivedBytes,u.totalBytes);break;case"complete":N();for(let S of n.written)d.json?B({event:"file.received",path:S}):process.stdout.write(`${S}
|
|
37
|
+
`);B({event:"download.completed",files:n.written,size:u.totalBytes}),setTimeout(()=>a(0),5200);break;case"error":N(),m(u.error==="expired"?"\u2717 this link has expired":`\u2717 ${u.error??"error"}`),B({event:"error",error:u.error}),a(1);break}},void 0,void 0,e.expires),w=setTimeout(()=>{m("\u2717 timeout"),a(1)},i);process.once("SIGINT",()=>a(130)),h.start();let g=await o;return clearTimeout(w),h.close(),g}async function mt(){if(d.version)return process.stdout.write(`0.3.0
|
|
38
|
+
`),0;let[s,...e]=dt;return d.help||!s?(process.stdout.write(Re),s||d.help?0:1):(Q(d.signal??process.env.FILENYMOUS_SIGNAL_URL??q),s==="upload"||s==="send"?ut(e):s==="receive"||s==="get"?ft(e[0]):(m(`unknown command: ${s}
|
|
39
|
+
`),process.stdout.write(Re),1))}mt().then(s=>process.exit(s),s=>{m(`\u2717 ${s.message||s}`),process.exit(1)});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "filenymous",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Ultra-light end-to-end encrypted P2P file sharing from the terminal (WebRTC, no server storage). Compatible with https://filenymous.eu/app/",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|