filenymous 0.1.0 → 0.2.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 +2 -0
- package/dist/filenymous.js +12 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ 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é »).
|
|
33
34
|
- `--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
35
|
- Codes de sortie :
|
|
35
36
|
- `0` : au moins un téléchargement terminé (ou `--allow-empty`)
|
|
@@ -74,6 +75,7 @@ filenymous upload <file...> [--ttl 1h] [--max-downloads 1] [--password X] \
|
|
|
74
75
|
[--webhook URL] [--webhook-no-link] [--allow-empty] [--json] [-q]
|
|
75
76
|
```
|
|
76
77
|
- The link goes to **stdout** and progress to stderr. With `--json`, stdout carries JSON lines events.
|
|
78
|
+
- `--stream`: web recipients can play audio/video while it downloads ("Streamed sharing").
|
|
77
79
|
- Prefer `FILENYMOUS_PASSWORD` over `--password`, so the password stays out of shell history and CI logs.
|
|
78
80
|
- Exit codes:
|
|
79
81
|
- `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 at}from"node:util";import{createInterface as ot}from"node:readline";import{resolve as _e}from"node:path";import{RTCPeerConnection as Re,RTCSessionDescription as Ie,RTCIceCandidate as Me,RTCDataChannel as De}from"node-datachannel/polyfill";import Le from"ws";function Be(s){return s.replace(/^a=/,"")}var N=class extends Re{pendingLocal=0;remoteSet=!1;held=[];userHandler=null;constructor(e){super(e);let t=n=>{let r=n.candidate,i={candidate:r?{candidate:Be(r.candidate),sdpMid:r.sdpMid??"0",sdpMLineIndex:r.sdpMLineIndex??0}:null};this.pendingLocal>0||!this.remoteSet?this.held.push(i):this.userHandler?.(i)};Object.defineProperty(this,"onicecandidate",{configurable:!0,get:()=>this.userHandler?t:null,set:n=>{this.userHandler=n}})}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=N,e.RTCSessionDescription??=Ie,e.RTCIceCandidate??=Me,e.RTCDataChannel??=De,e.WebSocket||(e.WebSocket=Le),e.__FILENYMOUS_SIGNAL_URL__=s}import{open as Ue,stat as Fe}from"node:fs/promises";import{basename as Ne,extname as Oe}from"node:path";var $e={".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 Fe(s);if(!e.isFile())throw new Error(`not a regular file: ${s}`);let t=await Ue(s,"r"),n=e.size;return{name:Ne(s),size:n,type:$e[Oe(s).toLowerCase()]??"application/octet-stream",slice(r,i){let a=Math.max(0,Math.min(n,r)),o=Math.max(a,Math.min(n,i));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 We,existsSync as qe,mkdirSync as Ge,rmSync as ze}from"node:fs";import{extname as Ye,join as fe,basename as Je}from"node:path";var Ke=/^(\d{3})(\d{3})([A-Z]{3})$/,te="ABCDEFGHJKLMNPQRSTUVWXYZ";function S(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 b(s){let e=s.replace(/-/g,"+").replace(/_/g,"/").padEnd(Math.ceil(s.length/4)*4,"="),t=atob(e),n=new Uint8Array(t.length);for(let r=0;r<t.length;r++)n[r]=t.charCodeAt(r);return n}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(n=>te[n%te.length]).join("");return`${e.slice(0,3)}-${e.slice(3)}-${t}`}function He(s){let t=String(s||"").toUpperCase().replace(/[^0-9A-Z]/g,"").match(Ke);return t?`${t[1]}-${t[2]}-${t[3]}`:null}function ne(){return S(crypto.getRandomValues(new Uint8Array(16)))}function se(s,e,t){return`${s.split("#")[0]}#p2p:${e}${t?`/${t}`:""}`}function ie(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[n,r]=e.split("/"),i=He(n??"");if(!i)return null;let a=r&&/^[A-Za-z0-9_-]{16,64}$/.test(r)?r:void 0;return a?{code:i,secret:a}:{code:i}}function O(s=16){return S(crypto.getRandomValues(new Uint8Array(s)))}async function $(s,e,t=31e4){let n=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(b(e)),iterations:t,hash:"SHA-256"},n,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 S(new Uint8Array(await crypto.subtle.sign("HMAC",s.auth,ae(e,t))))}async function ce(s,e,t,n){try{return await crypto.subtle.verify("HMAC",s.auth,f(b(n)),ae(e,t))}catch{return!1}}async function x(s,e){let t=b(s),n=t;e&&(n=new Uint8Array(t.length+e.length),n.set(t),n.set(e,t.length));let r=await crypto.subtle.importKey("raw",f(n),"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"))},r,{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)),n=await crypto.subtle.encrypt({name:"AES-GCM",iv:t},await le(s),f(b(e)));return`${S(t)}.${S(new Uint8Array(n))}`}async function pe(s,e){let[t,n]=String(e||"").split(".");if(!t||!n)throw new Error("invalid wrapped secret");let r=await crypto.subtle.decrypt({name:"AES-GCM",iv:f(b(t))},await le(s),f(b(n)));return S(new Uint8Array(r))}function ue(s,e){let t=new Uint8Array(12),n=new DataView(t.buffer);return n.setUint32(0,s),n.setUint32(4,Math.floor(e/2**32)),n.setUint32(8,e>>>0),t}async function K(s,e,t,n){let r=new Uint8Array(8),i=new DataView(r.buffer);i.setUint32(0,e),i.setUint32(4,t);let a=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:f(ue(e,t)),additionalData:f(r)},s,f(n))),o=new Uint8Array(8+a.byteLength);return o.set(r),o.set(a,8),o.buffer}async function _(s,e){let t=new Uint8Array(e);if(t.byteLength<24)throw new Error("short frame");let n=t.subarray(0,8),r=new DataView(t.buffer,t.byteOffset,8),i=r.getUint32(0),a=r.getUint32(4),o=new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:f(ue(i,a)),additionalData:f(n)},s,f(t.subarray(8))));return{fileIndex:i,chunkIndex:a,plain:o}}function C(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))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:n,to:r}=e;if(![t,n,r].every(i=>Number.isSafeInteger(i)&&i>=0)||r<n||r-n>=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 je(s,e,t=qe){let n=he(e),r=Ye(n),i=r?n.slice(0,-r.length):n,a=fe(s,n);for(let o=1;t(a);o++)a=fe(s,`${i} (${o})${r}`);return a}var R=class{constructor(e){this.dir=e;Ge(e,{recursive:!0})}kind="fs-dir";written=[];stream=null;path="";async open(e,t){this.path=je(this.dir,Je(t.name)),this.stream=We(this.path,{flags:"wx"}),await new Promise((n,r)=>this.stream.once("open",()=>n()).once("error",r))}async write(e){let t=this.stream;t.write(e)||await new Promise(n=>t.once("drain",()=>n()))}async close(){let e=this.stream;e&&(await new Promise((t,n)=>e.end(r=>r?n(r):t())),this.written.push(this.path),this.stream=null)}async abort(){let e=this.stream;if(this.stream=null,e){e.destroy();try{ze(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]),n=(e[2]??"s").toLowerCase(),r=t*{ms:1,s:1e3,m:6e4,h:36e5,d:864e5}[n];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:n,...r}=s;return r}function E(s){let e=["B","KB","MB","GB","TB"],t=0,n=s;for(;n>=1024&&t<e.length-1;)n/=1024,t++;return`${t?n.toFixed(1):n} ${e[t]}`}var q="https://filenymous.eu/app/",G="wss://www.opendpe.net/filenymous-signal/";import{platform as ye}from"node:os";function I(){return`Filenymous CLI \xB7 ${{linux:"Linux",darwin:"macOS",win32:"Windows"}[ye()]??ye()}`}var Ve="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":Ve}function Ze(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),n=await fetch(`${Ze()}/ice`,{signal:e.signal,cache:"no-store"});if(clearTimeout(t),!n.ok)throw new Error(String(n.status));let r=await n.json();return r.iceServers?.length?r.iceServers:ge}catch{return ge}})()),z}var A=class s{constructor(e,t,n,r=Y()){this.code=e;this.role=t;this.handlers=n;this.url=r}ws=null;closed=!1;attempt=0;queue=[];connect(){return new Promise((e,t)=>{let n=!1,r=new WebSocket(this.url);this.ws=r;let i=setTimeout(()=>{if(!n){n=!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,n||(n=!0,clearTimeout(i),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":n?this.handlers.onError?.(o.error||"signal-error"):(n=!0,clearTimeout(i),this.closed=!0,r.close(),t(new Error(o.error||"signal-error")));break;default:break}},r.onerror=()=>{n||(n=!0,clearTimeout(i),t(new Error("signal-unreachable")))},r.onclose=()=>{if(this.ws!==r||this.closed||!n)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 j(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,n=65536){let r=Math.max(0,t-1),i=Math.min(Math.max(0,s),r),a=Math.min(Math.max(i,e),r);return{from:Math.floor(i/n),to:Math.floor(a/n)}}var Qe=256,et=32;function tt(s,e,t){let n=[],r=-1;for(let i=s;i<=e+1;i++){let a=i<=e&&!t(i);a&&r<0&&(r=i),r>=0&&(!a||i-r===64)&&(n.push([r,i-1]),r=a?i:-1)}return n}var M=class{constructor(e,t,n,r){this.key=e;this.files=t;this.send=n;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:n,plain:r}=await _(this.key,e),i=`${t}:${n}`;for(this.requested.delete(i),this.received+=r.byteLength,this.onBytes?.(this.received),this.cache.delete(i),this.cache.set(i,r);this.cache.size>Qe;)this.cache.delete(this.cache.keys().next().value);let a=this.waiters.get(i);if(a){this.waiters.delete(i);for(let o of a)o(r)}}request(e,t,n,r=!1){let i=a=>this.cache.has(`${e}:${a}`)||this.requested.has(`${e}:${a}`);for(let[a,o]of tt(t,n,i)){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 n=`${e}:${t}`,r=this.cache.get(n);return r?(this.cache.delete(n),this.cache.set(n,r),Promise.resolve(r)):new Promise(i=>{let a=this.waiters.get(n)??[];a.push(i),this.waiters.set(n,a)})}async read(e,t,n){let r=this.files[e];if(!r)throw new Error("bad-file");if(r.size===0)return new Uint8Array(0);let{from:i,to:a}=Se(t,n,r.size);this.request(e,i,a);let o=Math.ceil(r.size/65536)-1;a<o&&this.request(e,a+1,Math.min(o,a+et),!0);let c=await Promise.all(Array.from({length:a-i+1},(v,d)=>this.chunk(e,i+d))),l=new Uint8Array(Math.min(n,r.size-1)-t+1),h=0,y=t-i*65536;for(let v of c){let d=v.subarray(y,y+(l.length-h));l.set(d,h),h+=d.length,y=0}return l}};var nt=4*1024*1024,Ae=1*1024*1024,Te=45e3,st=3e4,it=300;function w(s,e){s.readyState==="open"&&s.send(JSON.stringify(e))}function be(s){return s.bufferedAmount<=nt?Promise.resolve():new Promise((e,t)=>{let n=setInterval(()=>{s.readyState!=="open"?i():s.bufferedAmount<=Ae&&r()},200),r=()=>{a(),e()},i=()=>{a(),t(new Error("connection-lost"))},a=()=>{clearInterval(n),s.removeEventListener("bufferedamountlow",r),s.removeEventListener("close",i)};s.addEventListener("bufferedamountlow",r),s.addEventListener("close",i)})}async function ke(s,e,t){if(e.kind==="ice"){if(!s.remoteDescription){t.push(e.candidate);return}try{await s.addIceCandidate(e.candidate)}catch{}}}var L=class{constructor(e,t,n={}){this.files=e;this.onState=t;this.streamable=!!n.stream,n.password&&(this.pwSalt=O(16),this.pwKeys=$(n.password,this.pwSalt)),this.code=n.code??re(),this.secret=n.secret??ne(),this.message=n.message,this.state={status:"starting",code:this.code,secret:this.secret,sentBytes:0,confirmedBytes:0,totalBytes:e.reduce((r,i)=>r+i.size,0),speed:0,fileStates:e.map(()=>"pending"),downloads:0,log:[],protected:!!n.password,authFailures:0},this.signal=new A(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})},n.signalUrl)}code;secret;message;pwKeys;pwSalt;authFailures=0;signal;attempt=null;pendingIce=[];state;stopped=!1;nextId=1;offerGen=0;connectTimer;streamable;fileKey;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,n){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,...n?{reason:n}:{}};this.update({log:[r,...this.state.log].slice(0,50),downloads:this.state.downloads+(t==="complete"?1:0),status:t==="complete"?"complete":"failed",error:n,...t==="complete"?{confirmedBytes:this.state.totalBytes,fileStates:this.files.map(()=>"delivered")}:{fileStates:this.state.fileStates.map(i=>i==="delivered"?i:"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 n=new RTCPeerConnection({iceServers:t}),r=n.createDataChannel("filenymous-p2p-v2",{ordered:!0});r.binaryType="arraybuffer",r.bufferedAmountLowThreshold=Ae;let i={id:this.nextId++,pc:n,channel:r,open:!1,started:!1,authed:!1,nonce:"",finished:!1,startedAt:0,lastConfirm:{at:0,bytes:0}};this.attempt=i,n.onicecandidate=o=>{o.candidate&&this.attempt===i&&this.signal.send({kind:"ice",candidate:j(o.candidate)})},n.onconnectionstatechange=()=>{this.attempt===i&&n.connectionState==="failed"&&this.fail(i,i.open?"connection-lost":"ice-failed")},r.onopen=()=>{this.attempt===i&&(i.open=!0,clearTimeout(this.connectTimer),this.serve(i))},r.onclose=()=>{this.attempt===i&&!i.finished&&this.fail(i,i.started?"connection-lost":"recipient-left")};let a=await n.createOffer();await n.setLocalDescription(a),this.attempt===i&&this.signal.send({kind:"offer",sdp:a.sdp??""})}async onSignal(e){let t=this.attempt;if(!t)return;let n=t.pc;if(e.kind==="answer"&&n.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")},Te);try{await n.setRemoteDescription({type:"answer",sdp:e.sdp})}catch{this.attempt===t&&this.offer();return}for(let r of this.pendingIce.splice(0))await n.addIceCandidate(r).catch(()=>{})}else e.kind==="ice"&&await ke(n,e,this.pendingIce)}async serve(e){let{channel:t}=e;if(t.onmessage=n=>{if(typeof n.data!="string"||this.attempt!==e)return;let r=H(n.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 i=Date.now(),a=Math.min(this.state.totalBytes,Math.max(0,Number(r.bytes)||0)),o=(i-e.lastConfirm.at)/1e3,c=o>0?(a-e.lastConfirm.bytes)/o:0;e.lastConfirm={at:i,bytes:a},this.update({confirmedBytes:a,speed:this.state.speed?this.state.speed*.7+c*.3:c});break}case"file-ack":{let i=[...this.state.fileStates];r.index>=0&&r.index<i.length&&(i[r.index]="delivered"),this.update({fileStates:i});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){w(t,{type:"cancel",reason:"too-many-attempts"}),this.fail(e,"too-many-attempts");return}e.nonce=O(16),w(t,{type:"manifest",v:3,chunkSize:65536,files:[{name:"locked",size:0,mime:""}],wrappedSecret:"",auth:{salt:this.pwSalt,iter:31e4,nonce:e.nonce},device:I()}),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 n=typeof t=="string"&&await ce(await this.pwKeys,this.code,e.nonce,t);if(this.attempt!==e)return;if(n){e.authed=!0,w(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}),w(e.channel,{type:"auth-fail",remaining:r}),r===0&&(w(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;w(t,{type:"manifest",v:2,chunkSize:65536,files:this.files.map(n=>({name:n.name,size:n.size,mime:n.type||"application/octet-stream"})),wrappedSecret:await de(this.code,this.secret),...this.message?{message:this.message}:{},device:I(),...this.pwKeys?{protected:!0}:{},...this.streamable?{streamable:!0}:{}}),this.attempt===e&&this.update({status:"connected",error:void 0})}key(){return this.fileKey??=(async()=>x(this.secret,this.pwKeys?(await this.pwKeys).mix:void 0))(),this.fileKey}queueRange(e,t,n,r,i){let a=this.files[t];if(!a)return;let o=C(a.size)-1,c=i?e.prefetch??=[]:e.demand??=[];for(let l=n;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(),n=Number(globalThis.__FILENYMOUS_TEST_CHUNK_DELAY_MS__)||0,r=new Set;for(;;){let i=e.demand?.shift()??e.prefetch?.shift();if(!i)break;let[a,o]=i,c=`${a}:${o}`;if(r.has(c))continue;if(r.add(c),r.size>4096&&r.clear(),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),n&&await new Promise(h=>setTimeout(h,n)),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 n=await x(this.secret,this.pwKeys?(await this.pwKeys).mix:void 0),r=0,i=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=C(o.size);for(let h=0;h<l;h++){if(this.attempt!==e||t.readyState!=="open")throw new Error("connection-lost");let y=new Uint8Array(await o.slice(h*65536,(h+1)*65536).arrayBuffer());await be(t),t.send(await K(n,a,h,y)),r+=y.byteLength,(Date.now()-i>150||r===this.state.totalBytes)&&(i=Date.now(),this.update({sentBytes:r}))}w(t,{type:"file-end",index:a,chunks:l})}w(t,{type:"done"}),this.update({sentBytes:r})}catch(n){this.fail(e,String(n.message||n))}}stop(){this.stopped=!0;let e=this.attempt;if(e?.started&&!e.finished&&this.record(e,"failed","sender-stopped"),clearTimeout(this.connectTimer),e){try{w(e.channel,{type:"cancel",reason:"sender-stopped"})}catch{}try{e.pc.close()}catch{}}this.attempt=null,this.signal.close(),this.update({status:"stopped"})}},B=class{constructor(e,t,n,r,i=st){this.code=e;this.secret=t;this.onState=n;this.stallTimeoutMs=i;this.signal=new A(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})}},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})}}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 n=String(t.message||t);if(n!=="role-taken"||e>200){this.update({status:"error",error:n});return}this.update({status:"waiting-sender",error:n}),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 n=await J();if(t!==this.offerGen||this.closed)return;let r=new RTCPeerConnection({iceServers:n});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"})},Te),r.onicecandidate=a=>{a.candidate&&this.pc===r&&this.signal.send({kind:"ice",candidate:j(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 i=await r.createAnswer();if(await r.setLocalDescription(i),t!==this.offerGen)return;this.signal.send({kind:"answer",sdp:i.sdp??""})}else e.kind==="ice"&&(this.pc?await ke(this.pc,e,this.pendingIce):this.pendingIce.push(e.candidate))}attach(e){this.channel=e,e.binaryType="arraybuffer",e.onmessage=t=>{let n=t.data;this.lastData=Date.now(),this.queue=this.queue.then(()=>this.handle(n)).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<it)return;this.lastProgressSent=t;let n=(t-this.speedRef.at)/1e3;if(n>=.5){let r=(this.state.receivedBytes-this.speedRef.bytes)/n;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 i=H(e);if(!i)return;if(i.type==="manifest"&&i.v===3&&i.auth)clearTimeout(this.connectTimer),this.challenge=i.auth,this.pwMix=null,this.update({status:"password",peerDevice:i.device,authChecking:!1,authRemaining:5});else if(i.type==="auth-fail")this.pwMix=null,this.update({authChecking:!1,authError:!0,authRemaining:Math.max(0,Number(i.remaining)||0)});else if(i.type==="auth-ok")this.update({authError:!1});else if(i.type==="manifest"){if(clearTimeout(this.connectTimer),i.protected&&!this.pwMix)throw new Error("auth-required");if(i.v!==2||i.chunkSize!==65536)throw new Error("unsupported-protocol");let a=this.secret??await pe(this.code,i.wrappedSecret);this.key=await x(a,i.protected?this.pwMix:void 0),this.update({status:"ready",authChecking:!1,authError:!1,files:i.files,message:i.message,peerDevice:i.device,streamable:i.streamable===!0,totalBytes:i.files.reduce((o,c)=>o+c.size,0)}),this.channel?.send(JSON.stringify({type:"hello",device:I()}))}else if(i.type==="file-end"){let a=this.state.files[i.index];if(!a||i.index!==this.expected.file||i.chunks!==C(a.size)||this.expected.chunk!==i.chunks)throw new Error("integrity-error");i.chunks===0&&await this.sink.open(i.index,a),await this.sink.close(),this.expected={file:i.index+1,chunk:0},this.sendProgress(!0),this.channel?.send(JSON.stringify({type:"file-ack",index:i.index}))}else if(i.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 i.type==="cancel"&&this.fail(i.reason==="sender-stopped"?"sender-stopped":i.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:n,plain:r}=await _(this.key,e).catch(()=>{throw new Error("decrypt-error")});if(t!==this.expected.file||n!==this.expected.chunk)throw new Error("integrity-error");n===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,n=await $(e,t.salt,t.iter),r=await oe(n,this.code,t.nonce);this.pwMix=n.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 M(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.connectTimer),clearInterval(this.stallTimer),this.signal.close();try{this.pc?.close()}catch{}}};var xe=`filenymous 0.2.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,10 @@ 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
|
+
--stream let web recipients play audio/video while it downloads
|
|
15
16
|
--allow-empty exit 0 even if the TTL expires with zero downloads (default exit 2)
|
|
16
17
|
--json machine-readable JSON lines on stdout
|
|
17
|
-
--app-url <url> web app base for links (default ${
|
|
18
|
+
--app-url <url> web app base for links (default ${q})
|
|
18
19
|
|
|
19
20
|
receive options:
|
|
20
21
|
-o, --output <dir> destination directory (default .)
|
|
@@ -23,14 +24,14 @@ receive options:
|
|
|
23
24
|
--json machine-readable JSON lines on stdout
|
|
24
25
|
|
|
25
26
|
common:
|
|
26
|
-
--signal <wss-url> signaling relay (default ${
|
|
27
|
+
--signal <wss-url> signaling relay (default ${G})
|
|
27
28
|
-q, --quiet no progress on stderr
|
|
28
29
|
-h, --help, -v, --version
|
|
29
|
-
`,{values:
|
|
30
|
-
`),
|
|
31
|
-
`),
|
|
32
|
-
`);async function
|
|
33
|
-
`),
|
|
34
|
-
`);
|
|
35
|
-
`),0;let[
|
|
36
|
-
`),process.stdout.write(
|
|
30
|
+
`,{values:p,positionals:ct}=at({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"},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}
|
|
31
|
+
`),U=s=>p.json&&process.stdout.write(`${JSON.stringify(s)}
|
|
32
|
+
`),P=0;function Ce(s,e,t){if(p.quiet||!process.stderr.isTTY&&Date.now()-P<2e3||Date.now()-P<250&&e<t)return;P=Date.now();let n=t?Math.floor(e/t*100):100,r=`${s} ${n}% (${E(e)} / ${E(t)})`;process.stderr.isTTY?process.stderr.write(`\r\x1B[2K${r}`):m(r)}var F=()=>process.stderr.isTTY&&!p.quiet&&P&&process.stderr.write(`
|
|
33
|
+
`);async function lt(s,e){try{let t=new AbortController,n=setTimeout(()=>t.abort(),5e3),r=await fetch(s,{method:"POST",headers:{"content-type":"application/json","user-agent":"filenymous-cli/0.2.0"},body:JSON.stringify(e),signal:t.signal});clearTimeout(n),r.ok||m(`warning: webhook answered HTTP ${r.status}`)}catch(t){m(`warning: webhook failed: ${t.message}`)}}async function dt(s){if(!s.length)throw new Error("upload: missing file (try: filenymous upload app.zip --ttl 1h)");let e=W(p.ttl??"1h"),t=Number(p["max-downloads"]??1);if(!Number.isInteger(t)||t<1)throw new Error("--max-downloads must be a positive integer");let n=p.password??process.env.FILENYMOUS_PASSWORD??void 0,r=[];for(let u of s)r.push(await ee(_e(u)));let i=r.reduce((u,k)=>u+k.size,0),a=r.length===1?r[0].name:r.map(u=>u.name).join(", "),o=new Date(Date.now()+e).toISOString(),c=0,l=()=>{},h=new Promise(u=>l=u),y=new L(r,u=>{u.status==="transferring"&&Ce("sending",u.confirmedBytes,u.totalBytes),u.downloads>c?(c=u.downloads,F(),P=0,p.quiet||m(`\u2713 download ${c}/${t} completed${u.peerDevice?` (${u.peerDevice})`:""}`),d("download.completed").then(()=>c>=t&&setTimeout(()=>l(0),1600))):u.status==="failed"&&u.error==="too-many-attempts"?(m("\u2717 too many wrong passwords: share locked"),d("share.failed",u.error).then(()=>l(1))):u.status==="failed"&&u.error&&!p.quiet&&(F(),P=0,m(`\u2026 transfer interrupted (${u.error}); still waiting for a recipient`))},{password:n,stream:!!p.stream}),v=se(p["app-url"]??process.env.FILENYMOUS_APP_URL??q,y.code,y.secret);async function d(u,k){let X={event:u,link:v,code:y.code,file:a,size:i,expiresAt:o,downloads:c,maxDownloads:t,protected:!!n,...k?{error:k}:{}};U(X),p.webhook&&await lt(p.webhook,me(X,!!p["webhook-no-link"]))}await y.start(),p.json||process.stdout.write(`${v}
|
|
34
|
+
`),p.quiet||(m(`Filenymous \u2014 ${a} (${E(i)})${n?", password-protected":""}`),m(`code: ${y.code} expires: ${o} downloads: 0/${t}`),m("Keep this process running: files go directly to the recipient, nothing is stored on a server.")),await d("share.created");let T=setTimeout(()=>{d("share.expired").then(()=>l(c>0||p["allow-empty"]?0:2))},e),V=()=>l(130);process.once("SIGINT",V),process.once("SIGTERM",V);let Z=await h;clearTimeout(T),Z===2&&!p.quiet&&m("share expired with no download"),y.stop();for(let u of r)await u.close();return Z}function pt(s){let e=ot({input:process.stdin,output:process.stderr,terminal:!0});return new Promise(t=>e.question(s,n=>{e.close(),t(n)}))}async function ut(s){let e=ie(s??"");if(!e)throw new Error("receive: give a Filenymous link (\u2026#p2p:123-456-ABC/secret) or a code");let t=_e(p.output??"."),n=W(p.timeout??"30m"),r=p.password??process.env.FILENYMOUS_PASSWORD??void 0,i=new R(t),a=()=>{},o=new Promise(d=>a=d),c=!1,l=!1,h=new B(e.code,e.secret,d=>{switch(d.status){case"waiting-sender":!c&&!p.quiet&&m(`waiting for the sender of ${e.code}\u2026`),c=!0;break;case"password":if(d.authChecking)break;if(d.authError&&l&&(p.password||process.env.FILENYMOUS_PASSWORD||!process.stdin.isTTY)){m(`\u2717 wrong password (${d.authRemaining??0} attempts left)`),a(1);break}(async()=>{if(!r||d.authError){if(!process.stdin.isTTY)return m("\u2717 this share needs a password (--password or FILENYMOUS_PASSWORD)"),a(1);r=await pt(d.authError?`wrong password (${d.authRemaining} left), try again: `:"password: ")}l=!0,await h.submitPassword(r)})();break;case"ready":p.quiet||m(`receiving ${d.files.length} file(s), ${E(d.totalBytes)}${d.peerDevice?` from ${d.peerDevice}`:""} \u2192 ${t}`),h.download(i);break;case"receiving":Ce("receiving",d.receivedBytes,d.totalBytes);break;case"complete":F();for(let T of i.written)p.json?U({event:"file.received",path:T}):process.stdout.write(`${T}
|
|
35
|
+
`);U({event:"download.completed",files:i.written,size:d.totalBytes}),setTimeout(()=>a(0),5200);break;case"error":F(),m(`\u2717 ${d.error??"error"}`),U({event:"error",error:d.error}),a(1);break}}),y=setTimeout(()=>{m("\u2717 timeout"),a(1)},n);process.once("SIGINT",()=>a(130)),h.start();let v=await o;return clearTimeout(y),h.close(),v}async function ht(){if(p.version)return process.stdout.write(`0.2.0
|
|
36
|
+
`),0;let[s,...e]=ct;return p.help||!s?(process.stdout.write(xe),s||p.help?0:1):(Q(p.signal??process.env.FILENYMOUS_SIGNAL_URL??G),s==="upload"||s==="send"?dt(e):s==="receive"||s==="get"?ut(e[0]):(m(`unknown command: ${s}
|
|
37
|
+
`),process.stdout.write(xe),1))}ht().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.2.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",
|