UNPKG

pixel-serve-server

Version:

A robust Node.js utility for handling and processing images. This package provides features like resizing, format conversion and etc.

3 lines 17.4 kB
'use strict';var v=require('path'),S=require('fs/promises'),crypto=require('crypto'),q=require('sharp'),url=require('url'),N=require('dns/promises'),Q=require('http'),K=require('https'),net=require('net'),be=require('axios'),zod=require('zod');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}function _interopNamespace(e){if(e&&e.__esModule)return e;var n=Object.create(null);if(e){Object.keys(e).forEach(function(k){if(k!=='default'){var d=Object.getOwnPropertyDescriptor(e,k);Object.defineProperty(n,k,d.get?d:{enumerable:true,get:function(){return e[k]}});}})}n.default=e;return Object.freeze(n)}var v__default=/*#__PURE__*/_interopDefault(v);var S__namespace=/*#__PURE__*/_interopNamespace(S);var q__default=/*#__PURE__*/_interopDefault(q);var N__namespace=/*#__PURE__*/_interopNamespace(N);var Q__namespace=/*#__PURE__*/_interopNamespace(Q);var K__namespace=/*#__PURE__*/_interopNamespace(K);var be__default=/*#__PURE__*/_interopDefault(be);var pe=()=>typeof document>"u"?new URL(`file:${__filename}`).href:document.currentScript&&document.currentScript.tagName.toUpperCase()==="SCRIPT"?document.currentScript.src:new URL("main.js",document.baseURI).href,E=pe();var xe=v__default.default.dirname(url.fileURLToPath(E)),V=e=>v__default.default.join(xe,"assets",e),we=V("noimage.jpg"),ye=V("noavatar.png"),g={normal:async()=>S.readFile(we),avatar:async()=>S.readFile(ye)},G=/^\/api\/v1\//,D=["jpeg","jpg","png","webp","gif","tiff","avif"],T={jpeg:"image/jpeg",jpg:"image/jpeg",png:"image/png",webp:"image/webp",gif:"image/gif",tiff:"image/tiff",avif:"image/avif"};var x=(e,t,n,r)=>{if(e)try{e(t,{phase:n,src:r});}catch{}},ve=4096,X=async(e,t)=>{try{if(!e||!t||typeof t!="string"||t.length>ve||t.includes("\0")||t.includes("\\")||v__default.default.isAbsolute(t)||!/^[^\x00-\x1F\x7F]+$/.test(t))return !1;let n=v__default.default.resolve(e),r=v__default.default.resolve(n,t),[i,u]=await Promise.all([S__namespace.realpath(n),S__namespace.realpath(r)]);if(!(await S__namespace.stat(i)).isDirectory()||!(await S__namespace.stat(u)).isFile())return !1;let p=i+v__default.default.sep,f=(u+v__default.default.sep).startsWith(p)||u===i,o=v__default.default.relative(i,u);return !o.startsWith("..")&&!v__default.default.isAbsolute(o)&&f}catch{return false}},F=e=>{let t=net.isIP(e);if(t===0)return true;if(t===4){let r=e.split(".").map(s=>Number(s));if(r.length!==4||r.some(s=>Number.isNaN(s)))return true;let[i,u]=r;return i===0||i===10||i===127||i===169&&u===254||i===172&&u>=16&&u<=31||i===192&&u===168||i===192&&u===0||i===198&&(u===18||u===19)||i===198&&u===51&&r[2]===100||i===203&&u===0&&r[2]===113||i>=224&&i<=239||i>=240}let n=e.toLowerCase();if(n==="::"||n==="::0"||n==="::1")return true;if(n.startsWith("::ffff:")){let r=n.slice(7);return net.isIP(r)===4?F(r):true}return !!(/^fe[89ab][0-9a-f]?:/i.test(n)||/^f[cd][0-9a-f]{0,2}:/i.test(n)||n.startsWith("ff"))},Pe=async e=>{if(!e)return false;let t=e.replace(/^\[|\]$/g,"");if(net.isIP(t)!==0)return !F(t);try{let n=await N__namespace.lookup(t,{all:!0,verbatim:!0});return n.length?n.every(r=>!F(r.address)):!1}catch{return false}},J=async e=>{if(!e)return null;let t=e.replace(/^\[|\]$/g,"");if(net.isIP(t)!==0){if(F(t))return null;let n=net.isIP(t)===6?6:4;return {address:t,family:n}}try{let n=await N__namespace.lookup(t,{all:!0,verbatim:!0});if(!n.length||n.some(i=>F(i.address)))return null;let r=n[0];return {address:r.address,family:r.family===6?6:4}}catch{return null}},Ie=(e,t)=>(n,r,i)=>{i(null,e,t);},Z=(e,t)=>{let n=Ie(e,t);return {httpAgent:new Q__namespace.Agent({lookup:n}),httpsAgent:new K__namespace.Agent({lookup:n})}},Y=(e,t,n)=>n.includes(e)||n.includes(t),Se=async(e,t,n,r)=>{try{return await be__default.default.get(e,{responseType:"arraybuffer",timeout:t,maxContentLength:n,maxBodyLength:n,maxRedirects:0,httpAgent:r.httpAgent,httpsAgent:r.httpsAgent,validateStatus:i=>i>=200&&i<300||i>=300&&i<400})}catch(i){let u=i;return u?.response?u.response:null}},Ae=async(e,t="normal",{timeoutMs:n,maxBytes:r,allowedNetworkList:i,maxRedirects:u,onError:s})=>{try{let l=e;for(let p=0;p<=u;p++){let m;try{m=new URL(l);}catch(P){return x(s,P,"fetch",l),await g[t]()}if(!["http:","https:"].includes(m.protocol))return x(s,new Error(`disallowed protocol ${m.protocol}`),"fetch",l),await g[t]();if(!Y(m.hostname,m.host,i))return x(s,new Error(`host ${m.hostname} not in allowedNetworkList`),"fetch",l),await g[t]();let f=await J(m.hostname);if(!f)return x(s,new Error(`host ${m.hostname} resolves to a private IP or DNS lookup failed`),"fetch",l),await g[t]();let o=Z(f.address,f.family),h=await Se(l,n,r,o);if(!h)return x(s,new Error("network request returned no response"),"fetch",l),await g[t]();if(h.status>=300&&h.status<400){let P=h.headers?.location;if(!P)return x(s,new Error("redirect response missing Location header"),"fetch",l),await g[t]();try{l=new URL(P,l).toString();}catch(y){return x(s,y,"fetch",P),await g[t]()}continue}if(h.status<200||h.status>=300)return x(s,new Error(`non-2xx status ${h.status}`),"fetch",l),await g[t]();let c=h.headers?.["content-type"]?.toLowerCase()?.split(";")[0]?.trim(),w=Object.values(T);return c&&w.includes(c)?Buffer.from(h.data):(x(s,new Error(`disallowed content-type ${c??"missing"} for ${l}`),"fetch",l),await g[t]())}return x(s,new Error(`exceeded maxRedirects=${u}`),"fetch",e),await g[t]()}catch(l){return x(s,l,"fetch",e),await g[t]()}},B=async(e,t,n="normal",r,i)=>{if(!await X(t,e))return x(i,new Error(`invalid local path: ${e}`),"fs",e),await g[n]();try{let s=v__default.default.resolve(t,e);if(r){let l=await S__namespace.stat(s);if(l.size>r)return x(i,new Error(`local file ${e} exceeds maxDownloadBytes (${l.size} > ${r})`),"fs",e),await g[n]()}return await S__namespace.readFile(s)}catch(s){return x(i,s,"fs",e),await g[n]()}},ee=(e,t,n)=>n!==void 0?e.startsWith(n)?e.slice(n.length):e:e.replace(t,""),te=(e,t,n,r="normal",i,u=[],{timeoutMs:s,maxBytes:l,maxRedirects:p=3,onError:m,apiPrefix:f})=>{try{let o=new URL(e);if(n!==void 0&&[n,`www.${n}`].includes(o.hostname)){let w=ee(o.pathname,i,f);return B(w,t,r,l,m)}return Y(o.hostname,o.host,u)?["http:","https:"].includes(o.protocol)?Ae(e,r,{timeoutMs:s,maxBytes:l,allowedNetworkList:u,maxRedirects:p,onError:m}):(x(m,new Error(`disallowed protocol ${o.protocol}`),"fetch",e),g[r]()):(x(m,new Error(`host ${o.hostname} not in allowedNetworkList`),"fetch",e),g[r]())}catch(o){return x(m,o,"fetch",e),B(e,t,r,l,m)}};var Ee=zod.z.enum(D),Re=zod.z.enum(["avatar","normal"]),_=zod.z.object({src:zod.z.preprocess((e,t)=>{if(e==null||typeof e=="string")return e;let n=Array.isArray(e)?"array":typeof e;return t.addIssue({code:zod.z.ZodIssueCode.custom,message:`src must be a string (received ${n})`}),zod.z.NEVER},zod.z.string().optional()).optional(),format:zod.z.string().optional().transform(e=>{let t=e?.toLowerCase();return t&&Ee.options.includes(t)?t:void 0}).optional(),width:zod.z.union([zod.z.number(),zod.z.string()]).optional().transform(e=>e==null?void 0:Number(e)).pipe(zod.z.number().int().min(50,"width too small").max(4e3,"width too large").optional()),height:zod.z.union([zod.z.number(),zod.z.string()]).optional().transform(e=>e==null?void 0:Number(e)).pipe(zod.z.number().int().min(50,"height too small").max(4e3,"height too large").optional()),quality:zod.z.union([zod.z.number(),zod.z.string()]).optional().transform(e=>e==null?void 0:Number(e)).pipe(zod.z.number().int().min(1).max(100).default(80)),folder:zod.z.enum(["public","private"]).default("public"),type:Re.default("normal"),userId:zod.z.union([zod.z.string(),zod.z.number()]).optional().transform(e=>e==null?void 0:String(e).trim()).pipe(zod.z.string().min(1,"userId cannot be empty").max(128,"userId too long").optional())}).strict(),k=zod.z.object({baseDir:zod.z.string().min(1,"baseDir is required"),idHandler:zod.z.custom(e=>typeof e=="function",{message:"idHandler must be a function"}).optional(),getUserFolder:zod.z.custom(e=>typeof e=="function",{message:"getUserFolder must be a function"}).optional(),getUserFolderRootDir:zod.z.string().min(1).optional(),websiteURL:zod.z.union([zod.z.url(),zod.z.string().regex(/^(?!-)[A-Za-z0-9-]{1,63}(\.(?!-)[A-Za-z0-9-]{1,63})*$/)]).optional(),apiRegex:zod.z.instanceof(RegExp).default(G),apiPrefix:zod.z.string().min(1,"apiPrefix cannot be empty").optional(),allowedNetworkList:zod.z.array(zod.z.string().transform(e=>e.trim()).pipe(zod.z.string().min(1,"allowedNetworkList entries cannot be empty").regex(/^[a-z0-9.-]+$/i,"allowedNetworkList entry is not a valid hostname"))).transform(e=>e.map(t=>t.toLowerCase())).default([]),cacheControl:zod.z.string().optional(),etag:zod.z.boolean().default(true),minWidth:zod.z.number().int().positive().default(50),maxWidth:zod.z.number().int().positive().default(4e3),minHeight:zod.z.number().int().positive().default(50),maxHeight:zod.z.number().int().positive().default(4e3),defaultQuality:zod.z.number().int().min(1).max(100).default(80),requestTimeoutMs:zod.z.number().int().positive().default(5e3),idHandlerTimeoutMs:zod.z.number().int().positive().optional(),maxDownloadBytes:zod.z.number().int().positive().default(5e6),maxRedirects:zod.z.number().int().min(0).max(10).default(3),maxInputPixels:zod.z.number().int().positive().default(16e3*16e3),allowSvgInput:zod.z.boolean().default(false),onError:zod.z.custom(e=>typeof e=="function",{message:"onError must be a function"}).optional(),onComplete:zod.z.custom(e=>typeof e=="function",{message:"onComplete must be a function"}).optional()}).strict().refine(e=>e.minWidth<=e.maxWidth,{message:"minWidth must be less than or equal to maxWidth",path:["minWidth"]}).refine(e=>e.minHeight<=e.maxHeight,{message:"minHeight must be less than or equal to maxHeight",path:["minHeight"]});var re=e=>k.parse(e),ne=(e,t)=>{let n=_.parse(e),r=(i,u,s)=>{if(i!==void 0)return Math.min(Math.max(i,u),s)};return {...n,width:r(n.width,t.minWidth,t.maxWidth),height:r(n.height,t.minHeight,t.maxHeight),quality:n.quality??t.defaultQuality,format:n.format??"jpeg"}};var I=(e,t,n)=>{if(e)try{e(t,n);}catch{}},M=(e,t)=>{if(e)try{e(t);}catch{}},j=e=>{let t=process.hrtime.bigint()-e,n=Number(t/1000000n),r=Number(t%1000000n)/1e6;return n+r},Fe=100,ie="image",ae=(e,t)=>{let n=t,r=(e??"").split("#")[0].split("?")[0],i=v__default.default.basename(r),u=i&&i!=="/"&&i!=="\\"?v__default.default.basename(i,v__default.default.extname(i)):"",s=u.replace(/[^\x20-\x7E]/g,"_").replace(/["\\\x00-\x1F\x7F]/g,"_").replace(/_+/g,"_");s.startsWith("_")&&(s=s.slice(1)),s.endsWith("_")&&(s=s.slice(0,-1));let l=s.length>0?s:ie,p=Math.max(1,Fe-n.length-1),f=`${l.slice(0,p)}.${n}`,o=u.length>0?u:ie,c=encodeURIComponent(o).replace(/['()*]/g,y=>`%${y.charCodeAt(0).toString(16).toUpperCase()}`).slice(0,p),w=c.lastIndexOf("%");for(w>=0&&c.length-w<3&&(c=c.slice(0,w));c.length>=3;){let y=c.slice(-3);if(y[0]!=="%")break;let U=parseInt(y.slice(1),16);if(U>=192&&U<=253){c=c.slice(0,-3);break}if(U>=128&&U<=191){c=c.slice(0,-3);continue}break}let P=`${c}.${n}`;return {asciiFilename:f,encodedFilename:P}},le=async(e,t)=>{if(!e)return null;if(e.startsWith("http://")||e.startsWith("https://"))return `url:${e}`;try{let n=v__default.default.resolve(t,e),r=await S__namespace.stat(n);return `file:${r.mtimeMs}:${r.size}`}catch{return null}},ue=(e,t)=>{let n=JSON.stringify({src:e.src??"",w:e.width??"",h:e.height??"",f:e.format,q:e.quality,t:e.type,fo:e.folder,u:e.parsedUserId??"",sid:t});return `"${crypto.createHash("sha256").update(n).digest("hex")}"`},se=async(e,t,n)=>{let r;try{return await Promise.race([e,new Promise((i,u)=>{r=setTimeout(()=>u(new Error(`${n} timed out after ${t}ms`)),t);})])}finally{r!==void 0&&clearTimeout(r);}},me=async(e,t,n)=>{if(!e||!t)return false;let r;if(n!==void 0)r=n;else try{r=await S__namespace.realpath(v__default.default.resolve(e));}catch{r=v__default.default.resolve(e);}let i=v__default.default.resolve(t),u;try{u=await S__namespace.realpath(i);}catch{u=i;}if(r===u)return true;let s=v__default.default.relative(r,u);return s===""||s==="."?true:!s.startsWith("..")&&!v__default.default.isAbsolute(s)},de=async e=>{try{return await S__namespace.realpath(v__default.default.resolve(e))}catch{return v__default.default.resolve(e)}},ce=e=>{if(!e||e.length===0)return false;let t=0;for(;t<e.length&&(e[t]===9||e[t]===10||e[t]===13||e[t]===32);)t++;if(e.length>=t+2&&(e[t]===254&&e[t+1]===255||e[t]===255&&e[t+1]===254)){let r=e[t]===255,i=Math.min(e.length,t+2+4096),u;if(r)u=e.subarray(t+2,i).toString("utf16le");else {let l=e.subarray(t+2,i),p=Buffer.alloc(l.length-l.length%2);for(let m=0;m+1<l.length;m+=2)p[m]=l[m+1],p[m+1]=l[m];u=p.toString("utf16le");}let s=u.trimStart().toLowerCase();return s.startsWith("<svg")?true:s.startsWith("<?xml")||s.startsWith("<!--")?/<svg[\s>]/.test(s):/<svg[\s>]/.test(s)}e.length>=t+3&&e[t]===239&&e[t+1]===187&&e[t+2]===191&&(t+=3);let n=e.subarray(t,Math.min(e.length,t+4096)).toString("latin1").trimStart().toLowerCase();return n.startsWith("<svg")?true:n.startsWith("<?xml")||n.startsWith("<!--")?/<svg[\s>]/.test(n):false},Ce=async(e,t,n,r,i)=>{let u=process.hrtime.bigint(),s="normal",l=r.onError,p=r.onComplete,m,f;try{let o;try{o=ne(e.query,{minWidth:r.minWidth,maxWidth:r.maxWidth,minHeight:r.minHeight,maxHeight:r.maxHeight,defaultQuality:r.defaultQuality});}catch(d){throw I(l,d,{phase:"validation"}),d}m=o.src,f=o.userId,s=o.type??"normal";let h=r.baseDir,c;if(o.userId&&(c=o.userId,r.idHandler)){let d=o.userId,b=r.idHandlerTimeoutMs??r.requestTimeoutMs;try{let R=Promise.resolve().then(()=>r.idHandler(d)),$=await se(R,b,"idHandler");c=typeof $=="string"?$:d,typeof $!="string"&&I(l,new Error(`idHandler returned a non-string value (${typeof $})`),{phase:"idHandler",src:m,userId:d});}catch(R){c=d,I(l,R,{phase:"idHandler",src:m,userId:d});}f=c;}if(o.folder==="private"&&r.getUserFolder)try{let d=Promise.resolve().then(()=>r.getUserFolder(e,c)),b=await se(d,r.requestTimeoutMs,"getUserFolder");b&&(r.getUserFolderRootDir?await me(r.getUserFolderRootDir,b,i)?h=b:I(l,new Error(`getUserFolder returned path "${b}" outside getUserFolderRootDir "${r.getUserFolderRootDir}"`),{phase:"getUserFolder",src:m,userId:f}):h=b);}catch(d){I(l,d,{phase:"getUserFolder",src:m,userId:f});}let w=D.includes(o.format)?o.format:"jpeg",P=await le(o.src,h),y;if(r.etag&&P&&(y=ue({src:o.src,width:o.width,height:o.height,format:w,quality:o.quality,type:o.type,folder:o.folder,parsedUserId:c},P),e.headers["if-none-match"]===y)){t.status(304).end(),M(p,{src:m,userId:f,format:w,outputBytes:0,cached:!0,durationMs:j(u)});return}let W=await(async()=>o.src?o.src.startsWith("http://")||o.src.startsWith("https://")?te(o.src,h,r.websiteURL,o.type,r.apiRegex,r.allowedNetworkList,{timeoutMs:r.requestTimeoutMs,maxBytes:r.maxDownloadBytes,maxRedirects:r.maxRedirects,onError:l,apiPrefix:r.apiPrefix}):B(o.src,h,o.type,r.maxDownloadBytes,l):g[o.type]())();if(!r.allowSvgInput&&ce(W)){let d=new Error("svg input rejected");throw I(l,d,{phase:"sharp",src:m,userId:f}),d}let L;try{let d=q__default.default(W,{failOn:"warning",limitInputPixels:r.maxInputPixels,sequentialRead:!0,unlimited:!1}),b=await d.metadata();if(b.width&&b.height&&b.width*b.height>r.maxInputPixels)throw new Error("input exceeds maxInputPixels");if(!r.allowSvgInput&&b.format==="svg")throw new Error("svg input rejected");if(d=q__default.default(W,{failOn:"warning",limitInputPixels:r.maxInputPixels,sequentialRead:!0,unlimited:!1}).rotate(),o.width||o.height){let R={width:o.width??void 0,height:o.height??void 0,fit:q__default.default.fit.cover,withoutEnlargement:!0};d=d.resize(R);}L=await d.toFormat(w,{quality:o.quality}).toBuffer();}catch(d){throw I(l,d,{phase:"sharp",src:m,userId:f}),d}if(r.etag&&!y&&(y=`"${crypto.createHash("sha256").update(L).digest("hex")}"`,e.headers["if-none-match"]===y)){t.status(304).end(),M(p,{src:m,userId:f,format:w,outputBytes:0,cached:!0,durationMs:j(u)});return}let{asciiFilename:fe,encodedFilename:ge}=ae(o.src,w);t.type(T[w]),t.setHeader("Content-Disposition",`inline; filename="${fe}"; filename*=UTF-8''${ge}`),t.setHeader("Vary","Accept-Encoding"),t.setHeader("Cache-Control",r.cacheControl??"public, max-age=86400, stale-while-revalidate=604800"),y&&t.setHeader("ETag",y),t.setHeader("Content-Length",L.length.toString()),t.send(L),M(p,{src:m,userId:f,format:w,outputBytes:L.length,cached:!1,durationMs:j(u)});}catch{if(t.headersSent){let o=new Error("response already flushed");I(l,o,{phase:"fs",src:m,userId:f}),n(o);return}try{let h=await g[s==="avatar"?"avatar":"normal"]();t.type(T.jpeg),t.setHeader("Content-Disposition",'inline; filename="fallback.jpeg"'),t.setHeader("Vary","Accept-Encoding"),t.setHeader("Cache-Control","public, max-age=60"),t.send(h);}catch(o){I(l,o,{phase:"fs",src:m,userId:f}),n(o);}}},Ue=e=>{let t;try{t=re(e);}catch(s){throw I(e.onError,s,{phase:"schema"}),s}let n,r=false,i,u=async s=>r&&n!==void 0?n:(i||(i=de(s).then(l=>(n=l,r=true,l))),i);return async(s,l,p)=>{let m;return t.getUserFolderRootDir&&(m=await u(t.getUserFolderRootDir)),Ce(s,l,p,t,m)}},Le=Ue; exports.buildDeterministicEtag=ue;exports.buildFilename=ae;exports.buildPinnedAgents=Z;exports.buildSourceIdentifier=le;exports.isInsideRoot=me;exports.isPrivateIp=F;exports.isPublicHost=Pe;exports.isValidPath=X;exports.looksLikeSvg=ce;exports.optionsSchema=k;exports.registerServe=Le;exports.resolvePinnedAddress=J;exports.resolveRootDir=de;exports.stripApiPrefix=ee;exports.userDataSchema=_;//# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map