local-traffic 0.0.36 → 0.0.39

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.
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});const e=require("http2"),t=require("http"),r=require("https"),o=require("url"),n=require("fs"),s=require("zlib"),a=require("path");var i;!function(e){e[e.ERROR=124]="ERROR",e[e.SUCCESS=35]="SUCCESS",e[e.INFO=21]="INFO",e[e.WARNING=172]="WARNING"}(i||(i={}));const l=(0,a.resolve)(process.env.HOME,".local-traffic.json"),p=(0,a.resolve)(process.cwd(),process.argv.slice(-1)[0].endsWith(".json")?process.argv.slice(-1)[0]:l),c={mapping:{},port:8080,replaceResponseBodyUrls:!1,dontUseHttp2Downstream:!1,simpleLogs:!1,websocket:!1};let d,h;const u=(e,t,r)=>{console.log(`${(e=>{const t=new Date;return`${e?"":""}${`${t.getHours()}`.padStart(2,"0")}${e?":":":"}${`${t.getMinutes()}`.padStart(2,"0")}${e?":":":"}${`${t.getSeconds()}`.padStart(2,"0")}${e?"":""}`})(d.simpleLogs)} ${d.simpleLogs?e.replace(/⎸/g,"|").replace(/⎹/g,"|").replace(/\u001b\[[^m]*m/g,"").replace(/↘️/g,"inbound").replace(/☎️/g,"port").replace(/↗️/g,"outbound"):t?`[48;5;${t}m⎸ ${process.stdout.isTTY&&r||""} ${e.padEnd(36)} ⎹`:e}`)},m=async(e=!0)=>new Promise((t=>(0,n.readFile)(p,((r,o)=>{r&&!e&&u("config error. Using default value",i.ERROR,"❌");try{d=Object.assign({},c,JSON.parse((o||"{}").toString()))}catch(e){return u("config syntax incorrect, aborting",i.ERROR,"⛈️"),d=d||{...c},void t(d)}d.mapping[""]||u('default mapping "" not provided.',i.WARNING,"☢️"),r&&"ENOENT"===r.code&&e&&p===l?(0,n.writeFile)(p,JSON.stringify(c),(e=>{e?u("config file NOT created",i.ERROR,"⁉️"):u("config file created",i.SUCCESS,"✨"),t(d)})):t(d)})))).then((()=>{e&&(0,n.watchFile)(p,f)})),g=e=>{u(`⎸ ↘️ : ${e.ssl?"HTTP/2 ":"HTTP 1.1"} ⎸ ☎️ : ${e.port.toString().padStart(5)} ⎸ ↗️ : ${e.dontUseHttp2Downstream?"HTTP 1.1":"HTTP/2 "} ⎹`)},f=async()=>{const e={...d};return await m(!1),isNaN(d.port)||d.port>65535||d.port<0?(d=e,void u("port number invalid. Not refreshing",i.ERROR,"☎️")):"object"!=typeof d.mapping?(d=e,void u("mapping should be an object. Aborting",i.ERROR,"⚡")):(d.replaceResponseBodyUrls&&!e.replaceResponseBodyUrls&&u("response body url replacement",i.INFO,"✔️"),!d.replaceResponseBodyUrls&&e.replaceResponseBodyUrls&&u("response body url NO replacement",i.INFO,"✖️"),d.websocket&&!e.websocket&&u("websocket activated",i.INFO,"☄️"),!d.websocket&&e.websocket&&u("websocket deactivated",i.INFO,"☄️"),u(`${Object.keys(d.mapping).length.toString().padStart(5)} loaded mapping rules`,i.SUCCESS,"↻"),void(d.port!==e.port||JSON.stringify(d.ssl)!==JSON.stringify(e.ssl)?(await new Promise((e=>h?h.close(e):e(void 0))),S()):d.dontUseHttp2Downstream!==e.dontUseHttp2Downstream&&g(d)))},$=e=>""==e?"":(0,a.normalize)(e).replace(/\\/g,"/"),w=e=>{const t=(0,a.resolve)("/",e.hostname,...e.pathname.replace(/[?#].*$/,"").replace(/^\/+/,"").split("/"));return{error:null,data:null,hasRun:!1,run:function(){return this.hasRun?Promise.resolve():new Promise((r=>(0,n.readFile)(t,((o,s)=>{if(this.hasRun=!0,!o||"EISDIR"!==o.code)return this.error=o,this.data=s,void r(void 0);(0,n.readdir)(t,((t,o)=>{this.error=t,this.data=o,t?r(void 0):Promise.all(o.map((t=>new Promise((r=>(0,n.lstat)((0,a.resolve)(e.pathname,t),((e,o)=>r([t,o,e])))))))).then((t=>{const o=t.filter((e=>!e[2]&&e[1].isDirectory())).concat(t.filter((e=>!e[2]&&e[1].isFile())));this.data=`${b(128194,"directory",e.href)}\n <p>Directory content of <i>${e.href.replace(/\//g,"&#x002F;")}</i></p>\n <ul class="list-group">\n <li class="list-group-item">&#x1F4C1;<a href="${e.pathname.endsWith("/")?"..":"."}">&lt;parent&gt;</a></li>\n ${o.filter((e=>!e[2])).map((t=>`<li class="list-group-item">&#x${(t[1].isDirectory()?128193:128196).toString(16)};<a href="${e.pathname.endsWith("/")?"":`${e.pathname.split("/").slice(-1)[0]}/`}${t[0]}">${t[0]}</a></li>`)).join("\n")}\n </li>\n </ul>\n </body></html>`,r(void 0)}))}))}))))},events:{},on:function(e,r){return this.events[e]=r,this.run().then((()=>{"response"===e&&this.events.response({Server:"local","Content-Type":t.endsWith(".svg")?"image/svg+xml":null},0),"data"===e&&this.data&&(this.events.data(this.data),this.events.end()),"error"===e&&this.error&&this.events.error(this.error)})),this},end:function(){return this},request:function(){return this}}},b=(e,t,r)=>`<!doctype html>\n<html lang="en">\n<head>\n<title>&#x${e.toString(16)}; local-traffic ${t} | ${r}</title>\n<link href="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css" rel="stylesheet"/>\n<script src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"><\/script>\n<script src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"><\/script>\n</head>\n<body><div class="container"><h1>&#x${e.toString(16)}; local-traffic ${t}</h1>\n<br/>`,y=(e,t,r,o)=>`${b(128163,"error",e.message)}\n<p>An error happened while trying to proxy a remote exchange</p>\n<div class="alert alert-warning" role="alert">\n &#x24D8;&nbsp;This is not an error from the downstream service.\n</div>\n<div class="alert alert-danger" role="alert">\n<pre><code>${e.stack||`<i>${e.name} : ${e.message}</i>`}${e.errno?`<br/>(code : ${e.errno})`:""}</code></pre>\n</div>\nMore information about the request :\n<table class="table">\n <tbody>\n <tr>\n <td>phase</td>\n <td>${t}</td>\n </tr>\n <tr>\n <td>requested URL</td>\n <td>${r}</td>\n </tr>\n <tr>\n <td>downstream URL</td>\n <td>${o||"&lt;no-target-url&gt;"}</td>\n </tr>\n </tbody>\n</table>\n</div></body></html>`,R=(e,t,r)=>{t.writeHead(e,void 0,{"content-type":"text/html","content-length":r.length}),t.end(r)},v=e=>{const t=(e.headers[":authority"]?.toString()??e.headers.host??"localhost").replace(/:.*/,""),r=e.headers[":authority"]||`${e.headers.host}${e.headers.host.match(/:[0-9]+$/)?"":80!==d.port||d.ssl?443===d.port&&d.ssl?"":`:${d.port}`:""}`,n=new o.URL(`http${d.ssl?"s":""}://${r}${e.url}`),s=n.href.substring(n.origin.length),[a,i]=Object.entries({...Object.assign({},...Object.entries(d.mapping).map((([e,t])=>({[e]:new o.URL($(t))}))))}).find((([e])=>s.match(RegExp(e))))||[];return{proxyHostname:t,proxyHostnameAndPort:r,url:n,path:s,key:a,target:i}},S=()=>{h=(d.ssl?e.createSecureServer.bind(null,{...d.ssl,allowHTTP1:!0}):t.createServer)((async(n,a)=>{if(!n.headers.host&&!n.headers[":authority"])return void R(400,a,Buffer.from(y(new Error("client must supply a 'host' header"),"proxy",new o.URL(`http${d.ssl?"s":""}://unknowndomain${n.url}`))));const{proxyHostname:i,proxyHostnameAndPort:l,url:c,path:h,key:u,target:m}=v(n);if(!m)return void R(502,a,Buffer.from(y(new Error(`No mapping found in config file ${p}`),"proxy",c)));const g=m.host.replace(RegExp(/\/+$/),""),f=`${m.href.substring("https://".length+m.host.length)}${$(h.replace(RegExp($(u)),""))}`.replace(/^\/*/,"/"),b=new o.URL(`${m.protocol}//${g}${f}`);let S=null,O=!d.dontUseHttp2Downstream;const j="file:"===m.protocol?w(b):O?await Promise.race([new Promise((t=>{const r=(0,e.connect)(b,{rejectUnauthorized:!1,protocol:m.protocol},((e,o)=>{O=O&&!!o.alpnProtocol,t(O?r:null)}));r.on("error",(e=>{S=O&&Buffer.from(y(e,"connection",c,b))}))})),new Promise((e=>setTimeout((()=>{O=!1,e(null)}),3e3)))]):null;S instanceof Buffer||(S=null);const E={...[...Object.entries(n.headers)].filter((([e])=>!["host","connection"].includes(e.toLowerCase()))).reduce(((e,[t,r])=>(e[t]=(e[t]||"")+(Array.isArray(r)?r:[r]).map((e=>e.replace(c.hostname,g))).join(", "),e)),{}),origin:m.href,referer:b.toString(),":authority":g,":method":n.method,":path":f,":scheme":m.protocol.replace(":","")},N=j&&!S&&j.request(E,{endStream:d.ssl?!(n?.stream?.readableLength??1):!n.readableLength});N&&N.on("error",(e=>{const t=-505===e.errno;S=Buffer.from(y(e,"stream"+(t?" (error -505 usually means that the downstream service does not support this http version)":""),c,b))}));const U={hostname:m.hostname,path:f,port:m.port,protocol:m.protocol,rejectUnauthorized:!1,method:n.method,headers:{...Object.assign({},...Object.entries(E).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase())).map((([e,t])=>({[e]:t})))),host:m.hostname}},x=!S&&!O&&"file:"!==m.protocol&&await new Promise((e=>{const o="https:"===m.protocol?(0,r.request)(U,e):(0,t.request)(U,e);o.on("error",(t=>{S=Buffer.from(y(t,"request",c,b)),e(null)})),n.on("data",(e=>o.write(e))),n.on("end",(()=>o.end()))}));if(S)return void R(502,a,S);S=null,d.ssl&&n.stream&&n.stream.readableLength&&N&&(n.stream.on("data",(e=>N.write(e))),n.stream.on("end",(()=>N.end()))),!d.ssl&&n.readableLength&&N&&(n.on("data",(e=>N.write(e))),n.on("end",(()=>N.end())));const{outboundResponseHeaders:H}=await new Promise((e=>N?N.on("response",(t=>{e({outboundResponseHeaders:t})})):e(!N&&x?{outboundResponseHeaders:x.headers}:{outboundResponseHeaders:{}}))),P=H.location?new o.URL(H.location.startsWith("/")?`${m.href}${H.location.replace(/^\/+/,"")}`:H.location):null,L=P?P.href.substring(P.origin.length):null,C=c.origin,T=P?`${C}${L}`:null,A=N||x,B=S??await new Promise((e=>{let t=Buffer.alloc(0);A?(A.on("data",(e=>t=Buffer.concat([t,"string"==typeof e?Buffer.from(e):e]))),A.on("end",(()=>{e(t)}))):e(t)})).then((e=>d.replaceResponseBodyUrls&&e.length?(H["content-encoding"]||"").split(",").reduce((async(e,t)=>{const r=t.trim().toLowerCase(),o="gzip"===r||"x-gzip"===r?s.gunzip:"deflate"===r?s.inflate:"br"===r?s.brotliDecompress:"identity"===r||""===r?(e,t)=>{t(null,e)}:null;if(null===o)return void R(502,a,Buffer.from(y(new Error(`${r} compression not supported by the proxy`),"stream",c,b)));const n=await e;return await new Promise((e=>o(n,((t,r)=>{if(t)return R(502,a,Buffer.from(y(t,"stream",c,b))),void e("");e(r)}))))}),Promise.resolve(e)).then((e=>{const t=e.length>1e7,r=["text/html","application/javascript","application/json"].some((e=>(H["content-type"]??"").includes(e)));return!t&&(r||!/[^\x00-\x7F]/.test(e.toString()))?d.replaceResponseBodyUrls?Object.entries(d.mapping).reduce(((e,[t,r])=>""===t||t.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)?e.replace(new RegExp(r.replace(/^file:\/\//,"").replace(/[*+?^${}()|[\]\\]/g,"").replace(/^https/,"https?")+"/*","ig"),`https://${l}${t.replace(/\/+$/,"")}/`):e),e.toString()).split(`${l}/:`).join(`${l}:`).replace(/\?protocol=wss?%3A&hostname=[^&]+&port=[0-9]+&pathname=/g,`?protocol=ws${d.ssl?"s":""}%3A&hostname=${i}&port=${d.port}&pathname=${encodeURIComponent(u.replace(/\/+$/,""))}`):e.toString():e})).then((e=>(H["content-encoding"]||"").split(",").reduce(((e,t)=>{const r=t.trim().toLowerCase(),o="gzip"===r||"x-gzip"===r?s.gzip:"deflate"===r?s.deflate:"br"===r?s.brotliCompress:"identity"===r||""===r?(e,t)=>{t(null,e)}:null;if(null===o)throw new Error(`${r} compression not supported by the proxy`);return e.then((e=>new Promise((t=>o(e,((e,r)=>{if(e)throw e;t(r)}))))))}),Promise.resolve(Buffer.from(e))))):e)),k={...Object.entries({...H,...d.replaceResponseBodyUrls?{"content-length":`${B.byteLength}`}:{}}).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase()&&"connection"!==e.toLowerCase())).reduce(((e,[t,r])=>{const o=g.split("").map(((e,t)=>g.substring(t).startsWith(".")&&g.substring(t))).filter((e=>e)),n=[g].concat(o).reduce(((e,t)=>(Array.isArray(e)?e:[e]).map((e=>"string"==typeof e?e.replace(`Domain=${t}`,`Domain=${c.hostname}`):e))),r);return e[t]=(e[t]||[]).concat(n),e}),{}),...T?{location:[T]}:{}};try{Object.entries(k).forEach((([e,t])=>t&&a.setHeader(e,t)))}catch(e){}a.writeHead(H[":status"]||x.statusCode||200,d.ssl?void 0:x.statusMessage||"Status read from http/2",k),B?a.end(B):a.end()})).addListener("error",(e=>{"EACCES"===e.code&&u("permission denied for this port",i.ERROR,"⛔"),"EADDRINUSE"===e.code&&u("port is already used. NOT started",i.ERROR,"☠️")})).addListener("listening",(()=>{g(d)})).on("upgrade",((e,n)=>{if(!d.websocket)return void n.end("HTTP/1.1 503 Service Unavailable\r\n\r\n");const{key:s,target:a}=v(e),l=new o.URL(`${a.protocol}//${a.host}${e.url.endsWith("/_next/webpack-hmr")?e.url:e.url.replace(new RegExp(`^${s}`,"g"),"").replace(/^\/*/,"/")}`),p={hostname:l.hostname,path:l.pathname,port:l.port,protocol:l.protocol,rejectUnauthorized:!1,method:e.method,headers:e.headers,host:l.hostname},c="https:"===l.protocol?(0,r.request)(p):(0,t.request)(p);c.end(),c.on("error",(e=>{u("websocket request has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,"☄️")})),c.on("upgrade",((e,t)=>{const r=`HTTP/${e.httpVersion} ${e.statusCode} ${e.statusMessage}\r\n${Object.entries(e.headers).flatMap((([e,t])=>(Array.isArray(t)?t:[t]).map((t=>[e,t])))).map((([e,t])=>`${e}: ${t}\r\n`)).join("")}\r\n`;n.write(r),n.allowHalfOpen=!0,t.allowHalfOpen=!0,t.on("data",(e=>n.write(e))),n.on("data",(e=>t.write(e))),t.on("error",(e=>{u("downstream socket has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,"☄️")})),n.on("error",(e=>{u("upstream socket has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,"☄️")}))}))})).listen(d.port)};m().then(S);
2
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});const e=require("http2"),t=require("http"),r=require("https"),o=require("url"),n=require("fs"),s=require("zlib"),a=require("path");var i,l;!function(e){e[e.ERROR=124]="ERROR",e[e.INFO=93]="INFO",e[e.WARNING=172]="WARNING"}(i||(i={})),function(e){e.INBOUND="↘️ ",e.PORT="☎️ ",e.OUTBOUND="↗️ ",e.RULES="🔗",e.BODY_REPLACEMENT="✒️ ",e.WEBSOCKET="☄️ ",e.COLORED="✨",e.NO="⛔",e.ERROR_1="❌",e.ERROR_2="⛈️ ",e.ERROR_3="☢️ ",e.ERROR_4="⁉️ ",e.ERROR_5="⚡",e.ERROR_6="☠️ "}(l||(l={}));const p=(0,a.resolve)(process.env.HOME,".local-traffic.json"),c=(0,a.resolve)(process.cwd(),process.argv.slice(-1)[0].endsWith(".json")?process.argv.slice(-1)[0]:p),d={mapping:{},port:8080,replaceResponseBodyUrls:!1,dontUseHttp2Downstream:!1,simpleLogs:!1,websocket:!1};let h,m;const u=(e,t,r)=>{console.log(`${(e=>{const t=new Date;return`${e?"":""}${`${t.getHours()}`.padStart(2,"0")}${e?":":":"}${`${t.getMinutes()}`.padStart(2,"0")}${e?":":":"}${`${t.getSeconds()}`.padStart(2,"0")}${e?"":""}`})(h.simpleLogs)} ${h.simpleLogs?e.replace(/⎸/g,"|").replace(/⎹/g,"|").replace(/\u001b\[[^m]*m/g,"").replace(new RegExp(l.INBOUND,"g"),"inbound:").replace(new RegExp(l.PORT,"g"),"port:").replace(new RegExp(l.OUTBOUND,"g"),"outbound:").replace(new RegExp(l.RULES,"g"),"rules:").replace(new RegExp(l.NO,"g"),"").replace(new RegExp(l.BODY_REPLACEMENT,"g"),"body replacement").replace(new RegExp(l.WEBSOCKET,"g"),"websocket").replace(/\|+/g,"|"):t?`[48;5;${t}m⎸ ${process.stdout.isTTY&&r||""} ${e.padEnd(36)} ⎹`:e}`)},g=async(e=!0)=>new Promise((t=>(0,n.readFile)(c,((r,o)=>{r&&!e&&u("config error. Using default value",i.ERROR,l.ERROR_1);try{h=Object.assign({},d,JSON.parse((o||"{}").toString()))}catch(e){return u("config syntax incorrect, aborting",i.ERROR,l.ERROR_2),h=h||{...d},void t(h)}h.mapping[""]||u('default mapping "" not provided.',i.WARNING,l.ERROR_3),r&&"ENOENT"===r.code&&e&&c===p?(0,n.writeFile)(c,JSON.stringify(d),(e=>{e?u("config file NOT created",i.ERROR,l.ERROR_4):u("config file created",i.INFO,l.COLORED),t(h)})):t(h)})))).then((()=>{e&&(0,n.watchFile)(c,f)})),R=e=>{u(`⎸${l.PORT} ${e.port.toString().padStart(5)} ⎸${l.INBOUND} ${e.ssl?"H/2 ":"H1.1"} ⎸${l.OUTBOUND} ${e.dontUseHttp2Downstream?"H1.1":"H/2 "}⎹⎸${l.RULES}${Object.keys(h.mapping).length.toString().padStart(3)}⎹⎸${h.replaceResponseBodyUrls?l.BODY_REPLACEMENT:l.NO}⎹⎸${h.websocket?l.WEBSOCKET:l.NO}⎹⎸${h.simpleLogs?l.NO:l.COLORED}⎹`)},f=async()=>{const e={...h};return await g(!1),isNaN(h.port)||h.port>65535||h.port<0?(h=e,void u("port number invalid. Not refreshing",i.ERROR,l.PORT)):"object"!=typeof h.mapping?(h=e,void u("mapping should be an object. Aborting",i.ERROR,l.ERROR_5)):(h.replaceResponseBodyUrls!==e.replaceResponseBodyUrls&&u(`response body url ${h.replaceResponseBodyUrls?"":"NO "}replacement`,i.INFO,l.BODY_REPLACEMENT),h.dontUseHttp2Downstream!==e.dontUseHttp2Downstream&&u(`http/2 ${h.dontUseHttp2Downstream?"de":""}activated downstream`,i.INFO,l.OUTBOUND),h.websocket!==e.websocket&&u(`websocket ${h.websocket?"":"de"}activated`,i.INFO,l.WEBSOCKET),h.simpleLogs!==e.simpleLogs&&u("simple logs "+(h.simpleLogs?"on":"off"),i.INFO,l.COLORED),Object.keys(h.mapping).join("\n")!==Object.keys(e.mapping).join("\n")&&u(`${Object.keys(h.mapping).length.toString().padStart(5)} loaded mapping rules`,i.INFO,l.RULES),h.port!==e.port&&u(`port changed from ${e.port} to ${h.port}`,i.INFO,l.PORT),h.ssl&&!e.ssl&&u("ssl configuration added",i.INFO,l.INBOUND),!h.ssl&&e.ssl&&u("ssl configuration removed",i.INFO,l.INBOUND),void(h.port!==e.port||JSON.stringify(h.ssl)!==JSON.stringify(e.ssl)?(await new Promise((e=>m?m.close(e):e(void 0))),v()):R(h)))},$=e=>""==e?"":(0,a.normalize)(e).replace(/\\/g,"/"),O=e=>{const t=(0,a.resolve)("/",e.hostname,...e.pathname.replace(/[?#].*$/,"").replace(/^\/+/,"").split("/"));return{error:null,data:null,hasRun:!1,run:function(){return this.hasRun?Promise.resolve():new Promise((r=>(0,n.readFile)(t,((o,s)=>{if(this.hasRun=!0,!o||"EISDIR"!==o.code)return this.error=o,this.data=s,void r(void 0);(0,n.readdir)(t,((t,o)=>{this.error=t,this.data=o,t?r(void 0):Promise.all(o.map((t=>new Promise((r=>(0,n.lstat)((0,a.resolve)(e.pathname,t),((e,o)=>r([t,o,e])))))))).then((t=>{const o=t.filter((e=>!e[2]&&e[1].isDirectory())).concat(t.filter((e=>!e[2]&&e[1].isFile())));this.data=`${w(128194,"directory",e.href)}\n <p>Directory content of <i>${e.href.replace(/\//g,"&#x002F;")}</i></p>\n <ul class="list-group">\n <li class="list-group-item">&#x1F4C1;<a href="${e.pathname.endsWith("/")?"..":"."}">&lt;parent&gt;</a></li>\n ${o.filter((e=>!e[2])).map((t=>`<li class="list-group-item">&#x${(t[1].isDirectory()?128193:128196).toString(16)};<a href="${e.pathname.endsWith("/")?"":`${e.pathname.split("/").slice(-1)[0]}/`}${t[0]}">${t[0]}</a></li>`)).join("\n")}\n </li>\n </ul>\n </body></html>`,r(void 0)}))}))}))))},events:{},on:function(e,r){return this.events[e]=r,this.run().then((()=>{"response"===e&&this.events.response({Server:"local","Content-Type":t.endsWith(".svg")?"image/svg+xml":null},0),"data"===e&&this.data&&(this.events.data(this.data),this.events.end()),"error"===e&&this.error&&this.events.error(this.error)})),this},end:function(){return this},request:function(){return this}}},w=(e,t,r)=>`<!doctype html>\n<html lang="en">\n<head>\n<title>&#x${e.toString(16)}; local-traffic ${t} | ${r}</title>\n<link href="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css" rel="stylesheet"/>\n<script src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"><\/script>\n<script src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"><\/script>\n</head>\n<body><div class="container"><h1>&#x${e.toString(16)}; local-traffic ${t}</h1>\n<br/>`,E=(e,t,r,o)=>`${w(128163,"error",e.message)}\n<p>An error happened while trying to proxy a remote exchange</p>\n<div class="alert alert-warning" role="alert">\n &#x24D8;&nbsp;This is not an error from the downstream service.\n</div>\n<div class="alert alert-danger" role="alert">\n<pre><code>${e.stack||`<i>${e.name} : ${e.message}</i>`}${e.errno?`<br/>(code : ${e.errno})`:""}</code></pre>\n</div>\nMore information about the request :\n<table class="table">\n <tbody>\n <tr>\n <td>phase</td>\n <td>${t}</td>\n </tr>\n <tr>\n <td>requested URL</td>\n <td>${r}</td>\n </tr>\n <tr>\n <td>downstream URL</td>\n <td>${o||"&lt;no-target-url&gt;"}</td>\n </tr>\n </tbody>\n</table>\n</div></body></html>`,b=(e,t,r)=>{t.writeHead(e,void 0,{"content-type":"text/html","content-length":r.length}),t.end(r)},y=e=>{const t=(e.headers[":authority"]?.toString()??e.headers.host??"localhost").replace(/:.*/,""),r=e.headers[":authority"]||`${e.headers.host}${e.headers.host.match(/:[0-9]+$/)?"":80!==h.port||h.ssl?443===h.port&&h.ssl?"":`:${h.port}`:""}`,n=new o.URL(`http${h.ssl?"s":""}://${r}${e.url}`),s=n.href.substring(n.origin.length),[a,i]=Object.entries({...Object.assign({},...Object.entries(h.mapping).map((([e,t])=>({[e]:new o.URL($(t))}))))}).find((([e])=>s.match(RegExp(e))))||[];return{proxyHostname:t,proxyHostnameAndPort:r,url:n,path:s,key:a,target:i}},v=()=>{m=(h.ssl?e.createSecureServer.bind(null,{...h.ssl,allowHTTP1:!0}):t.createServer)((async(n,a)=>{if(!n.headers.host&&!n.headers[":authority"])return void b(400,a,Buffer.from(E(new Error("client must supply a 'host' header"),"proxy",new o.URL(`http${h.ssl?"s":""}://unknowndomain${n.url}`))));const{proxyHostname:i,proxyHostnameAndPort:l,url:p,path:d,key:m,target:u}=y(n);if(!u)return void b(502,a,Buffer.from(E(new Error(`No mapping found in config file ${c}`),"proxy",p)));const g=u.host.replace(RegExp(/\/+$/),""),R=`${u.href.substring("https://".length+u.host.length)}${$(d.replace(RegExp($(m)),""))}`.replace(/^\/*/,"/"),f=new o.URL(`${u.protocol}//${g}${R}`);let w=null,v=!h.dontUseHttp2Downstream;const N="file:"===u.protocol?O(f):v?await Promise.race([new Promise((t=>{const r=(0,e.connect)(f,{rejectUnauthorized:!1,protocol:u.protocol},((e,o)=>{v=v&&!!o.alpnProtocol,t(v?r:null)}));r.on("error",(e=>{w=v&&Buffer.from(E(e,"connection",p,f))}))})),new Promise((e=>setTimeout((()=>{v=!1,e(null)}),3e3)))]):null;w instanceof Buffer||(w=null);const U={...[...Object.entries(n.headers)].filter((([e])=>!["host","connection"].includes(e.toLowerCase()))).reduce(((e,[t,r])=>(e[t]=(e[t]||"")+(Array.isArray(r)?r:[r]).map((e=>e.replace(p.hostname,g))).join(", "),e)),{}),origin:u.href,referer:f.toString(),":authority":g,":method":n.method,":path":R,":scheme":u.protocol.replace(":","")},S=N&&!w&&N.request(U,{endStream:h.ssl?!(n?.stream?.readableLength??1):!n.readableLength});S&&S.on("error",(e=>{const t=-505===e.errno;w=Buffer.from(E(e,"stream"+(t?" (error -505 usually means that the downstream service does not support this http version)":""),p,f))}));const B={hostname:u.hostname,path:R,port:u.port,protocol:u.protocol,rejectUnauthorized:!1,method:n.method,headers:{...Object.assign({},...Object.entries(U).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase())).map((([e,t])=>({[e]:t})))),host:u.hostname}},L=!w&&!v&&"file:"!==u.protocol&&await new Promise((e=>{const o="https:"===u.protocol?(0,r.request)(B,e):(0,t.request)(B,e);o.on("error",(t=>{w=Buffer.from(E(t,"request",p,f)),e(null)})),n.on("data",(e=>o.write(e))),n.on("end",(()=>o.end()))}));if(w)return void b(502,a,w);w=null,h.ssl&&n.stream&&n.stream.readableLength&&S&&(n.stream.on("data",(e=>S.write(e))),n.stream.on("end",(()=>S.end()))),!h.ssl&&n.readableLength&&S&&(n.on("data",(e=>S.write(e))),n.on("end",(()=>S.end())));const{outboundResponseHeaders:j}=await new Promise((e=>S?S.on("response",(t=>{e({outboundResponseHeaders:t})})):e(!S&&L?{outboundResponseHeaders:L.headers}:{outboundResponseHeaders:{}}))),x=j.location?new o.URL(j.location.startsWith("/")?`${u.href}${j.location.replace(/^\/+/,"")}`:j.location):null,D=x?x.href.substring(x.origin.length):null,T=p.origin,P=x?`${T}${D}`:null,C=S||L,H=w??await new Promise((e=>{let t=Buffer.alloc(0);C?(C.on("data",(e=>t=Buffer.concat([t,"string"==typeof e?Buffer.from(e):e]))),C.on("end",(()=>{e(t)}))):e(t)})).then((e=>h.replaceResponseBodyUrls&&e.length?(j["content-encoding"]||"").split(",").reduce((async(e,t)=>{const r=t.trim().toLowerCase(),o="gzip"===r||"x-gzip"===r?s.gunzip:"deflate"===r?s.inflate:"br"===r?s.brotliDecompress:"identity"===r||""===r?(e,t)=>{t(null,e)}:null;if(null===o)return void b(502,a,Buffer.from(E(new Error(`${r} compression not supported by the proxy`),"stream",p,f)));const n=await e;return await new Promise((e=>o(n,((t,r)=>{if(t)return b(502,a,Buffer.from(E(t,"stream",p,f))),void e("");e(r)}))))}),Promise.resolve(e)).then((e=>{const t=e.length>1e7,r=["text/html","application/javascript","application/json"].some((e=>(j["content-type"]??"").includes(e)));return!t&&(r||!/[^\x00-\x7F]/.test(e.toString()))?h.replaceResponseBodyUrls?Object.entries(h.mapping).reduce(((e,[t,r])=>""===t||t.match(/^[-a-zA-Z0-9()@:%_\+.~#?&//=]*$/)?e.replace(new RegExp(r.replace(/^file:\/\//,"").replace(/[*+?^${}()|[\]\\]/g,"").replace(/^https/,"https?")+"/*","ig"),`https://${l}${t.replace(/\/+$/,"")}/`):e),e.toString()).split(`${l}/:`).join(`${l}:`).replace(/\?protocol=wss?%3A&hostname=[^&]+&port=[0-9]+&pathname=/g,`?protocol=ws${h.ssl?"s":""}%3A&hostname=${i}&port=${h.port}&pathname=${encodeURIComponent(m.replace(/\/+$/,""))}`):e.toString():e})).then((e=>(j["content-encoding"]||"").split(",").reduce(((e,t)=>{const r=t.trim().toLowerCase(),o="gzip"===r||"x-gzip"===r?s.gzip:"deflate"===r?s.deflate:"br"===r?s.brotliCompress:"identity"===r||""===r?(e,t)=>{t(null,e)}:null;if(null===o)throw new Error(`${r} compression not supported by the proxy`);return e.then((e=>new Promise((t=>o(e,((e,r)=>{if(e)throw e;t(r)}))))))}),Promise.resolve(Buffer.from(e))))):e)),I={...Object.entries({...j,...h.replaceResponseBodyUrls?{"content-length":`${H.byteLength}`}:{}}).filter((([e])=>!e.startsWith(":")&&"transfer-encoding"!==e.toLowerCase()&&"connection"!==e.toLowerCase())).reduce(((e,[t,r])=>{const o=g.split("").map(((e,t)=>g.substring(t).startsWith(".")&&g.substring(t))).filter((e=>e)),n=[g].concat(o).reduce(((e,t)=>(Array.isArray(e)?e:[e]).map((e=>"string"==typeof e?e.replace(`Domain=${t}`,`Domain=${p.hostname}`):e))),r);return e[t]=(e[t]||[]).concat(n),e}),{}),...P?{location:[P]}:{}};try{Object.entries(I).forEach((([e,t])=>t&&a.setHeader(e,t)))}catch(e){}a.writeHead(j[":status"]||L.statusCode||200,h.ssl?void 0:L.statusMessage||"Status read from http/2",I),H?a.end(H):a.end()})).addListener("error",(e=>{"EACCES"===e.code&&u("permission denied for this port",i.ERROR,l.NO),"EADDRINUSE"===e.code&&u("port is already used. NOT started",i.ERROR,l.ERROR_6)})).addListener("listening",(()=>{R(h)})).on("upgrade",((e,n)=>{if(!h.websocket)return void n.end("HTTP/1.1 503 Service Unavailable\r\n\r\n");const{key:s,target:a}=y(e),p=new o.URL(`${a.protocol}//${a.host}${e.url.endsWith("/_next/webpack-hmr")?e.url:e.url.replace(new RegExp(`^${s}`,"g"),"").replace(/^\/*/,"/")}`),c={hostname:p.hostname,path:p.pathname,port:p.port,protocol:p.protocol,rejectUnauthorized:!1,method:e.method,headers:e.headers,host:p.hostname},d="https:"===p.protocol?(0,r.request)(c):(0,t.request)(c);d.end(),d.on("error",(e=>{u("websocket request has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,l.WEBSOCKET)})),d.on("upgrade",((e,t)=>{const r=`HTTP/${e.httpVersion} ${e.statusCode} ${e.statusMessage}\r\n${Object.entries(e.headers).flatMap((([e,t])=>(Array.isArray(t)?t:[t]).map((t=>[e,t])))).map((([e,t])=>`${e}: ${t}\r\n`)).join("")}\r\n`;n.write(r),n.allowHalfOpen=!0,t.allowHalfOpen=!0,t.on("data",(e=>n.write(e))),n.on("data",(e=>t.write(e))),t.on("error",(e=>{u("downstream socket has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,l.WEBSOCKET)})),n.on("error",(e=>{u("upstream socket has errored "+(e.errno?`(${e.errno})`:""),i.WARNING,l.WEBSOCKET)}))}))})).listen(h.port)};g().then(v);
package/index.ts CHANGED
@@ -37,11 +37,27 @@ type ErrorWithErrno = NodeJS.ErrnoException;
37
37
 
38
38
  enum LogLevel {
39
39
  ERROR = 124,
40
- SUCCESS = 35,
41
- INFO = 21,
40
+ INFO = 93,
42
41
  WARNING = 172,
43
42
  }
44
43
 
44
+ enum EMOJIS {
45
+ INBOUND = '↘️ ',
46
+ PORT = '☎️ ',
47
+ OUTBOUND = '↗️ ',
48
+ RULES = '🔗',
49
+ BODY_REPLACEMENT = '✒️ ',
50
+ WEBSOCKET = '☄️ ',
51
+ COLORED = '✨',
52
+ NO = '⛔',
53
+ ERROR_1 = '❌',
54
+ ERROR_2 = '⛈️ ',
55
+ ERROR_3 = '☢️ ',
56
+ ERROR_4 = '⁉️ ',
57
+ ERROR_5 = '⚡',
58
+ ERROR_6 = '☠️ ',
59
+ }
60
+
45
61
  interface LocalConfiguration {
46
62
  mapping?: { [subPath: string]: string };
47
63
  ssl?: SecureServerOptions;
@@ -89,9 +105,14 @@ const log = (text: string, level?: LogLevel, emoji?: string) => {
89
105
  .replace(/⎸/g, "|")
90
106
  .replace(/⎹/g, "|")
91
107
  .replace(/\u001b\[[^m]*m/g, "")
92
- .replace(/↘️/g, "inbound")
93
- .replace(/☎️/g, "port")
94
- .replace(/↗️/g, "outbound")
108
+ .replace(new RegExp(EMOJIS.INBOUND, "g"), "inbound:")
109
+ .replace(new RegExp(EMOJIS.PORT, "g"), "port:")
110
+ .replace(new RegExp(EMOJIS.OUTBOUND, "g"), "outbound:")
111
+ .replace(new RegExp(EMOJIS.RULES, "g"), 'rules:')
112
+ .replace(new RegExp(EMOJIS.NO, "g"), '')
113
+ .replace(new RegExp(EMOJIS.BODY_REPLACEMENT, "g"), 'body replacement')
114
+ .replace(new RegExp(EMOJIS.WEBSOCKET, "g"), 'websocket')
115
+ .replace(/\|+/g, '|')
95
116
  : level
96
117
  ? `\u001b[48;5;${level}m⎸ ${
97
118
  !process.stdout.isTTY ? "" : emoji || ""
@@ -100,11 +121,12 @@ const log = (text: string, level?: LogLevel, emoji?: string) => {
100
121
  }`
101
122
  );
102
123
  };
124
+
103
125
  const load = async (firstTime: boolean = true) =>
104
126
  new Promise((resolve) =>
105
127
  readFile(filename, (error, data) => {
106
128
  if (error && !firstTime) {
107
- log("config error. Using default value", LogLevel.ERROR, "❌");
129
+ log("config error. Using default value", LogLevel.ERROR, EMOJIS.ERROR_1);
108
130
  }
109
131
  try {
110
132
  config = Object.assign(
@@ -113,13 +135,13 @@ const load = async (firstTime: boolean = true) =>
113
135
  JSON.parse((data || "{}").toString())
114
136
  );
115
137
  } catch (e) {
116
- log("config syntax incorrect, aborting", LogLevel.ERROR, "⛈️");
138
+ log("config syntax incorrect, aborting", LogLevel.ERROR, EMOJIS.ERROR_2);
117
139
  config = config || { ...defaultConfig };
118
140
  resolve(config);
119
141
  return;
120
142
  }
121
143
  if (!config.mapping[""]) {
122
- log('default mapping "" not provided.', LogLevel.WARNING, "☢️");
144
+ log('default mapping "" not provided.', LogLevel.WARNING, EMOJIS.ERROR_3);
123
145
  }
124
146
  if (
125
147
  error &&
@@ -129,8 +151,8 @@ const load = async (firstTime: boolean = true) =>
129
151
  ) {
130
152
  writeFile(filename, JSON.stringify(defaultConfig), (fileWriteErr) => {
131
153
  if (fileWriteErr)
132
- log("config file NOT created", LogLevel.ERROR, "⁉️");
133
- else log("config file created", LogLevel.SUCCESS, "✨");
154
+ log("config file NOT created", LogLevel.ERROR, EMOJIS.ERROR_4);
155
+ else log("config file created", LogLevel.INFO, EMOJIS.COLORED);
134
156
  resolve(config);
135
157
  });
136
158
  } else resolve(config);
@@ -139,15 +161,21 @@ const load = async (firstTime: boolean = true) =>
139
161
  if (firstTime) watchFile(filename, onWatch);
140
162
  });
141
163
 
142
- const logProtocols = (thisConfig: LocalConfiguration) => {
164
+ const quickStatus = (thisConfig: LocalConfiguration) => {
143
165
  log(
144
- `\u001b[48;5;5m⎸ ↘️ : ${
145
- thisConfig.ssl ? "HTTP/2 " : "HTTP 1.1"
146
- } \u001b[48;5;21m⎸ ☎️ : ${thisConfig.port
166
+ `\u001b[48;5;52m⎸${EMOJIS.PORT} ${thisConfig.port
147
167
  .toString()
148
- .padStart(5)} \u001b[48;5;31m⎸ ↗️ : ${
149
- thisConfig.dontUseHttp2Downstream ? "HTTP 1.1" : "HTTP/2 "
150
- } ⎹\u001b[0m`
168
+ .padStart(5)} \u001b[48;5;53m⎸${EMOJIS.INBOUND} ${
169
+ thisConfig.ssl ? "H/2 " : "H1.1"
170
+ } \u001b[48;5;54m⎸${EMOJIS.OUTBOUND} ${
171
+ thisConfig.dontUseHttp2Downstream ? "H1.1" : "H/2 "
172
+ }⎹\u001b[48;5;55m⎸${EMOJIS.RULES}${Object.keys(config.mapping)
173
+ .length.toString()
174
+ .padStart(3)}⎹\u001b[48;5;56m⎸${
175
+ config.replaceResponseBodyUrls ?
176
+ EMOJIS.BODY_REPLACEMENT : EMOJIS.NO}⎹\u001b[48;5;57m⎸${
177
+ config.websocket ? EMOJIS.WEBSOCKET : EMOJIS.NO}⎹\u001b[48;5;93m⎸${
178
+ !config.simpleLogs ? EMOJIS.COLORED : EMOJIS.NO}⎹\u001b[0m`
151
179
  );
152
180
  };
153
181
 
@@ -156,45 +184,59 @@ const onWatch = async () => {
156
184
  await load(false);
157
185
  if (isNaN(config.port) || config.port > 65535 || config.port < 0) {
158
186
  config = previousConfig;
159
- log("port number invalid. Not refreshing", LogLevel.ERROR, "☎️");
187
+ log("port number invalid. Not refreshing", LogLevel.ERROR, EMOJIS.PORT);
160
188
  return;
161
189
  }
162
190
  if (typeof config.mapping !== "object") {
163
191
  config = previousConfig;
164
- log("mapping should be an object. Aborting", LogLevel.ERROR, "⚡");
192
+ log("mapping should be an object. Aborting", LogLevel.ERROR, EMOJIS.ERROR_5);
165
193
  return;
166
194
  }
167
195
  if (
168
- config.replaceResponseBodyUrls &&
169
- !previousConfig.replaceResponseBodyUrls
196
+ config.replaceResponseBodyUrls !== previousConfig.replaceResponseBodyUrls
170
197
  ) {
171
- log("response body url replacement", LogLevel.INFO, "✔️");
198
+ log(`response body url ${
199
+ !config.replaceResponseBodyUrls ? 'NO ' : ''
200
+ }replacement`, LogLevel.INFO, EMOJIS.BODY_REPLACEMENT);
172
201
  }
173
202
  if (
174
- !config.replaceResponseBodyUrls &&
175
- previousConfig.replaceResponseBodyUrls
203
+ config.dontUseHttp2Downstream !== previousConfig.dontUseHttp2Downstream
176
204
  ) {
177
- log("response body url NO replacement", LogLevel.INFO, "✖️");
205
+ log(`http/2 ${
206
+ config.dontUseHttp2Downstream ? 'de' : ''}activated downstream`, LogLevel.INFO, EMOJIS.OUTBOUND);
178
207
  }
179
208
  if (
180
- config.websocket &&
181
- !previousConfig.websocket
209
+ config.websocket !== previousConfig.websocket
182
210
  ) {
183
- log("websocket activated", LogLevel.INFO, "☄️");
211
+ log(`websocket ${
212
+ !config.websocket ? 'de' : ''}activated`, LogLevel.INFO, EMOJIS.WEBSOCKET);
184
213
  }
185
214
  if (
186
- !config.websocket &&
187
- previousConfig.websocket
215
+ config.simpleLogs !== previousConfig.simpleLogs
188
216
  ) {
189
- log("websocket deactivated", LogLevel.INFO, "☄️");
217
+ log(`simple logs ${
218
+ !config.simpleLogs ? 'off' : 'on'}`, LogLevel.INFO, EMOJIS.COLORED);
219
+ }
220
+ if (Object.keys(config.mapping).join('\n') !== Object.keys(previousConfig.mapping).join('\n')) {
221
+ log(
222
+ `${Object.keys(config.mapping)
223
+ .length.toString()
224
+ .padStart(5)} loaded mapping rules`,
225
+ LogLevel.INFO,
226
+ EMOJIS.RULES
227
+ );
228
+ }
229
+ if (
230
+ config.port !== previousConfig.port
231
+ ) {
232
+ log(`port changed from ${previousConfig.port} to ${config.port}`, LogLevel.INFO, EMOJIS.PORT);
233
+ }
234
+ if (config.ssl && !previousConfig.ssl) {
235
+ log(`ssl configuration added`, LogLevel.INFO, EMOJIS.INBOUND);
236
+ }
237
+ if (!config.ssl && previousConfig.ssl) {
238
+ log(`ssl configuration removed`, LogLevel.INFO, EMOJIS.INBOUND);
190
239
  }
191
- log(
192
- `${Object.keys(config.mapping)
193
- .length.toString()
194
- .padStart(5)} loaded mapping rules`,
195
- LogLevel.SUCCESS,
196
- "↻"
197
- );
198
240
  if (
199
241
  config.port !== previousConfig.port ||
200
242
  JSON.stringify(config.ssl) !== JSON.stringify(previousConfig.ssl)
@@ -203,11 +245,7 @@ const onWatch = async () => {
203
245
  !server ? resolve(void 0) : server.close(resolve)
204
246
  );
205
247
  start();
206
- } else if (
207
- config.dontUseHttp2Downstream !== previousConfig.dontUseHttp2Downstream
208
- ) {
209
- logProtocols(config);
210
- }
248
+ } else quickStatus(config);
211
249
  };
212
250
 
213
251
  const unixNorm = (path: string) =>
@@ -901,12 +939,12 @@ const start = () => {
901
939
  ) as Server)
902
940
  .addListener("error", (err: Error) => {
903
941
  if ((err as ErrorWithErrno).code === "EACCES")
904
- log(`permission denied for this port`, LogLevel.ERROR, "⛔");
942
+ log(`permission denied for this port`, LogLevel.ERROR, EMOJIS.NO);
905
943
  if ((err as ErrorWithErrno).code === "EADDRINUSE")
906
- log(`port is already used. NOT started`, LogLevel.ERROR, "☠️");
944
+ log(`port is already used. NOT started`, LogLevel.ERROR, EMOJIS.ERROR_6);
907
945
  })
908
946
  .addListener("listening", () => {
909
- logProtocols(config);
947
+ quickStatus(config);
910
948
  })
911
949
  .on("upgrade", (request: IncomingMessage, upstreamSocket: Duplex) => {
912
950
  if (!config.websocket) {
@@ -937,7 +975,8 @@ const start = () => {
937
975
  downstreamRequest.on('error', (error) => {
938
976
  log(`websocket request has errored ${
939
977
  (error as ErrorWithErrno).errno ?
940
- `(${(error as ErrorWithErrno).errno})` : ''}`, LogLevel.WARNING, "☄️")
978
+ `(${(error as ErrorWithErrno).errno})` : ''}`,
979
+ LogLevel.WARNING, EMOJIS.WEBSOCKET)
941
980
  });
942
981
  downstreamRequest.on('upgrade', (response, downstreamSocket) => {
943
982
  const upgradeResponse = `HTTP/${response.httpVersion} ${response.statusCode} ${
@@ -954,12 +993,14 @@ const start = () => {
954
993
  downstreamSocket.on('error', (error) => {
955
994
  log(`downstream socket has errored ${
956
995
  (error as ErrorWithErrno).errno ?
957
- `(${(error as ErrorWithErrno).errno})` : ''}`, LogLevel.WARNING, "☄️")
996
+ `(${(error as ErrorWithErrno).errno})` : ''}`,
997
+ LogLevel.WARNING, EMOJIS.WEBSOCKET)
958
998
  })
959
999
  upstreamSocket.on('error', (error) => {
960
1000
  log(`upstream socket has errored ${
961
1001
  (error as ErrorWithErrno).errno ?
962
- `(${(error as ErrorWithErrno).errno})` : ''}`, LogLevel.WARNING, "☄️")
1002
+ `(${(error as ErrorWithErrno).errno})` : ''}`,
1003
+ LogLevel.WARNING, EMOJIS.WEBSOCKET)
963
1004
  })
964
1005
  });
965
1006
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-traffic",
3
- "version": "0.0.36",
3
+ "version": "0.0.39",
4
4
  "main": "index.ts",
5
5
  "private": false,
6
6
  "keywords": [