@vbarbarosh/node-helpers 3.78.0 → 3.79.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.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/dist/browser.js +1 -1
  3. package/dist/index.html +8 -16
  4. package/package.json +5 -5
  5. package/src/crypto_decrypt_aes256.js +43 -7
  6. package/src/crypto_decrypt_aes256.md +15 -10
  7. package/src/crypto_encrypt_aes256.js +23 -9
  8. package/src/crypto_encrypt_aes256.md +20 -10
  9. package/src/escape_content_disposition.js +8 -2
  10. package/src/escape_content_disposition.md +10 -2
  11. package/src/escape_content_disposition_utf8.js +11 -0
  12. package/src/escape_content_disposition_utf8.md +11 -0
  13. package/src/fastdl.js +7 -1
  14. package/src/fastdl.md +5 -0
  15. package/src/format_date_fs.js +8 -0
  16. package/src/format_error_report.js +102 -4
  17. package/src/format_error_report.md +47 -0
  18. package/src/http_get_stream_range.js +5 -1
  19. package/src/http_get_stream_range.md +2 -1
  20. package/src/http_stream_range.js +14 -1
  21. package/src/http_stream_range.md +3 -1
  22. package/src/make_progress.js +16 -9
  23. package/src/make_progress.md +7 -4
  24. package/src/pgid_exists.js +5 -0
  25. package/src/pgid_exists.md +4 -0
  26. package/src/pgid_kill_grace.js +5 -0
  27. package/src/pgid_kill_grace.md +3 -0
  28. package/src/pid_exists.js +5 -0
  29. package/src/pid_exists.md +4 -0
  30. package/src/pid_kill_grace.js +5 -0
  31. package/src/pid_kill_grace.md +3 -0
  32. package/src/plural.js +1 -1
  33. package/src/plural.md +13 -10
  34. package/src/plural_ru.js +27 -0
  35. package/src/plural_ru.md +21 -0
  36. package/src/redis_poll_zshift_callback_rpush.js +4 -0
  37. package/src/redis_poll_zshift_callback_rpush.md +4 -3
  38. package/src/redis_poll_zshift_spawn_rpush.js +4 -0
  39. package/src/redis_poll_zshift_spawn_rpush.md +4 -3
  40. package/src/shell.js +1 -1
  41. package/src/shell.md +2 -1
  42. package/src/shell_json.js +1 -1
  43. package/src/shell_json.md +2 -1
  44. package/src/stream_strpbrk.js +9 -1
  45. package/src/stream_xml_analyze.js +6 -1
  46. package/src/stream_xml_parse.js +11 -3
  47. package/src/urlparts.js +4 -1
  48. package/src/urlparts.md +6 -0
package/README.md CHANGED
@@ -20,7 +20,7 @@ npm install @vbarbarosh/node-helpers
20
20
  A subset of `@vbarbarosh/node-helpers` is available for browser:
21
21
 
22
22
  ```html
23
- <script src="https://unpkg.com/@vbarbarosh/node-helpers@3.78.0/dist/browser.js"></script>
23
+ <script src="https://unpkg.com/@vbarbarosh/node-helpers@3.79.0/dist/browser.js"></script>
24
24
  ```
25
25
 
26
26
  ⚠️ All browser functions are exposed globally.
package/dist/browser.js CHANGED
@@ -1 +1 @@
1
- (()=>{var t={932(t){t.exports=function(t){return"number"==typeof t&&isFinite(t)}},5392(t,e,n){const r=n(932);t.exports=function(t,e){return r(t)&&t>e}},4106(t,e){function n(t,e){var n=[],r=[];return null==e&&(e=function(t,e){return n[0]===e?"[Circular ~]":"[Circular ~."+r.slice(0,n.indexOf(e)).join(".")+"]"}),function(o,s){if(n.length>0){var u=n.indexOf(this);~u?n.splice(u+1):n.push(this),~u?r.splice(u,1/0,o):r.push(o),~n.indexOf(s)&&(s=e.call(this,o,s))}else n.push(s);return null==t?s:t.call(this,o,s)}}(t.exports=function(t,e,r,o){return JSON.stringify(t,n(e,o),r)}).getSerialize=n},6083(t){t.exports=function(t=[],e=1){if(e<1)throw new Error("Limit value should be at least 1");const n=[];for(let r=0,o=t.length;r<o;r+=e)n.push(t.slice(r,r+e));return n}},2458(t,e,n){const r=n(7727);t.exports=function(t=[],e=1,n=1){const o=r(1,e,Math.floor(t.length/n)),s=Math.floor(t.length/o);let u=t.length%o;const c=[];for(let e=0;e<t.length;){const n=s+(u?1:0);c.push(t.slice(e,e+n)),e+=n,u&&u--}return c}},353(t,e,n){const r=n(4476),o=1e-5;t.exports=function(t,e){const n=Math.floor(t*e);if(n<=1)return[0];const s=[];for(let e=0;e<n;++e)s.push(r(e/(n-1)*(t-o),o));return s}},6372(t,e,n){const r=n(6785);t.exports=function(t,e=Number){return t.map(t=>e(t)).reduce((t,e)=>r(t,e),0)}},3537(t){t.exports=function(t,e){const n=new Map;return t.forEach(function(t){const r=e(t);n.has(r)||n.set(r,{key:r,items:[]}),n.get(r).items.push(t)}),n.values().toArray()}},2976(t){t.exports=function(t,e){const n={};return t.forEach(function(t){const r=e(t);n[r]=n[r]||{key:r,items:[]},n[r].items.push(t)}),n}},2166(t,e,n){const r=n(956);t.exports=function(t,e=r){const n={};return t.forEach(t=>n[e(t)]=t),n}},7866(t,e,n){const r=n(7347);t.exports=function(t,e=Number){return t.map(t=>e(t)).reduce((t,e)=>r(t,e),1)}},6772(t,e,n){const r=n(956);t.exports=function(t,e=r){let n=null,o=null,s=!1;return t.forEach(function(t){const r=e(t);Number.isNaN(r)||(!s||o<r)&&(s=!0,o=r,n=t)}),n}},42(t,e,n){const r=n(956);t.exports=function(t,e=r){let n=null,o=null,s=!1;return t.forEach(function(t){const r=e(t);Number.isNaN(r)||(!s||o>r)&&(s=!0,o=r,n=t)}),n}},3685(t){t.exports=function t(e,n=e.length){if(n!==e.length)return function(e,n){if(0===n)return[[]];if(e.length<n)return[];const r=[];for(let o=0;o<e.length;o++){const s=e.slice(0,o).concat(e.slice(o+1));for(const u of t(s,n-1))r.push([e[o],...u])}return r}(e,n);let r,o,s=(e=e.slice()).length,u=[e.slice()],c=new Array(s).fill(0),a=1;for(;a<s;)c[a]<a?(r=a%2&&c[a],o=e[a],e[a]=e[r],e[r]=o,++c[a],a=1,u.push(e.slice())):(c[a]=0,++a);return u}},4943(t){t.exports=function(t){for(let e=t.length;--e>0;){const n=Math.floor(Math.random()*(e+1)),r=t[e];t[e]=t[n],t[n]=r}return t}},2356(t,e,n){const r=n(8310),o=n(4332);t.exports=function(t,e,n=r){const s=new Map;return t.sort(function(t,r){if(!s.has(t)){const n=e(t);s.set(t,Array.isArray(n)?n:[n])}if(!s.has(r)){const t=e(r);s.set(r,Array.isArray(t)?t:[t])}return o(s.get(t),s.get(r),n)})}},5181(t,e,n){const r=n(8310);t.exports=function(t,e,n,o=r){const s=Object.create(null);return n.forEach((t,e)=>s[t]=e+1),t.sort(function(t,n){const r=s[e(t)],u=s[e(n)];return r&&u?r-u:r?-1:u?1:o(t,n)})}},453(t){t.exports=function(t,e=Number){let n=0;return t.forEach(function(t){n+=e(t)}),n}},3523(t,e,n){const r=n(956);t.exports=function(t,e=r){const n=new Set;return t.filter(function(t){const r=e(t);return!n.has(r)&&(n.add(r),!0)})}},6382(t,e,n){const r=n(956);t.exports=function(t,e=r){const n=[],o=new Set;for(let r=t.length;--r>=0;){const s=t[r],u=e(s);o.has(u)||(o.add(u),n.push(s))}return n.reverse()}},7727(t){t.exports=function(t,e,n){return Math.max(t,Math.min(e,n))}},9870(t){t.exports=function(t,e){return t.setHours(t.getHours()+e),t}},5799(t){t.exports=function(t,e){return t.setMilliseconds(t.getMilliseconds()+e),t}},3160(t){t.exports=function(t,e){return t.setMinutes(t.getMinutes()+e),t}},1570(t){t.exports=function(t,e){if(!e)return t;const n=t.getDate();return t.setMonth(t.getMonth()+e),t.getDate()!==n&&t.setDate(0),t}},9296(t){t.exports=function(t,e){return t.setSeconds(t.getSeconds()+e),t}},5488(t){t.exports=function(t,e){return Math.floor((t.getTime()-e.getTime())/1e3)}},3752(t){t.exports=function(t){const e=t.getFullYear();return e%4==0&&e%100!=0||e%400==0}},3150(t){t.exports=function(t){return t=Math.trunc(Math.abs(t)),[Math.floor(t/36e5),Math.floor(t%36e5/6e4),Math.floor(t%6e4/1e3),t%1e3]}},2806(t){t.exports=function(t,e){return t.getTime()-e.getTime()}},8310(t,e,n){const r=n(7668);t.exports=function(t,e){return"string"==typeof t&&"string"==typeof e?r(t,e):t<e?-1:t>e?1:0}},2092(t,e,n){const r=n(8310);t.exports=function(t,e){return r(e,t)}},3553(t){function e(t){return"-"===t[0]?(t=t.slice(1),function(e,r){return n(r[t],e[t])}):function(e,r){return n(e[t],r[t])}}function n(t,e){return"string"==typeof t&&"string"==typeof e?t.localeCompare(e):t-e}t.exports=function(t){const n=t.map(e);switch(n.length){case 0:return()=>0;case 1:return n[0];case 2:return function(t,e){return n[0](t,e)||n[1](t,e)};case 3:return function(t,e){return n[0](t,e)||n[1](t,e)||n[2](t,e)};default:return function(t,e){for(let r=0;r<n.length;++r){const o=n[r](t,e);if(o)return o}return 0}}}},3129(t){t.exports=function(t,e){return t-e}},4332(t,e,n){const r=n(8310);t.exports=function(t,e,n=r){const o=Math.min(t.length,e.length);for(let r=0;r<o;++r){const o=n(t[r],e[r]);if(o)return o}return 0}},3106(t){t.exports=function(t,e){return t<e?-1:t>e?1:0}},2271(t){t.exports=function(t,e){return t.localeCompare(e,void 0,{sensitivity:"base"})}},4457(t){t.exports=function(t,e){return t.localeCompare(e,void 0,{sensitivity:"variant"})}},7668(t){function e(t,e){const r=t.replace(/^0+/,""),o=e.replace(/^0+/,"");return r.length-o.length||n(r,o)||t.length-e.length}function n(t,e){return t<e?-1:t>e?1:0}t.exports=function(t,r){const o=t.match(/\d+|\D+/g)||[],s=r.match(/\d+|\D+/g)||[],u=Math.min(o.length,s.length);for(let t=0;t<u;++t){const r=o[t][0]<="9"&&s[t][0]<="9"&&o[t][0]>="0"&&s[t][0]>="0"?e(o[t],s[t]):n(o[t],s[t]);if(r)return r}return o.length-s.length}},4385(t){t.exports=function(t,e){return t.localeCompare(e,void 0,{numeric:!0,sensitivity:"base"})}},8951(t){t.exports=function(t,e){return t.localeCompare(e,void 0,{numeric:!0,sensitivity:"variant"})}},4734(t,e,n){const r=n(8310),o=n(2092),s=n(956),u=Symbol("fcmpx.missing");function c(t){const{read:e,fcmp:n}=a(t);return function(t,r){return n(e(t),e(r))}}function a(t){if("function"==typeof t)return{read:i(t),fcmp:f(r)};if("string"==typeof t){const e=t.startsWith("-");return{read:i(e?t.slice(1):t),fcmp:f(e?o:r)}}const e=t.fcmp,n=i(t.read),s=f(t.desc?e?(t,n)=>e(n,t):o:e??r);if(Array.isArray(t.top)){const e=new Map(t.top.map((t,e)=>[t,e]));return{read:t=>e.get(n(t))??e.size,fcmp:s}}if(Array.isArray(t.bottom)){const e=new Map(t.bottom.map((t,e)=>[t,e]));return{read:t=>e.get(n(t))??-1,fcmp:s}}return{read:n,fcmp:s}}function i(t){if(void 0===t||""===t||"."===t)return s;if("function"==typeof t)return function(e){return t(e)??u};if("string"==typeof t){const e=t.split("."),[n,r,o,s,c]=e;switch(e.length){case 1:return t=>t?.[n]??u;case 2:return t=>t?.[n]?.[r]??u;case 3:return t=>t?.[n]?.[r]?.[o]??u;case 4:return t=>t?.[n]?.[r]?.[o]?.[s]??u;case 5:return t=>t?.[n]?.[r]?.[o]?.[s]?.[c]??u;default:return t=>e.reduce((t,e)=>t?.[e],t)??u}}return e=>e?.[t]??u}function f(t){return function(e,n){return e===u?n===u?0:1:n===u?-1:t(e,n)}}t.exports=function t(e){if("string"==typeof e)return e.includes(",")?t(e.split(",")):c(e);if(!Array.isArray(e))return c(e);const n=e.map(a);return function(t,e){const r=n.map(e=>e.read(t)),o=n.map(t=>t.read(e));for(let t=0;t<n.length;++t){const e=n[t].fcmp(r[t],o[t]);if(e)return e}return 0}}},7960(t){function e(t){let e,n,r;return e=t.replaceAll("^",""),n=e.length!==t.length,e=(t=e).replaceAll("$",""),r=e.length!==t.length,e=(t=e).replaceAll("!",""),(t.length-e.length)%2?n&&r?t=>!(t.startsWith(e)&&t.endsWith(e)):n?t=>!t.startsWith(e):r?t=>!t.endsWith(e):t=>!t.includes(e):n&&r?t=>t.startsWith(e)&&t.endsWith(e):n?t=>t.startsWith(e):r?t=>t.endsWith(e):t=>t.includes(e)}t.exports=function(t){let n,r,o,s;const u=function(t){return t.split("/").filter(t=>t).map(e)}(t);switch(u.length){case 0:return()=>!0;case 1:return u[0];case 2:return[n,r]=u,t=>n(t)&&r(t);case 3:return[n,r,o]=u,t=>n(t)&&r(t)&&o(t);case 4:return[n,r,o,s]=u,t=>n(t)&&r(t)&&o(t)&&s(t);default:return t=>u.every(e=>e(t))}}},6017(t){t.exports=function(t){if("number"!=typeof t||Number.isNaN(t))return"n/a";if(!t)return"0KB";if(t<1024)return"1KB";const e=Math.floor(Math.log(t)/Math.log(1024));return`${(t/1024**e).toFixed(t>1048576?2:0)}${["B","KB","MB","GB","TB","PB"][e]}`.replace(/\.00/,".0")}},1284(t){t.exports=function(t){return t.getFullYear()+`/0${t.getMonth()+1}/0${t.getDate()} 0${t.getHours()}:0${t.getMinutes()}:0${t.getSeconds()}`.replace(/0(\d\d)/g,"$1")}},5224(t){function e(t){return t>9?`${t}`:`0${t}`}t.exports=function(t){return t.getFullYear()+e(t.getMonth()+1)+e(t.getDate())+"_"+(e(t.getHours())+e(t.getMinutes())+e(t.getSeconds()))}},1690(t){function e(t){return t>9?`${t}`:`0${t}`}t.exports=function(t){return t.getFullYear()+"/"+e(t.getMonth()+1)+"/"+e(t.getDate())+" "+e(t.getHours())+":"+e(t.getMinutes())+":"+e(t.getSeconds())}},6997(t){t.exports=function(t){return t.getFullYear()+`/0${t.getMonth()+1}/0${t.getDate()}`.replace(/0(\d\d)/g,"$1")}},9009(t,e,n){const r=n(3332);t.exports=function(t){return t?t.response?.status&&t.response?.statusText&&t.response?.config?function(t){return`\n${t.message??"n/a"}\n\n--- REQUEST ---\n\n${(t.response.config.method||"").toUpperCase()} ${t.response.config.url}\n\n${JSON.stringify(t.response.config.headers||{},null,4).slice(1,-1).replace(/^\s+|,$/gm,"").trim()}\n\n--- RESPONSE ---\n\n${t.response.status} ${t.response.statusText}\n\n${String(t.response.data??"").slice(0,10240)||"n/a"}\n\n--- STACK ---\n\n${t.stack??"n/a"}\n`.trimStart()}(t):t.config?function(t){return`\n${t.message??"n/a"}\n\n--- REQUEST ---\n\n${(t.config.method||"").toUpperCase()} ${t.config.url}\n\n${JSON.stringify(t.config.headers||{},null,4).slice(1,-1).replace(/^\s+|,$/gm,"").trim()}\n\n--- STACK ---\n\n${t.stack??"n/a"}\n`.trimStart()}(t):r({code:t.code,name:t.name,message:t.message??"n/a",stack:t.stack&&t.stack.split(/\n\s*/)},null,4):r({error:void 0===t?"---undefined---":t})}},6015(t){t.exports=function(t,e=6){const[n,r]=t;return(n+r/1e9).toFixed(e)+"s"}},4153(t){t.exports=function(t){if("number"!=typeof t||Number.isNaN(t))return"n/a";if(!t)return"0";if(t<1e3)return t.toFixed(0);const e=parseInt(Math.floor(Math.log(t)/Math.log(1e3)),10);return`${(t/1e3**e).toFixed(2)}${["","K","M","G","T","P"][e]}`}},1120(t,e,n){const r=n(3150);t.exports=function(t){t||(t=0);const e=t<0?"-":"",[n,o,s]=r(t);return n?e+`0${n}:0${o}:0${s}`.replace(/0+(?=\d\d)/g,""):e+`0${o}:0${s}`.replace(/0+(?=\d\d)/g,"")}},7336(t,e,n){const r=n(3150);t.exports=function(t){t||(t=0);const e=t<0?"-":"",[n,o,s,u]=r(10*Math.round(Math.abs(t)/10));return n?e+`0${n}:0${o}:0${s}.0${u/10}`.replace(/0+(?=\d\d)/g,""):e+`0${o}:0${s}.0${u/10}`.replace(/0+(?=\d\d)/g,"")}},495(t,e,n){const r=n(3150);t.exports=function(t,e=!1){t||(t=0);const n=t<0?"-":"",[o,s,u,c]=r(t);return o||e?n+`0${o}:0${s}:0${u}.00${c}`.replace(/0+(?=\d\d+[:.]|\d\d\d)/g,""):n+`0${s}:0${u}.00${c}`.replace(/0+(?=\d\d+[:.]|\d\d\d)/g,"")}},7882(t){t.exports=function(t){return(100*t).toFixed(2).replace(/^(0|100).00$/,"$1")+"%"}},7111(t,e,n){const r=n(6017),o=n(7882),s=n(3527),u=n(5392);t.exports=function({percents:t,total:e,done:n,rate:c,eta:a,duration:i}){const f=u(c,0)?`${r(c)}/s`:"~";if(n>e)return`${r(n)} at ${f} duration=${s(i)}`;if(u(e,0)){const c=u(a,0)?s(a):"~";return`${o(t)} | ${r(n)} of ${r(e)} at ${f} ETA ${c} duration=${s(i)}`}return u(n,0)?`${r(n)} of ~ at ${f} duration=${s(i)}`:u(i,0)?`~ duration=${s(i)}`:"~"}},5051(t,e,n){const r=n(4153),o=n(7882),s=n(3527),u=n(5392);t.exports=function({percents:t,total:e,done:n,rate:c,eta:a,duration:i}){const f=u(c,0)?`${r(c)}/s`:"~";if(n>e)return`${r(n)} at ${f} duration=${s(i)}`;if(u(e,0)){const c=u(a,0)?s(a):"~";return`${o(t)} | ${r(n)} of ${r(e)} at ${f} ETA ${c} duration=${s(i)}`}return u(n,0)?`${r(n)} of ~ at ${f} duration=${s(i)}`:u(i,0)?`~ duration=${s(i)}`:"~"}},3527(t){t.exports=function(t){const e=Math.abs(Math.trunc(t)),n=`0${Math.floor(e/60/60)}:0${Math.floor(e/60)%60}:0${e%60}`.replace(/0(\d\d)/g,"$1");return t<=-1?"-"+n:n}},2677(t){t.exports=function(t){return t.toString().replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g,",")}},4206(t,e,n){const r=n(3719);t.exports=function(t,e){return r.delete(t,{responseType:"json",...e}).then(t=>t.data)}},4131(t,e,n){const r=n(3719);t.exports=function(t,e){return r.get(t,{responseType:"blob",...e}).then(t=>t.data)}},706(t,e,n){const r=n(3719);t.exports=function(t,e){return r.get(t,{responseType:"arraybuffer",...e}).then(t=>t.data)}},3066(t,e,n){const r=n(3719);t.exports=function(t,e){return r.get(t,{responseType:"json",...e}).then(t=>t.data)}},3019(t,e,n){const r=n(3719);t.exports=function(t,e){return r.get(t,{responseType:"text",responseEncoding:"utf8",...e}).then(t=>t.data)}},7379(t,e,n){const r=n(3719);t.exports=function(t,e){return r.head(t,e).then(t=>t.request.res.headers)}},2552(t,e,n){const r=n(3719);t.exports=function(t,e,n){return r.patch(t,e,n).then(t=>t.data)}},7484(t,e,n){const r=n(3719);t.exports=function(t,e,n){return r.post(t,e,n).then(t=>t.data)}},5420(t,e,n){const r=n(8241),o=n(3719);t.exports=function(t,e,n){const s=new r;for(let t=0,n=e.length;t<n;++t){const n=e[t];n.options?s.append(n.name,n.body,n.options):s.append(n.name,n.body)}if(s.getHeaders){const e={...n};return e.headers={...e.headers,...s.getHeaders()},Promise.resolve(o.post(t,s,e)).then(t=>t.data)}return Promise.resolve(o.post(t,s,n)).then(t=>t.data)}},4605(t,e,n){const r=n(3719);t.exports=function(t,e,n){return r.put(t,e,n).then(t=>t.data)}},6357(t,e,n){const r=n(3719);t.exports=function(t,e,n){return r.put(t,e,n).then(t=>t.data)}},2380(t,e,n){const r=n(3719);t.exports=async function(t,e,n){return r.put(t,e,n).then(t=>t.data)}},956(t){t.exports=function(t){return t}},1828(t){t.exports=function(){}},3332(t,e,n){const r=n(4106);t.exports=r},6785(t){t.exports=function t(e,n){return n?t(n,e%n):Math.abs(e)}},7347(t,e,n){const r=n(6785);t.exports=function(t,e){return Math.abs(t*e)/r(t,e)}},792(t){t.exports=function(t,e,n,r){return 0===t&&"string"==typeof r?r:t%10==1&&t%100!=11?e.split("#").join(t):n.split("#").join(t)}},131(t){t.exports=function(t,e){return Math.floor(Math.random()*(e-t+1)+t)}},4476(t){t.exports=function(t,e=1){if(0===e)return t;const n=1/e,r=Math.sign(t),o=function(t){if(Math.floor(t)===t)return 0;const[e,n]=t.toExponential().split("e"),r=(e.split(".")[1]||"").length;return Math.max(0,r-parseInt(n,10))}(e),s=1e-12*r,u=Math.round(t*n+s)/n;return Number(u.toFixed(o))}},7233(t){t.exports=function(t,e){const n=String(t||""),r=Object.entries(e||{});if(!r.length)return n;const o=n.indexOf("#"),s=-1===o?"":n.slice(o),u=-1===o?n:n.slice(0,o),c=u.indexOf("?"),a=-1===c?u:u.slice(0,c),i=-1===c?"":u.slice(c+1),f=new URLSearchParams(i);r.forEach(function([t,e]){switch(e){case null:case void 0:f.delete(t);break;case!0:f.set(t,"1");break;case!1:f.set(t,"0");break;default:f.set(t,String(e))}});const p=f.toString();return a+(p?`?${p}`:"")+s}},6966(t,e,n){const r=n(2468);t.exports=function(t){return new r(function(e,n){t(function(t,r){t?n(t):e(r)})})}},7072(t,e,n){const r={"./array_chunk.js":6083,"./array_chunk_balanced.js":2458,"./array_fps.js":353,"./array_gcd.js":6372,"./array_group.js":3537,"./array_group_map.js":2976,"./array_index.js":2166,"./array_lcm.js":7866,"./array_max.js":6772,"./array_min.js":42,"./array_permutations.js":3685,"./array_shuffle.js":4943,"./array_sort.js":2356,"./array_sort_other.js":5181,"./array_sum.js":453,"./array_unique.js":3523,"./array_unique_last.js":6382,"./date_add_hours.js":9870,"./date_add_milliseconds.js":5799,"./date_add_minutes.js":3160,"./date_add_months.js":1570,"./date_add_seconds.js":9296,"./date_diff_seconds.js":5488,"./date_is_leap_year.js":3752,"./fcmp_dates.js":2806,"./fcmp_default.js":8310,"./fcmp_default_desc.js":2092,"./fcmp_from_spec.js":3553,"./fcmp_numbers.js":3129,"./fcmp_tuples.js":4332,"./fcmp_utf8_bin.js":3106,"./fcmp_utf8_ci.js":2271,"./fcmp_utf8_cs.js":4457,"./fcmp_utf8_natural_bin.js":7668,"./fcmp_utf8_natural_ci.js":4385,"./fcmp_utf8_natural_cs.js":8951,"./fcmpx.js":4734,"./filter1_from_spec.js":7960,"./format_bytes.js":6017,"./format_date.js":1284,"./format_date_fs.js":5224,"./format_date_human.js":1690,"./format_date_ymd.js":6997,"./format_error_report.js":9009,"./format_hrtime.js":6015,"./format_kilo.js":4153,"./format_ms.js":1120,"./format_ms2.js":7336,"./format_ms3.js":495,"./format_percents.js":7882,"./format_progress_bytes.js":7111,"./format_progress_kilo.js":5051,"./format_seconds.js":3527,"./format_thousands.js":2677,"./http_delete.js":4206,"./http_get_blob.js":4131,"./http_get_buffer.js":706,"./http_get_json.js":3066,"./http_get_utf8.js":3019,"./http_head.js":7379,"./http_patch_json.js":2552,"./http_post_json.js":7484,"./http_post_multipart.js":5420,"./http_put_buffer.js":4605,"./http_put_json.js":6357,"./http_put_utf8.js":2380,"./identity.js":956,"./ignore.js":1828,"./plural.js":792,"./random_int.js":131,"./urlmod.js":7233,"./waitcb.js":6966};function o(t){const e=s(t);return n(e)}function s(t){if(!n.o(r,t)){const e=new Error("Cannot find module '"+t+"'");throw e.code="MODULE_NOT_FOUND",e}return r[t]}o.keys=function(){return Object.keys(r)},o.resolve=s,t.exports=o,o.id=7072},8241(t){"use strict";t.exports=FormData},2468(t){"use strict";t.exports=Promise},3719(t){"use strict";t.exports=axios}};const e={};function n(r){const o=e[r];if(void 0!==o)return o.exports;const s=e[r]={exports:{}};return t[r](s,s.exports,n),s.exports}n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);const r=new URL(document.currentScript.src).searchParams.get("var")??"h";if(void 0!==window[r])console.log(`❌ @vbarbarosh/node-helpers@3.78.0 was not injected — window.${r} is already in use`);else{console.log(`🎉 @vbarbarosh/node-helpers@3.78.0 successfully exposed as window.${r}`),window[r]={};const t=n(7072);t.keys().forEach(function(e){const[,n]=e.match(/([^/]+)\.js$/);window[r][n]=t(e)})}})();
1
+ (()=>{var t={932(t){t.exports=function(t){return"number"==typeof t&&isFinite(t)}},5392(t,e,n){const r=n(932);t.exports=function(t,e){return r(t)&&t>e}},4106(t,e){function n(t,e){var n=[],r=[];return null==e&&(e=function(t,e){return n[0]===e?"[Circular ~]":"[Circular ~."+r.slice(0,n.indexOf(e)).join(".")+"]"}),function(o,s){if(n.length>0){var u=n.indexOf(this);~u?n.splice(u+1):n.push(this),~u?r.splice(u,1/0,o):r.push(o),~n.indexOf(s)&&(s=e.call(this,o,s))}else n.push(s);return null==t?s:t.call(this,o,s)}}(t.exports=function(t,e,r,o){return JSON.stringify(t,n(e,o),r)}).getSerialize=n},6083(t){t.exports=function(t=[],e=1){if(e<1)throw new Error("Limit value should be at least 1");const n=[];for(let r=0,o=t.length;r<o;r+=e)n.push(t.slice(r,r+e));return n}},2458(t,e,n){const r=n(7727);t.exports=function(t=[],e=1,n=1){const o=r(1,e,Math.floor(t.length/n)),s=Math.floor(t.length/o);let u=t.length%o;const c=[];for(let e=0;e<t.length;){const n=s+(u?1:0);c.push(t.slice(e,e+n)),e+=n,u&&u--}return c}},353(t,e,n){const r=n(4476),o=1e-5;t.exports=function(t,e){const n=Math.floor(t*e);if(n<=1)return[0];const s=[];for(let e=0;e<n;++e)s.push(r(e/(n-1)*(t-o),o));return s}},6372(t,e,n){const r=n(6785);t.exports=function(t,e=Number){return t.map(t=>e(t)).reduce((t,e)=>r(t,e),0)}},3537(t){t.exports=function(t,e){const n=new Map;return t.forEach(function(t){const r=e(t);n.has(r)||n.set(r,{key:r,items:[]}),n.get(r).items.push(t)}),n.values().toArray()}},2976(t){t.exports=function(t,e){const n={};return t.forEach(function(t){const r=e(t);n[r]=n[r]||{key:r,items:[]},n[r].items.push(t)}),n}},2166(t,e,n){const r=n(956);t.exports=function(t,e=r){const n={};return t.forEach(t=>n[e(t)]=t),n}},7866(t,e,n){const r=n(7347);t.exports=function(t,e=Number){return t.map(t=>e(t)).reduce((t,e)=>r(t,e),1)}},6772(t,e,n){const r=n(956);t.exports=function(t,e=r){let n=null,o=null,s=!1;return t.forEach(function(t){const r=e(t);Number.isNaN(r)||(!s||o<r)&&(s=!0,o=r,n=t)}),n}},42(t,e,n){const r=n(956);t.exports=function(t,e=r){let n=null,o=null,s=!1;return t.forEach(function(t){const r=e(t);Number.isNaN(r)||(!s||o>r)&&(s=!0,o=r,n=t)}),n}},3685(t){t.exports=function t(e,n=e.length){if(n!==e.length)return function(e,n){if(0===n)return[[]];if(e.length<n)return[];const r=[];for(let o=0;o<e.length;o++){const s=e.slice(0,o).concat(e.slice(o+1));for(const u of t(s,n-1))r.push([e[o],...u])}return r}(e,n);let r,o,s=(e=e.slice()).length,u=[e.slice()],c=new Array(s).fill(0),a=1;for(;a<s;)c[a]<a?(r=a%2&&c[a],o=e[a],e[a]=e[r],e[r]=o,++c[a],a=1,u.push(e.slice())):(c[a]=0,++a);return u}},4943(t){t.exports=function(t){for(let e=t.length;--e>0;){const n=Math.floor(Math.random()*(e+1)),r=t[e];t[e]=t[n],t[n]=r}return t}},2356(t,e,n){const r=n(8310),o=n(4332);t.exports=function(t,e,n=r){const s=new Map;return t.sort(function(t,r){if(!s.has(t)){const n=e(t);s.set(t,Array.isArray(n)?n:[n])}if(!s.has(r)){const t=e(r);s.set(r,Array.isArray(t)?t:[t])}return o(s.get(t),s.get(r),n)})}},5181(t,e,n){const r=n(8310);t.exports=function(t,e,n,o=r){const s=Object.create(null);return n.forEach((t,e)=>s[t]=e+1),t.sort(function(t,n){const r=s[e(t)],u=s[e(n)];return r&&u?r-u:r?-1:u?1:o(t,n)})}},453(t){t.exports=function(t,e=Number){let n=0;return t.forEach(function(t){n+=e(t)}),n}},3523(t,e,n){const r=n(956);t.exports=function(t,e=r){const n=new Set;return t.filter(function(t){const r=e(t);return!n.has(r)&&(n.add(r),!0)})}},6382(t,e,n){const r=n(956);t.exports=function(t,e=r){const n=[],o=new Set;for(let r=t.length;--r>=0;){const s=t[r],u=e(s);o.has(u)||(o.add(u),n.push(s))}return n.reverse()}},7727(t){t.exports=function(t,e,n){return Math.max(t,Math.min(e,n))}},9870(t){t.exports=function(t,e){return t.setHours(t.getHours()+e),t}},5799(t){t.exports=function(t,e){return t.setMilliseconds(t.getMilliseconds()+e),t}},3160(t){t.exports=function(t,e){return t.setMinutes(t.getMinutes()+e),t}},1570(t){t.exports=function(t,e){if(!e)return t;const n=t.getDate();return t.setMonth(t.getMonth()+e),t.getDate()!==n&&t.setDate(0),t}},9296(t){t.exports=function(t,e){return t.setSeconds(t.getSeconds()+e),t}},5488(t){t.exports=function(t,e){return Math.floor((t.getTime()-e.getTime())/1e3)}},3752(t){t.exports=function(t){const e=t.getFullYear();return e%4==0&&e%100!=0||e%400==0}},3150(t){t.exports=function(t){return t=Math.trunc(Math.abs(t)),[Math.floor(t/36e5),Math.floor(t%36e5/6e4),Math.floor(t%6e4/1e3),t%1e3]}},2806(t){t.exports=function(t,e){return t.getTime()-e.getTime()}},8310(t,e,n){const r=n(7668);t.exports=function(t,e){return"string"==typeof t&&"string"==typeof e?r(t,e):t<e?-1:t>e?1:0}},2092(t,e,n){const r=n(8310);t.exports=function(t,e){return r(e,t)}},3553(t){function e(t){return"-"===t[0]?(t=t.slice(1),function(e,r){return n(r[t],e[t])}):function(e,r){return n(e[t],r[t])}}function n(t,e){return"string"==typeof t&&"string"==typeof e?t.localeCompare(e):t-e}t.exports=function(t){const n=t.map(e);switch(n.length){case 0:return()=>0;case 1:return n[0];case 2:return function(t,e){return n[0](t,e)||n[1](t,e)};case 3:return function(t,e){return n[0](t,e)||n[1](t,e)||n[2](t,e)};default:return function(t,e){for(let r=0;r<n.length;++r){const o=n[r](t,e);if(o)return o}return 0}}}},3129(t){t.exports=function(t,e){return t-e}},4332(t,e,n){const r=n(8310);t.exports=function(t,e,n=r){const o=Math.min(t.length,e.length);for(let r=0;r<o;++r){const o=n(t[r],e[r]);if(o)return o}return 0}},3106(t){t.exports=function(t,e){return t<e?-1:t>e?1:0}},2271(t){t.exports=function(t,e){return t.localeCompare(e,void 0,{sensitivity:"base"})}},4457(t){t.exports=function(t,e){return t.localeCompare(e,void 0,{sensitivity:"variant"})}},7668(t){function e(t,e){const r=t.replace(/^0+/,""),o=e.replace(/^0+/,"");return r.length-o.length||n(r,o)||t.length-e.length}function n(t,e){return t<e?-1:t>e?1:0}t.exports=function(t,r){const o=t.match(/\d+|\D+/g)||[],s=r.match(/\d+|\D+/g)||[],u=Math.min(o.length,s.length);for(let t=0;t<u;++t){const r=o[t][0]<="9"&&s[t][0]<="9"&&o[t][0]>="0"&&s[t][0]>="0"?e(o[t],s[t]):n(o[t],s[t]);if(r)return r}return o.length-s.length}},4385(t){t.exports=function(t,e){return t.localeCompare(e,void 0,{numeric:!0,sensitivity:"base"})}},8951(t){t.exports=function(t,e){return t.localeCompare(e,void 0,{numeric:!0,sensitivity:"variant"})}},4734(t,e,n){const r=n(8310),o=n(2092),s=n(956),u=Symbol("fcmpx.missing");function c(t){const{read:e,fcmp:n}=a(t);return function(t,r){return n(e(t),e(r))}}function a(t){if("function"==typeof t)return{read:i(t),fcmp:f(r)};if("string"==typeof t){const e=t.startsWith("-");return{read:i(e?t.slice(1):t),fcmp:f(e?o:r)}}const e=t.fcmp,n=i(t.read),s=f(t.desc?e?(t,n)=>e(n,t):o:e??r);if(Array.isArray(t.top)){const e=new Map(t.top.map((t,e)=>[t,e]));return{read:t=>e.get(n(t))??e.size,fcmp:s}}if(Array.isArray(t.bottom)){const e=new Map(t.bottom.map((t,e)=>[t,e]));return{read:t=>e.get(n(t))??-1,fcmp:s}}return{read:n,fcmp:s}}function i(t){if(void 0===t||""===t||"."===t)return s;if("function"==typeof t)return function(e){return t(e)??u};if("string"==typeof t){const e=t.split("."),[n,r,o,s,c]=e;switch(e.length){case 1:return t=>t?.[n]??u;case 2:return t=>t?.[n]?.[r]??u;case 3:return t=>t?.[n]?.[r]?.[o]??u;case 4:return t=>t?.[n]?.[r]?.[o]?.[s]??u;case 5:return t=>t?.[n]?.[r]?.[o]?.[s]?.[c]??u;default:return t=>e.reduce((t,e)=>t?.[e],t)??u}}return e=>e?.[t]??u}function f(t){return function(e,n){return e===u?n===u?0:1:n===u?-1:t(e,n)}}t.exports=function t(e){if("string"==typeof e)return e.includes(",")?t(e.split(",")):c(e);if(!Array.isArray(e))return c(e);const n=e.map(a);return function(t,e){const r=n.map(e=>e.read(t)),o=n.map(t=>t.read(e));for(let t=0;t<n.length;++t){const e=n[t].fcmp(r[t],o[t]);if(e)return e}return 0}}},7960(t){function e(t){let e,n,r;return e=t.replaceAll("^",""),n=e.length!==t.length,e=(t=e).replaceAll("$",""),r=e.length!==t.length,e=(t=e).replaceAll("!",""),(t.length-e.length)%2?n&&r?t=>!(t.startsWith(e)&&t.endsWith(e)):n?t=>!t.startsWith(e):r?t=>!t.endsWith(e):t=>!t.includes(e):n&&r?t=>t.startsWith(e)&&t.endsWith(e):n?t=>t.startsWith(e):r?t=>t.endsWith(e):t=>t.includes(e)}t.exports=function(t){let n,r,o,s;const u=function(t){return t.split("/").filter(t=>t).map(e)}(t);switch(u.length){case 0:return()=>!0;case 1:return u[0];case 2:return[n,r]=u,t=>n(t)&&r(t);case 3:return[n,r,o]=u,t=>n(t)&&r(t)&&o(t);case 4:return[n,r,o,s]=u,t=>n(t)&&r(t)&&o(t)&&s(t);default:return t=>u.every(e=>e(t))}}},6017(t){t.exports=function(t){if("number"!=typeof t||Number.isNaN(t))return"n/a";if(!t)return"0KB";if(t<1024)return"1KB";const e=Math.floor(Math.log(t)/Math.log(1024));return`${(t/1024**e).toFixed(t>1048576?2:0)}${["B","KB","MB","GB","TB","PB"][e]}`.replace(/\.00/,".0")}},1284(t){t.exports=function(t){return t.getFullYear()+`/0${t.getMonth()+1}/0${t.getDate()} 0${t.getHours()}:0${t.getMinutes()}:0${t.getSeconds()}`.replace(/0(\d\d)/g,"$1")}},5224(t){function e(t){return t>9?`${t}`:`0${t}`}t.exports=function(t){return t.getFullYear()+e(t.getMonth()+1)+e(t.getDate())+"_"+(e(t.getHours())+e(t.getMinutes())+e(t.getSeconds()))}},1690(t){function e(t){return t>9?`${t}`:`0${t}`}t.exports=function(t){return t.getFullYear()+"/"+e(t.getMonth()+1)+"/"+e(t.getDate())+" "+e(t.getHours())+":"+e(t.getMinutes())+":"+e(t.getSeconds())}},6997(t){t.exports=function(t){return t.getFullYear()+`/0${t.getMonth()+1}/0${t.getDate()}`.replace(/0(\d\d)/g,"$1")}},9009(t,e,n){const r=n(3332),o="***",s=new Set(["api-key","authorization","cookie","proxy-authorization","set-cookie","x-api-key","x-auth-token"]),u=new Set(["accesstoken","apikey","auth","authorization","key","pass","passwd","password","pwd","secret","sig","signature","token"]);function c(t){if(!t||"object"!=typeof t)return t;if("function"==typeof t.toJSON&&(t=t.toJSON()),Array.isArray(t))return t.map(c);const e={};return Object.entries(t).forEach(function([t,n]){s.has(String(t).toLowerCase())?e[t]=o:e[t]=c(n)}),e}function a(t){return null==t?t:String(t).replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/?#@]*@/i,`$1${o}@`).replace(/([?&])([^=&#]*)=[^&#]*/g,function(t,e,n){return function(t){try{t=decodeURIComponent(t)}catch{}return!!u.has(t.toLowerCase().replace(/[^a-z0-9]/g,""))||t.replace(/([a-z0-9])([A-Z])/g,"$1_$2").toLowerCase().split(/[^a-z0-9]+/).some(t=>u.has(t))}(n)?`${e}${n}=${o}`:t})}t.exports=function(t){return t?t.response?.status&&t.response?.statusText&&t.response?.config?function(t){return`\n${t.message??"n/a"}\n\n--- REQUEST ---\n\n${(t.response.config.method||"").toUpperCase()} ${a(t.response.config.url)}\n\n${JSON.stringify(c(t.response.config.headers||{}),null,4).slice(1,-1).replace(/^\s+|,$/gm,"").trim()}\n\n--- RESPONSE ---\n\n${t.response.status} ${t.response.statusText}\n\n${String(t.response.data??"").slice(0,10240)||"n/a"}\n\n--- STACK ---\n\n${t.stack??"n/a"}\n`.trimStart()}(t):t.config?function(t){return`\n${t.message??"n/a"}\n\n--- REQUEST ---\n\n${(t.config.method||"").toUpperCase()} ${a(t.config.url)}\n\n${JSON.stringify(c(t.config.headers||{}),null,4).slice(1,-1).replace(/^\s+|,$/gm,"").trim()}\n\n--- STACK ---\n\n${t.stack??"n/a"}\n`.trimStart()}(t):r({code:t.code,name:t.name,message:t.message??"n/a",stack:t.stack&&t.stack.split(/\n\s*/)},null,4):r({error:void 0===t?"---undefined---":t})}},6015(t){t.exports=function(t,e=6){const[n,r]=t;return(n+r/1e9).toFixed(e)+"s"}},4153(t){t.exports=function(t){if("number"!=typeof t||Number.isNaN(t))return"n/a";if(!t)return"0";if(t<1e3)return t.toFixed(0);const e=parseInt(Math.floor(Math.log(t)/Math.log(1e3)),10);return`${(t/1e3**e).toFixed(2)}${["","K","M","G","T","P"][e]}`}},1120(t,e,n){const r=n(3150);t.exports=function(t){t||(t=0);const e=t<0?"-":"",[n,o,s]=r(t);return n?e+`0${n}:0${o}:0${s}`.replace(/0+(?=\d\d)/g,""):e+`0${o}:0${s}`.replace(/0+(?=\d\d)/g,"")}},7336(t,e,n){const r=n(3150);t.exports=function(t){t||(t=0);const e=t<0?"-":"",[n,o,s,u]=r(10*Math.round(Math.abs(t)/10));return n?e+`0${n}:0${o}:0${s}.0${u/10}`.replace(/0+(?=\d\d)/g,""):e+`0${o}:0${s}.0${u/10}`.replace(/0+(?=\d\d)/g,"")}},495(t,e,n){const r=n(3150);t.exports=function(t,e=!1){t||(t=0);const n=t<0?"-":"",[o,s,u,c]=r(t);return o||e?n+`0${o}:0${s}:0${u}.00${c}`.replace(/0+(?=\d\d+[:.]|\d\d\d)/g,""):n+`0${s}:0${u}.00${c}`.replace(/0+(?=\d\d+[:.]|\d\d\d)/g,"")}},7882(t){t.exports=function(t){return(100*t).toFixed(2).replace(/^(0|100).00$/,"$1")+"%"}},7111(t,e,n){const r=n(6017),o=n(7882),s=n(3527),u=n(5392);t.exports=function({percents:t,total:e,done:n,rate:c,eta:a,duration:i}){const f=u(c,0)?`${r(c)}/s`:"~";if(n>e)return`${r(n)} at ${f} duration=${s(i)}`;if(u(e,0)){const c=u(a,0)?s(a):"~";return`${o(t)} | ${r(n)} of ${r(e)} at ${f} ETA ${c} duration=${s(i)}`}return u(n,0)?`${r(n)} of ~ at ${f} duration=${s(i)}`:u(i,0)?`~ duration=${s(i)}`:"~"}},5051(t,e,n){const r=n(4153),o=n(7882),s=n(3527),u=n(5392);t.exports=function({percents:t,total:e,done:n,rate:c,eta:a,duration:i}){const f=u(c,0)?`${r(c)}/s`:"~";if(n>e)return`${r(n)} at ${f} duration=${s(i)}`;if(u(e,0)){const c=u(a,0)?s(a):"~";return`${o(t)} | ${r(n)} of ${r(e)} at ${f} ETA ${c} duration=${s(i)}`}return u(n,0)?`${r(n)} of ~ at ${f} duration=${s(i)}`:u(i,0)?`~ duration=${s(i)}`:"~"}},3527(t){t.exports=function(t){const e=Math.abs(Math.trunc(t)),n=`0${Math.floor(e/60/60)}:0${Math.floor(e/60)%60}:0${e%60}`.replace(/0(\d\d)/g,"$1");return t<=-1?"-"+n:n}},2677(t){t.exports=function(t){return t.toString().replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g,",")}},4206(t,e,n){const r=n(3719);t.exports=function(t,e){return r.delete(t,{responseType:"json",...e}).then(t=>t.data)}},4131(t,e,n){const r=n(3719);t.exports=function(t,e){return r.get(t,{responseType:"blob",...e}).then(t=>t.data)}},706(t,e,n){const r=n(3719);t.exports=function(t,e){return r.get(t,{responseType:"arraybuffer",...e}).then(t=>t.data)}},3066(t,e,n){const r=n(3719);t.exports=function(t,e){return r.get(t,{responseType:"json",...e}).then(t=>t.data)}},3019(t,e,n){const r=n(3719);t.exports=function(t,e){return r.get(t,{responseType:"text",responseEncoding:"utf8",...e}).then(t=>t.data)}},7379(t,e,n){const r=n(3719);t.exports=function(t,e){return r.head(t,e).then(t=>t.request.res.headers)}},2552(t,e,n){const r=n(3719);t.exports=function(t,e,n){return r.patch(t,e,n).then(t=>t.data)}},7484(t,e,n){const r=n(3719);t.exports=function(t,e,n){return r.post(t,e,n).then(t=>t.data)}},5420(t,e,n){const r=n(8241),o=n(3719);t.exports=function(t,e,n){const s=new r;for(let t=0,n=e.length;t<n;++t){const n=e[t];n.options?s.append(n.name,n.body,n.options):s.append(n.name,n.body)}if(s.getHeaders){const e={...n};return e.headers={...e.headers,...s.getHeaders()},Promise.resolve(o.post(t,s,e)).then(t=>t.data)}return Promise.resolve(o.post(t,s,n)).then(t=>t.data)}},4605(t,e,n){const r=n(3719);t.exports=function(t,e,n){return r.put(t,e,n).then(t=>t.data)}},6357(t,e,n){const r=n(3719);t.exports=function(t,e,n){return r.put(t,e,n).then(t=>t.data)}},2380(t,e,n){const r=n(3719);t.exports=async function(t,e,n){return r.put(t,e,n).then(t=>t.data)}},956(t){t.exports=function(t){return t}},1828(t){t.exports=function(){}},3332(t,e,n){const r=n(4106);t.exports=r},6785(t){t.exports=function t(e,n){return n?t(n,e%n):Math.abs(e)}},7347(t,e,n){const r=n(6785);t.exports=function(t,e){return Math.abs(t*e)/r(t,e)}},792(t){t.exports=function(t,e,n,r){return 0===t&&"string"==typeof r?r:1===t?e.split("#").join(t):n.split("#").join(t)}},6718(t){t.exports=function(t,e,n,r,o){return 0===t&&"string"==typeof o?o:function(t,e,n,r){if(!Number.isInteger(t))return n;const o=Math.abs(t)%10,s=Math.abs(t)%100;return 1===o&&11!==s?e:o>=2&&o<=4&&(s<12||s>14)?n:r}(t,e,n,r).split("#").join(t)}},131(t){t.exports=function(t,e){return Math.floor(Math.random()*(e-t+1)+t)}},4476(t){t.exports=function(t,e=1){if(0===e)return t;const n=1/e,r=Math.sign(t),o=function(t){if(Math.floor(t)===t)return 0;const[e,n]=t.toExponential().split("e"),r=(e.split(".")[1]||"").length;return Math.max(0,r-parseInt(n,10))}(e),s=1e-12*r,u=Math.round(t*n+s)/n;return Number(u.toFixed(o))}},7233(t){t.exports=function(t,e){const n=String(t||""),r=Object.entries(e||{});if(!r.length)return n;const o=n.indexOf("#"),s=-1===o?"":n.slice(o),u=-1===o?n:n.slice(0,o),c=u.indexOf("?"),a=-1===c?u:u.slice(0,c),i=-1===c?"":u.slice(c+1),f=new URLSearchParams(i);r.forEach(function([t,e]){switch(e){case null:case void 0:f.delete(t);break;case!0:f.set(t,"1");break;case!1:f.set(t,"0");break;default:f.set(t,String(e))}});const p=f.toString();return a+(p?`?${p}`:"")+s}},6966(t,e,n){const r=n(2468);t.exports=function(t){return new r(function(e,n){t(function(t,r){t?n(t):e(r)})})}},9258(t,e,n){const r={"./array_chunk.js":6083,"./array_chunk_balanced.js":2458,"./array_fps.js":353,"./array_gcd.js":6372,"./array_group.js":3537,"./array_group_map.js":2976,"./array_index.js":2166,"./array_lcm.js":7866,"./array_max.js":6772,"./array_min.js":42,"./array_permutations.js":3685,"./array_shuffle.js":4943,"./array_sort.js":2356,"./array_sort_other.js":5181,"./array_sum.js":453,"./array_unique.js":3523,"./array_unique_last.js":6382,"./date_add_hours.js":9870,"./date_add_milliseconds.js":5799,"./date_add_minutes.js":3160,"./date_add_months.js":1570,"./date_add_seconds.js":9296,"./date_diff_seconds.js":5488,"./date_is_leap_year.js":3752,"./fcmp_dates.js":2806,"./fcmp_default.js":8310,"./fcmp_default_desc.js":2092,"./fcmp_from_spec.js":3553,"./fcmp_numbers.js":3129,"./fcmp_tuples.js":4332,"./fcmp_utf8_bin.js":3106,"./fcmp_utf8_ci.js":2271,"./fcmp_utf8_cs.js":4457,"./fcmp_utf8_natural_bin.js":7668,"./fcmp_utf8_natural_ci.js":4385,"./fcmp_utf8_natural_cs.js":8951,"./fcmpx.js":4734,"./filter1_from_spec.js":7960,"./format_bytes.js":6017,"./format_date.js":1284,"./format_date_fs.js":5224,"./format_date_human.js":1690,"./format_date_ymd.js":6997,"./format_error_report.js":9009,"./format_hrtime.js":6015,"./format_kilo.js":4153,"./format_ms.js":1120,"./format_ms2.js":7336,"./format_ms3.js":495,"./format_percents.js":7882,"./format_progress_bytes.js":7111,"./format_progress_kilo.js":5051,"./format_seconds.js":3527,"./format_thousands.js":2677,"./http_delete.js":4206,"./http_get_blob.js":4131,"./http_get_buffer.js":706,"./http_get_json.js":3066,"./http_get_utf8.js":3019,"./http_head.js":7379,"./http_patch_json.js":2552,"./http_post_json.js":7484,"./http_post_multipart.js":5420,"./http_put_buffer.js":4605,"./http_put_json.js":6357,"./http_put_utf8.js":2380,"./identity.js":956,"./ignore.js":1828,"./plural.js":792,"./plural_ru.js":6718,"./random_int.js":131,"./urlmod.js":7233,"./waitcb.js":6966};function o(t){const e=s(t);return n(e)}function s(t){if(!n.o(r,t)){const e=new Error("Cannot find module '"+t+"'");throw e.code="MODULE_NOT_FOUND",e}return r[t]}o.keys=function(){return Object.keys(r)},o.resolve=s,t.exports=o,o.id=9258},8241(t){"use strict";t.exports=FormData},2468(t){"use strict";t.exports=Promise},3719(t){"use strict";t.exports=axios}};const e={};function n(r){const o=e[r];if(void 0!==o)return o.exports;const s=e[r]={exports:{}};return t[r](s,s.exports,n),s.exports}n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);const r=new URL(document.currentScript.src).searchParams.get("var")??"h";if(void 0!==window[r])console.log(`❌ @vbarbarosh/node-helpers@3.79.0 was not injected — window.${r} is already in use`);else{console.log(`🎉 @vbarbarosh/node-helpers@3.79.0 successfully exposed as window.${r}`),window[r]={};const t=n(9258);t.keys().forEach(function(e){const[,n]=e.match(/([^/]+)\.js$/);window[r][n]=t(e)})}})();
package/dist/index.html CHANGED
@@ -1,19 +1,11 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6
- <link href="https://unpkg.com/@vbarbarosh/smcss@1.2.0/dist/reset.css" rel="stylesheet">
7
- <link href="https://unpkg.com/@vbarbarosh/smcss@1.2.0/dist/sm.css" rel="stylesheet">
8
- <title>@vbarbarsh/node-helpers</title>
9
- </head>
10
- <body>
1
+ <!doctype html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,shrink-to-fit=no"><link href=https://unpkg.com/@vbarbarosh/smcss@1.2.0/dist/reset.css rel=stylesheet><link href=https://unpkg.com/@vbarbarosh/smcss@1.2.0/dist/sm.css rel=stylesheet><title>@vbarbarsh/node-helpers</title></head><body>
11
2
 
12
- <div id="app"></div>
3
+ <div id=app></div>
13
4
 
14
- <script src="https://unpkg.com/axios@1.18.1/dist/axios.js"></script>
15
- <script src="https://unpkg.com/bluebird@3.7.2/js/browser/bluebird.js"></script>
16
- <script src="browser.js"></script>
5
+ <script src=https://unpkg.com/axios@1.20.0/dist/axios.js></script>
6
+ <script src=https://unpkg.com/bluebird@3.7.2/js/browser/bluebird.js></script>
7
+ <script src=browser.js></script>
17
8
 
18
- </body>
19
- </html>
9
+
10
+
11
+ </body></html>
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "Vladimir Barbarosh <vladimir.barbarosh@gmail.com>",
3
3
  "license": "MIT",
4
4
  "name": "@vbarbarosh/node-helpers",
5
- "version": "3.78.0",
5
+ "version": "3.79.0",
6
6
  "description": "A set of helpers for JavaScript/Node.js",
7
7
  "unpkg": "dist/browser.js",
8
8
  "jsdelivr": "dist/browser.js",
@@ -46,8 +46,8 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@paralleldrive/cuid2": "^3.3.0",
49
- "@vbarbarosh/type-helpers": "^0.2.0",
50
- "axios": "1.18.1",
49
+ "@vbarbarosh/type-helpers": "^0.4.0",
50
+ "axios": "1.20.0",
51
51
  "bluebird": "^3.7.2",
52
52
  "csv-parse": "^7.0.1",
53
53
  "form-data": "^4.0.1",
@@ -65,12 +65,12 @@
65
65
  "devDependencies": {
66
66
  "@eslint/js": "^10.0.1",
67
67
  "@vbarbarosh/tree": "^0.5.0",
68
- "c8": "^11.0.0",
68
+ "c8": "^12.0.0",
69
69
  "copy-webpack-plugin": "^14.0.0",
70
70
  "eslint": "^10.7.0",
71
71
  "express": "^5.2.1",
72
72
  "globals": "^17.7.0",
73
- "mocha": "^11.0.1",
73
+ "mocha": "^12.0.3",
74
74
  "redis-v1": "npm:redis@1",
75
75
  "redis-v2": "npm:redis@2",
76
76
  "redis-v3": "npm:redis@3",
@@ -1,16 +1,52 @@
1
1
  const crypto = require('crypto');
2
2
 
3
+ const VERSION = 1;
4
+ const SALT_LENGTH = 16;
5
+ const NONCE_LENGTH = 12;
6
+ const TAG_LENGTH = 16;
7
+ const HEADER_LENGTH = 1 + SALT_LENGTH + NONCE_LENGTH;
8
+
3
9
  /**
4
- * @link https://nodejs.org/api/crypto.html#crypto_class_cipher
5
- * @link https://attacomsian.com/blog/nodejs-encrypt-decrypt-data
10
+ * Decrypts an envelope produced by crypto_encrypt_aes256. Throws when the
11
+ * envelope is malformed, the password is wrong, or the data was tampered
12
+ * with.
13
+ *
14
+ * @link https://nodejs.org/api/crypto.html#class-decipher
6
15
  */
7
16
  function crypto_decrypt_aes256(password, ivenc)
8
17
  {
9
- const key = crypto.createHash('sha256').update(password).digest();
10
- const iv = ivenc.slice(1, ivenc[0] + 1);
11
- const enc = ivenc.slice(ivenc[0] + 1);
12
- const decipher = crypto.createDecipheriv('aes-256-ctr', key, iv);
13
- return Buffer.concat([decipher.update(enc), decipher.final()]);
18
+ if (!(ivenc instanceof Uint8Array)) {
19
+ throw new Error('crypto_decrypt_aes256: envelope should be a Buffer');
20
+ }
21
+ if (!Buffer.isBuffer(ivenc)) {
22
+ ivenc = Buffer.from(ivenc.buffer, ivenc.byteOffset, ivenc.byteLength);
23
+ }
24
+ if (ivenc[0] !== VERSION) {
25
+ throw new Error('crypto_decrypt_aes256: malformed envelope');
26
+ }
27
+ return decrypt_v1(password, ivenc);
28
+ }
29
+
30
+ function decrypt_v1(password, ivenc)
31
+ {
32
+ if (ivenc.length < HEADER_LENGTH + TAG_LENGTH) {
33
+ throw new Error('crypto_decrypt_aes256: malformed envelope');
34
+ }
35
+ const header = ivenc.subarray(0, HEADER_LENGTH);
36
+ const salt = ivenc.subarray(1, 1 + SALT_LENGTH);
37
+ const nonce = ivenc.subarray(1 + SALT_LENGTH, HEADER_LENGTH);
38
+ const tag = ivenc.subarray(HEADER_LENGTH, HEADER_LENGTH + TAG_LENGTH);
39
+ const enc = ivenc.subarray(HEADER_LENGTH + TAG_LENGTH);
40
+ const key = crypto.scryptSync(password, salt, 32);
41
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
42
+ decipher.setAAD(header);
43
+ decipher.setAuthTag(tag);
44
+ try {
45
+ return Buffer.concat([decipher.update(enc), decipher.final()]);
46
+ }
47
+ catch (error) {
48
+ throw new Error('crypto_decrypt_aes256: authentication failed (wrong password or tampered data)', {cause: error});
49
+ }
14
50
  }
15
51
 
16
52
  module.exports = crypto_decrypt_aes256;
@@ -1,13 +1,18 @@
1
- Decrypts a `Buffer` produced by `crypto_encrypt_aes256` (`aes-256-ctr`, key =
2
- `sha256(password)`, layout `[iv_length, iv, ciphertext]`). Returns the
3
- plaintext as a `Buffer` — call `.toString('utf8')` for text.
1
+ Decrypts a `Buffer` envelope produced by `crypto_encrypt_aes256` and returns
2
+ the plaintext as a `Buffer` — call `.toString('utf8')` for text.
3
+
4
+ The version 1 envelope is
5
+ `[0x01][16-byte salt][12-byte nonce][16-byte GCM tag][ciphertext]`
6
+ (`aes-256-gcm`, key = `crypto.scryptSync(password, salt, 32)`, header
7
+ authenticated as additional data). Decryption **throws** when:
8
+
9
+ - the password is wrong or the envelope was modified in any byte
10
+ (`authentication failed (wrong password or tampered data)`);
11
+ - the envelope is malformed: not a `Buffer`/`Uint8Array`, too short, or an
12
+ unknown version byte (`malformed envelope`).
4
13
 
5
14
  ```js
6
- const ivenc = crypto_encrypt_aes256('pass123', 'hello');
7
- crypto_decrypt_aes256('pass123', ivenc).toString('utf8'); // 'hello'
8
- crypto_decrypt_aes256('wrong', ivenc).toString('utf8'); // garbage, no error
15
+ const envelope = crypto_encrypt_aes256('pass123', 'hello');
16
+ crypto_decrypt_aes256('pass123', envelope).toString('utf8'); // 'hello'
17
+ crypto_decrypt_aes256('wrong', envelope); // throws
9
18
  ```
10
-
11
- **No integrity check:** CTR mode does not authenticate — a wrong password or
12
- tampered ciphertext yields garbage bytes, never an error, so verify the result
13
- yourself if you need to detect bad input.
@@ -1,18 +1,32 @@
1
1
  const crypto = require('crypto');
2
2
 
3
+ const VERSION = 1;
4
+ const SALT_LENGTH = 16;
5
+ const NONCE_LENGTH = 12;
6
+
3
7
  /**
4
- * @link https://nodejs.org/api/crypto.html#crypto_class_cipher
5
- * @link https://attacomsian.com/blog/nodejs-encrypt-decrypt-data
8
+ * Authenticated encryption with a password.
9
+ *
10
+ * Envelope (version 1):
11
+ * [0x01][16-byte salt][12-byte nonce][16-byte GCM tag][ciphertext]
12
+ *
13
+ * The key is scrypt(password, salt, 32) and the version/salt/nonce header
14
+ * is authenticated together with the ciphertext (AES-256-GCM with the
15
+ * header as additional data), so any modification is detected on decrypt.
16
+ *
17
+ * @link https://nodejs.org/api/crypto.html#cryptoscryptsyncpassword-salt-keylen-options
18
+ * @link https://nodejs.org/api/crypto.html#class-cipher
6
19
  */
7
20
  function crypto_encrypt_aes256(password, data)
8
21
  {
9
- const iv = crypto.randomBytes(16);
10
- const key = crypto.createHash('sha256').update(password).digest();
11
- const cipher = crypto.createCipheriv('aes-256-ctr', key, iv);
12
- if (iv.length > 255) {
13
- throw new Error('iv size is too big');
14
- }
15
- return Buffer.concat([Buffer.from([iv.length]), iv, cipher.update(data), cipher.final()]);
22
+ const salt = crypto.randomBytes(SALT_LENGTH);
23
+ const nonce = crypto.randomBytes(NONCE_LENGTH);
24
+ const key = crypto.scryptSync(password, salt, 32);
25
+ const header = Buffer.concat([Buffer.from([VERSION]), salt, nonce]);
26
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
27
+ cipher.setAAD(header);
28
+ const enc = Buffer.concat([cipher.update(data), cipher.final()]);
29
+ return Buffer.concat([header, cipher.getAuthTag(), enc]);
16
30
  }
17
31
 
18
32
  module.exports = crypto_encrypt_aes256;
@@ -1,13 +1,23 @@
1
- Encrypts data with `aes-256-ctr`. The key is `sha256(password)`, the IV is a
2
- fresh random 16 bytes on every call, so encrypting the same input twice gives
3
- different output. Returns a `Buffer` laid out as `[iv_length, iv, ciphertext]`
4
- (1 + 16 + data-length bytes); pass it as-is to `crypto_decrypt_aes256`.
1
+ Encrypts data with a password using authenticated encryption. Returns a
2
+ `Buffer` envelope; pass it as-is to `crypto_decrypt_aes256`.
5
3
 
6
- ```js
7
- const ivenc = crypto_encrypt_aes256('pass123', 'hello');
8
- // <Buffer 10 ...> — 22 bytes: 0x10 (iv length), 16-byte iv, 5-byte ciphertext
9
- crypto_decrypt_aes256('pass123', ivenc).toString('utf8'); // 'hello'
4
+ **Envelope (version 1), 45 bytes + data length:**
5
+
6
+ ```
7
+ [0x01][16-byte salt][12-byte nonce][16-byte GCM tag][ciphertext]
10
8
  ```
11
9
 
12
- **No integrity check:** CTR mode does not authenticate — a wrong password or
13
- tampered data decrypts to garbage, not an error.
10
+ - The key is `crypto.scryptSync(password, salt, 32)` with a fresh random salt
11
+ per call, so guessing passwords offline is expensive and the same password
12
+ never yields the same key twice.
13
+ - The cipher is `aes-256-gcm` with a fresh random nonce per call; the
14
+ version/salt/nonce header is authenticated as additional data together with
15
+ the ciphertext. Any modification of the envelope, or a wrong password, makes
16
+ `crypto_decrypt_aes256` throw instead of returning garbage.
17
+ - Encrypting the same input twice gives different output.
18
+
19
+ ```js
20
+ const envelope = crypto_encrypt_aes256('pass123', 'hello');
21
+ // <Buffer 01 ...> — 50 bytes: version, 16-byte salt, 12-byte nonce, 16-byte tag, 5-byte ciphertext
22
+ crypto_decrypt_aes256('pass123', envelope).toString('utf8'); // 'hello'
23
+ ```
@@ -7,8 +7,14 @@ function escape_content_disposition(s)
7
7
  const out = s.replaceAll('\\', '\\\\')
8
8
  .replaceAll('"', '\\"')
9
9
  .replaceAll('%', '\\%')
10
- .replaceAll('\n', '%x0A')
11
- .replaceAll('\0', '%x00');
10
+ // Control characters (\n, \r, \0, \x01..., DEL) are illegal in a
11
+ // header value; encode every one of them as %xHH.
12
+ // eslint-disable-next-line no-control-regex
13
+ .replace(/[\x00-\x1f\x7f]/g, c => '%x' + c.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'))
14
+ // A quoted-string may carry ISO-8859-1 only; anything above U+00FF
15
+ // is replaced with '?' (use escape_content_disposition_utf8 for the RFC 5987 filename*).
16
+ // eslint-disable-next-line no-control-regex
17
+ .replace(/[^\x00-\xff]/g, '?');
12
18
  // A bare value must be a pure RFC 7230 token: any other character
13
19
  // (space, ';', ',', '"', '\', ...) requires the whole value to be a
14
20
  // quoted-string — an unquoted 'a\"b' or 'a,b' breaks header parsing.
@@ -1,13 +1,21 @@
1
1
  Escapes a value (typically a filename) for use in a `Content-Disposition`
2
2
  header. A pure RFC 7230 token (letters, digits, `!#$%&'*+.^_`|~-`) is returned
3
3
  as-is; anything else — spaces, `;`, `,`, quotes, etc. — becomes a quoted-string
4
- with `\` `"` `%` backslash-escaped and `\n`/`\0` encoded as `%x0A`/`%x00`.
5
- An unquoted `a"b` or `a,b` would break header parsing.
4
+ with `\` `"` `%` backslash-escaped, every control character (`\0`-`\x1f`,
5
+ `\x7f`, so `\n`, `\r`, `\x01`...) encoded as `%xHH`, and any character above
6
+ U+00FF (not representable in ISO-8859-1) replaced with `?`. The result is
7
+ always a valid header value. An unquoted `a"b` or `a,b` would break header
8
+ parsing.
9
+
10
+ See `escape_content_disposition_utf8` for the RFC 5987 `filename*` form that
11
+ carries a non-ASCII name alongside the `?`-degraded `filename` fallback.
6
12
 
7
13
  ```js
8
14
  escape_content_disposition('report.pdf') // 'report.pdf'
9
15
  escape_content_disposition('foo bar.txt') // '"foo bar.txt"'
10
16
  escape_content_disposition('foo"bar') // '"foo\\"bar"'
11
17
  escape_content_disposition('foo\nbar') // '"foo%x0Abar"'
18
+ escape_content_disposition('foo\rbar') // '"foo%x0Dbar"'
19
+ escape_content_disposition('файл.txt') // '"????.txt"'
12
20
  escape_content_disposition('') // '""'
13
21
  ```
@@ -0,0 +1,11 @@
1
+ /**
2
+ * RFC 5987 ext-value for a `filename*` parameter: `UTF-8''<percent-encoded>`.
3
+ * Everything outside attr-char is percent-encoded, as jshttp/content-disposition does.
4
+ */
5
+ function escape_content_disposition_utf8(s)
6
+ {
7
+ // eslint-disable-next-line no-control-regex
8
+ return `UTF-8''` + encodeURIComponent(s).replace(/[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'));
9
+ }
10
+
11
+ module.exports = escape_content_disposition_utf8;
@@ -0,0 +1,11 @@
1
+ Returns the RFC 5987 ext-value `UTF-8''<percent-encoded>` for a
2
+ `Content-Disposition` `filename*` parameter, so a non-ASCII filename can be
3
+ sent alongside the `?`-degraded `filename` fallback produced by
4
+ `escape_content_disposition`. Everything outside RFC 5987 attr-char is
5
+ percent-encoded, as jshttp/content-disposition does.
6
+
7
+ ```js
8
+ escape_content_disposition_utf8('файл.txt') // "UTF-8''%D1%84%D0%B0%D0%B9%D0%BB.txt"
9
+ escape_content_disposition_utf8('foo bar.txt') // "UTF-8''foo%20bar.txt"
10
+ // Content-Disposition: attachment; filename="????.txt"; filename*=UTF-8''%D1%84%D0%B0%D0%B9%D0%BB.txt
11
+ ```
package/src/fastdl.js CHANGED
@@ -54,6 +54,11 @@ async function download({file, read_stream_with_range, concurrency, user_friendl
54
54
  const rs0 = await read_stream_with_range(0, chunk_min_bytes);
55
55
 
56
56
  const total = rs0.content_range.total;
57
+ if (!Number.isInteger(total)) {
58
+ rs0.once('error', ignore);
59
+ rs0.destroy();
60
+ throw new UserFriendlyError('Server did not return the resource size (it may have ignored the Range header)');
61
+ }
57
62
  const chunk_size = Math.max(chunk_min_bytes, Math.min(chunk_max_bytes, Math.trunc(total/concurrency)));
58
63
  const progress = make_progress(total);
59
64
 
@@ -62,7 +67,7 @@ async function download({file, read_stream_with_range, concurrency, user_friendl
62
67
  let total_written = 0;
63
68
  let rs0_used = false;
64
69
 
65
- const timer = setInterval(tick, 1000);
70
+ let timer = null;
66
71
  function tick() {
67
72
  progress.refresh();
68
73
  user_friendly_status(`${format_progress_bytes(progress)} connections=${connections}`);
@@ -71,6 +76,7 @@ async function download({file, read_stream_with_range, concurrency, user_friendl
71
76
  user_friendly_status(`${format_bytes(total)} [${format_thousands(total)} bytes] to download`);
72
77
 
73
78
  try {
79
+ timer = setInterval(tick, 1000);
74
80
  await parallel({concurrency, spawn});
75
81
  }
76
82
  catch (error) {
package/src/fastdl.md CHANGED
@@ -8,6 +8,11 @@ flushed, closed, and atomically renamed over `file`. A failed request, exhausted
8
8
  retry, size mismatch, or write error removes the temporary file and preserves
9
9
  an existing destination.
10
10
 
11
+ The first range response must carry an integer `content_range.total`. When the
12
+ server ignores `Range` and answers with an unknown size (for example a chunked
13
+ 200), the stream is destroyed and the call rejects with a `UserFriendlyError`
14
+ without leaving a progress timer behind.
15
+
11
16
  ```js
12
17
  await fastdl({
13
18
  file: fs_path_basename(new URL(url).pathname),
@@ -1,3 +1,11 @@
1
+ // // 20260916_210411, local time
2
+ // function format_date_fs(date)
3
+ // {
4
+ // return [date.getFullYear(), date.getMonth() + 1, date.getDate(), '_',
5
+ // date.getHours(), date.getMinutes(), date.getSeconds()]
6
+ // .map(v => typeof v === 'string' || v > 9 ? v : `0${v}`).join('');
7
+ // }
8
+
1
9
  function format_date_fs(d)
2
10
  {
3
11
  const ymd = d.getFullYear() + dd(d.getMonth() + 1) + dd(d.getDate());
@@ -1,5 +1,38 @@
1
1
  const json_stringify_safe = require('./json_stringify_safe');
2
2
 
3
+ const MASK = '***';
4
+
5
+ // Header names (lower-cased) whose value is masked in the report
6
+ const SENSITIVE_HEADERS = new Set([
7
+ 'api-key',
8
+ 'authorization',
9
+ 'cookie',
10
+ 'proxy-authorization',
11
+ 'set-cookie',
12
+ 'x-api-key',
13
+ 'x-auth-token',
14
+ ]);
15
+
16
+ // A query parameter is sensitive when any of its name's segments (split on
17
+ // `_`, `-`, `.` and camelCase boundaries, lower-cased) is one of these words,
18
+ // so `api_key`, `apiKey`, `access_token`, `x-auth-token`, `sig` and `key`
19
+ // match while `keyword`, `design`, `author`, `keyboard`, `signal` do not
20
+ const SENSITIVE_QUERY_WORDS = new Set([
21
+ 'accesstoken',
22
+ 'apikey',
23
+ 'auth',
24
+ 'authorization',
25
+ 'key',
26
+ 'pass',
27
+ 'passwd',
28
+ 'password',
29
+ 'pwd',
30
+ 'secret',
31
+ 'sig',
32
+ 'signature',
33
+ 'token',
34
+ ]);
35
+
3
36
  function format_error_report(error)
4
37
  {
5
38
  if (!error) {
@@ -29,9 +62,9 @@ ${error.message ?? 'n/a'}
29
62
 
30
63
  --- REQUEST ---
31
64
 
32
- ${(error.response.config.method||'').toUpperCase()} ${error.response.config.url}
65
+ ${(error.response.config.method||'').toUpperCase()} ${mask_url(error.response.config.url)}
33
66
 
34
- ${JSON.stringify(error.response.config.headers||{}, null, 4).slice(1, -1).replace(/^\s+|,$/mg, '').trim()}
67
+ ${JSON.stringify(mask_headers(error.response.config.headers||{}), null, 4).slice(1, -1).replace(/^\s+|,$/mg, '').trim()}
35
68
 
36
69
  --- RESPONSE ---
37
70
 
@@ -52,9 +85,9 @@ ${error.message ?? 'n/a'}
52
85
 
53
86
  --- REQUEST ---
54
87
 
55
- ${(error.config.method||'').toUpperCase()} ${error.config.url}
88
+ ${(error.config.method||'').toUpperCase()} ${mask_url(error.config.url)}
56
89
 
57
- ${JSON.stringify(error.config.headers||{}, null, 4).slice(1, -1).replace(/^\s+|,$/mg, '').trim()}
90
+ ${JSON.stringify(mask_headers(error.config.headers||{}), null, 4).slice(1, -1).replace(/^\s+|,$/mg, '').trim()}
58
91
 
59
92
  --- STACK ---
60
93
 
@@ -62,4 +95,69 @@ ${error.stack ?? 'n/a'}
62
95
  `.trimStart();
63
96
  }
64
97
 
98
+ /**
99
+ * Returns a copy of `headers` with sensitive values replaced by [MASK].
100
+ * Header names are matched case-insensitively. Axios may nest headers per
101
+ * method (`headers.common`, `headers.get`, ...) and may pass an AxiosHeaders
102
+ * instance (which serializes via `toJSON`), so nested objects are walked too.
103
+ */
104
+ function mask_headers(headers)
105
+ {
106
+ if (!headers || typeof headers !== 'object') {
107
+ return headers;
108
+ }
109
+ if (typeof headers.toJSON === 'function') {
110
+ headers = headers.toJSON();
111
+ }
112
+ if (Array.isArray(headers)) {
113
+ return headers.map(mask_headers);
114
+ }
115
+ const out = {};
116
+ Object.entries(headers).forEach(function ([key, value]) {
117
+ if (SENSITIVE_HEADERS.has(String(key).toLowerCase())) {
118
+ out[key] = MASK;
119
+ }
120
+ else {
121
+ out[key] = mask_headers(value);
122
+ }
123
+ });
124
+ return out;
125
+ }
126
+
127
+ /**
128
+ * Replaces URL userinfo (`scheme://user:pass@host`) and the values of query
129
+ * parameters with a sensitive-looking name by [MASK]. Works on the raw
130
+ * string so the rest of the URL is reported exactly as it was requested.
131
+ */
132
+ function mask_url(url)
133
+ {
134
+ if (url === undefined || url === null) {
135
+ return url;
136
+ }
137
+ return String(url)
138
+ .replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/?#@]*@/i, `$1${MASK}@`)
139
+ .replace(/([?&])([^=&#]*)=[^&#]*/g, function (match, sep, name) {
140
+ return is_sensitive_query_name(name) ? `${sep}${name}=${MASK}` : match;
141
+ });
142
+ }
143
+
144
+ function is_sensitive_query_name(name)
145
+ {
146
+ try {
147
+ name = decodeURIComponent(name);
148
+ }
149
+ catch {
150
+ // keep the raw name
151
+ }
152
+ // whole names such as `apikey`, `accesstoken`, `api-key`
153
+ if (SENSITIVE_QUERY_WORDS.has(name.toLowerCase().replace(/[^a-z0-9]/g, ''))) {
154
+ return true;
155
+ }
156
+ return name
157
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
158
+ .toLowerCase()
159
+ .split(/[^a-z0-9]+/)
160
+ .some(segment => SENSITIVE_QUERY_WORDS.has(segment));
161
+ }
162
+
65
163
  module.exports = format_error_report;
@@ -10,3 +10,50 @@ errors get special treatment; everything else becomes indented JSON.
10
10
  split into an array of lines (circular-safe via `json_stringify_safe`).
11
11
  - Falsy error — `{"error":null}`-style JSON; `undefined` is reported as the
12
12
  string `'---undefined---'`.
13
+
14
+ **Masking:** credentials are replaced by `***` in the REQUEST
15
+ section so the report can be logged or sent to error tracking.
16
+
17
+ Header values are masked when the name, matched case-insensitively, is one
18
+ of:
19
+
20
+ - `api-key`
21
+ - `authorization`
22
+ - `cookie`
23
+ - `proxy-authorization`
24
+ - `set-cookie`
25
+ - `x-api-key`
26
+ - `x-auth-token`
27
+
28
+ Headers nested per method the Axios way (`headers.common`, `headers.get`,
29
+ ...) and `AxiosHeaders` instances (via `toJSON`) are handled; the error
30
+ object itself is not modified.
31
+
32
+ URL userinfo is masked (`https://user:pass@host` becomes
33
+ `https://***@host`), and so is the value of every query parameter with a
34
+ secret-looking name (`?token=abc&page=2` becomes `?token=***&page=2`). A
35
+ name is secret-looking when, split on `_`/`-`/`.` and camelCase boundaries
36
+ and lower-cased, any segment is one of the words below, or when the whole
37
+ name with separators removed is one of them:
38
+
39
+ - `accesstoken`
40
+ - `apikey`
41
+ - `auth`
42
+ - `authorization`
43
+ - `key`
44
+ - `pass`
45
+ - `passwd`
46
+ - `password`
47
+ - `pwd`
48
+ - `secret`
49
+ - `sig`
50
+ - `signature`
51
+ - `token`
52
+
53
+ So `api_key`, `apiKey`, `access_token`, `x-auth-token`, `client_secret`,
54
+ `sig` and `key` are masked while `keyword`, `design`, `author`,
55
+ `keyboard`, `signal` and `client_id` are not. The rest of the URL is
56
+ reported verbatim.
57
+
58
+ The response body is **not** sanitized — only truncated to 10240
59
+ characters — and the request body is never included.
@@ -46,7 +46,11 @@ async function http_get_stream_range(url, first, last, options)
46
46
  else if (b !== '') {
47
47
  const actual_length = out.content_range.last - out.content_range.first + 1;
48
48
  const expected_length = out.content_range.total === null ? Math.min(actual_length, b) : Math.min(out.content_range.total, b);
49
- if (actual_length !== expected_length) {
49
+ if (out.content_range.last === null) {
50
+ // Chunked 200 with no Content-Range: the server ignored the suffix range
51
+ out.destroy(new Error(`Server ignored the requested suffix range of ${fmt(b)} bytes and returned a response of unknown length`));
52
+ }
53
+ else if (actual_length !== expected_length) {
50
54
  out.destroy(new Error(`Length of a returned range (${fmt(actual_length)}) is not as expected: [${fmt(expected_length)}]`));
51
55
  }
52
56
  else if (out.content_range.total !== null && out.content_range.last !== out.content_range.total - 1) {
@@ -14,7 +14,8 @@ chunked 200 response (neither header) is accepted as
14
14
  The returned range is validated according to the requested form. Closed ranges
15
15
  accept a `last` clamped to the end of the resource; open-ended ranges must end
16
16
  at the resource end when its size is known; suffix ranges validate their
17
- length and resource end. A server that ignores a range and replies 200 is
17
+ length and resource end, and a chunked 200 (unknown length) in reply to a
18
+ suffix request is rejected. A server that ignores a range and replies 200 is
18
19
  rejected.
19
20
 
20
21
  ```js
@@ -1,4 +1,5 @@
1
1
  const escape_content_disposition = require('./escape_content_disposition');
2
+ const escape_content_disposition_utf8 = require('./escape_content_disposition_utf8');
2
3
  const fs_fclose = require('./fs_fclose');
3
4
  const fs_fopen = require('./fs_fopen');
4
5
  const fs_fread = require('./fs_fread');
@@ -30,7 +31,7 @@ async function http_stream_range(req, res, file)
30
31
  res.header('Accept-Ranges', 'bytes');
31
32
  res.header('Content-Type', mime);
32
33
  res.header('Content-Length', total);
33
- res.header('Content-Disposition', `inline; filename=${escape_content_disposition(fs_path_basename(file))};`);
34
+ res.header('Content-Disposition', content_disposition(fs_path_basename(file)));
34
35
  res.status(200);
35
36
  res.end();
36
37
  return;
@@ -122,4 +123,16 @@ async function http_stream_range(req, res, file)
122
123
  }
123
124
  }
124
125
 
126
+ function content_disposition(name)
127
+ {
128
+ // The quoted `filename` degrades anything outside ISO-8859-1 to '?', so a
129
+ // non-ASCII name also gets the RFC 5987 `filename*` that browsers prefer.
130
+ let out = `inline; filename=${escape_content_disposition(name)}`;
131
+ // eslint-disable-next-line no-control-regex
132
+ if (/[^\x00-\x7f]/.test(name)) {
133
+ out += `; filename*=${escape_content_disposition_utf8(name)}`;
134
+ }
135
+ return out + ';';
136
+ }
137
+
125
138
  module.exports = http_stream_range;
@@ -11,7 +11,9 @@ http_stream_range(req, res, file) → Promise
11
11
 
12
12
  - `HEAD` → 200 with `Content-Type`, `Content-Length`, and an
13
13
  `inline; filename=...` `Content-Disposition` (escaped via
14
- `escape_content_disposition`); no body.
14
+ `escape_content_disposition`, so control characters are encoded and a
15
+ non-ASCII basename is also sent as an RFC 5987 `filename*=UTF-8''...`
16
+ parameter); no body.
15
17
  - `GET` with `Range` → 206 with `Content-Range: bytes first-last/total` and a
16
18
  matching `Content-Length`.
17
19
  - `GET` without `Range` → 200 with the whole file.
@@ -21,10 +21,16 @@ function make_progress(total)
21
21
  progress: null,
22
22
  percents: null,
23
23
  add: function (delta = 0) {
24
- const now_minus_10sec = Date.now() - 10000;
25
- history.splice(0, history.findLastIndex(v => v.time < now_minus_10sec) + 1);
26
- history.push({time: Date.now(), delta});
24
+ const now = Date.now();
25
+ // The oldest sample is the anchor the rate is measured from, so
26
+ // the newest sample older than 10s is kept: the window then
27
+ // stays close to 10s instead of shrinking after each eviction
28
+ const expired = history.findLastIndex(v => v.time < now - 10000);
29
+ if (expired > 0) {
30
+ history.splice(0, expired);
31
+ }
27
32
  out.done += delta;
33
+ history.push({time: now, done: out.done});
28
34
  out.refresh();
29
35
  },
30
36
  update: function (done) {
@@ -32,16 +38,17 @@ function make_progress(total)
32
38
  },
33
39
  refresh: function () {
34
40
  out.duration = (Date.now() - time0)/1000;
35
- // `rate` and `eta` are null while unknown (same as before the
36
- // first add): a zero time window would give rate = Infinity,
37
- // and a zero rate would give eta = Infinity.
38
- if (history.length) {
39
- const delta = history.reduce((a,v) => a + v.delta, 0);
41
+ // `rate` and `eta` are null while unknown: a single sample says
42
+ // nothing about how long its delta took to accumulate (counting
43
+ // it doubled the rate early on), a zero time window would give
44
+ // rate = Infinity, and a zero rate would give eta = Infinity.
45
+ if (history.length > 1) {
46
+ const delta = out.done - history[0].done;
40
47
  const time_sec = (Date.now() - history[0].time)/1000;
41
48
  out.rate = time_sec ? delta/time_sec : null;
42
49
  }
43
50
  else {
44
- out.rate = out.duration ? out.done/out.duration : null;
51
+ out.rate = null;
45
52
  }
46
53
  out.percents = !out.total ? null : out.done/out.total;
47
54
  out.eta = (!out.total || !out.rate) ? null : (out.total - out.done)/out.rate;
@@ -3,10 +3,13 @@ rows created). Pass the snapshot to `format_progress_bytes` /
3
3
  `format_progress_kilo` for display.
4
4
 
5
5
  **Fields:** `done`, `total`, `duration` (seconds since creation), `rate`
6
- (units/second over a sliding 10-second history window), `eta` (seconds),
7
- `percents` (a fraction, `done/total`). `rate` and `eta` are `null` while
8
- unknown — before the first `add`, or when they would be `Infinity` (zero
9
- time window, zero rate); `percents` is `null` when `total` is falsy.
6
+ (units/second over a sliding ~10-second history window), `eta` (seconds),
7
+ `percents` (a fraction, `done/total`). The rate is measured between the
8
+ oldest sample in the window and now, so the first sample's own delta is
9
+ not counted (how long it took to accumulate is unknown). `rate` and `eta`
10
+ are `null` while unknown — until two `add` calls exist, or when they would
11
+ be `Infinity` (zero time window, zero rate); `percents` is `null` when
12
+ `total` is falsy.
10
13
 
11
14
  **Methods:**
12
15
  - `add(delta)` — add `delta` to `done` and recompute
@@ -2,6 +2,11 @@
2
2
  // keep in sync with pid_exists
3
3
  function pgid_exists(pgid)
4
4
  {
5
+ // kill(-0, ...) targets the caller's own group and kill(-1, ...) every
6
+ // process the user may signal: never let such ids reach the kernel
7
+ if (!Number.isInteger(pgid) || pgid <= 1) {
8
+ throw new TypeError(`pgid_exists: invalid pgid: ${pgid}`);
9
+ }
5
10
  try {
6
11
  process.kill(-pgid, 0);
7
12
  return true;
@@ -3,4 +3,8 @@ Checks whether a process group is alive by sending signal `0` to the group via
3
3
  at least one member, `false` on any error (no such group, or no permission).
4
4
  A group id equals the pid of the group leader.
5
5
 
6
+ Throws `TypeError` when `pgid` is not an integer greater than `1`: `0` would
7
+ probe the caller's own process group and `1` every process the user may
8
+ signal (`kill(-1, ...)`), so such values never reach `kill(2)`.
9
+
6
10
  See `pid_exists` for the single-process variant.
@@ -7,6 +7,11 @@ const pgid_exists = require('./pgid_exists');
7
7
  // keep in sync with pid_kill_grace
8
8
  async function pgid_kill_grace(pgid, {grace_timeout_ms = 5000, log = ignore} = {})
9
9
  {
10
+ // kill(-0, ...) targets the caller's own group and kill(-1, ...) every
11
+ // process the user may signal: never let such ids reach the kernel
12
+ if (!Number.isInteger(pgid) || pgid <= 1) {
13
+ throw new TypeError(`pgid_kill_grace: invalid pgid: ${pgid}`);
14
+ }
10
15
  log(`Terminating process group ${pgid}: sending SIGTERM`);
11
16
  try {
12
17
  process.kill(-pgid, 'SIGTERM');
@@ -5,6 +5,9 @@ to deliver it. A group id equals the pid of the group leader.
5
5
 
6
6
  - Options: `grace_timeout_ms` (default `5000`) and `log` (progress callback,
7
7
  default no-op).
8
+ - Throws `TypeError` before sending anything when `pgid` is not an integer
9
+ greater than `1` (`0` would signal the caller's own group, `1` every
10
+ process the user may signal via `kill(-1, ...)`).
8
11
  - Throws `Failed to send SIGTERM to process group <pgid>: ...` when the
9
12
  initial signal cannot be sent.
10
13
  - A group that dies on its own during the grace period skips the `SIGKILL`;
package/src/pid_exists.js CHANGED
@@ -1,6 +1,11 @@
1
1
  // keep in sync with pgid_exists
2
2
  function pid_exists(pid)
3
3
  {
4
+ // kill(0, ...) targets the caller's own group and kill(-1, ...) every
5
+ // process the user may signal: never let such ids reach the kernel
6
+ if (!Number.isInteger(pid) || pid <= 1) {
7
+ throw new TypeError(`pid_exists: invalid pid: ${pid}`);
8
+ }
4
9
  try {
5
10
  process.kill(pid, 0);
6
11
  return true;
package/src/pid_exists.md CHANGED
@@ -2,4 +2,8 @@ Checks whether a process is alive by sending signal `0` via
2
2
  `process.kill(pid, 0)`. Returns `true` when the signal can be delivered,
3
3
  `false` on any error (no such process, or no permission to signal it).
4
4
 
5
+ Throws `TypeError` when `pid` is not an integer greater than `1`: `0` would
6
+ probe the caller's own process group and `-1` every process the user may
7
+ signal, so such values never reach `kill(2)`.
8
+
5
9
  See `pgid_exists` for the process-group variant.
@@ -6,6 +6,11 @@ const pid_exists = require('./pid_exists');
6
6
  // keep in sync with pgid_kill_grace
7
7
  async function pid_kill_grace(pid, {grace_timeout_ms = 5000, log = ignore} = {})
8
8
  {
9
+ // kill(0, ...) targets the caller's own group and kill(-1, ...) every
10
+ // process the user may signal: never let such ids reach the kernel
11
+ if (!Number.isInteger(pid) || pid <= 1) {
12
+ throw new TypeError(`pid_kill_grace: invalid pid: ${pid}`);
13
+ }
9
14
  log(`Terminating process ${pid}: sending SIGTERM`);
10
15
  try {
11
16
  process.kill(pid, 'SIGTERM');
@@ -4,6 +4,9 @@ waits up to 200ms more for the kernel to deliver it.
4
4
 
5
5
  - Options: `grace_timeout_ms` (default `5000`) and `log` (progress callback,
6
6
  default no-op).
7
+ - Throws `TypeError` before sending anything when `pid` is not an integer
8
+ greater than `1` (`0` would signal the caller's own group, `-1` every
9
+ process the user may signal).
7
10
  - Throws `Failed to send SIGTERM to process <pid>: ...` when the initial
8
11
  signal cannot be sent (e.g. the process does not exist).
9
12
  - A process that dies on its own during the grace period skips the `SIGKILL`
package/src/plural.js CHANGED
@@ -3,7 +3,7 @@ function plural(n, singular, plural, zero)
3
3
  if (n === 0 && typeof zero === 'string') {
4
4
  return zero;
5
5
  }
6
- if (n % 10 === 1 && n % 100 !== 11) {
6
+ if (n === 1) {
7
7
  return singular.split('#').join(n);
8
8
  }
9
9
  return plural.split('#').join(n);
package/src/plural.md CHANGED
@@ -1,13 +1,16 @@
1
- Slavic-style pluralizer: `plural(n, singular, plural, zero)`. The `singular`
2
- form is used when `n` ends in 1 but not in 11 (1, 21, 101, ...); everything
3
- else gets the `plural` form. Every `#` in the chosen form is replaced with
4
- `n`. The optional `zero` string is returned as-is when `n === 0`.
1
+ English pluralizer: `plural(n, singular, plural, zero)`. The `singular` form is
2
+ used only when `n === 1`; every other `n` (0, 2, 11, 21, 101, ...) gets the
3
+ `plural` form. Every `#` in the chosen form is replaced with `n`. The optional
4
+ `zero` string is returned as-is when `n === 0`. A form without `#` comes back
5
+ as it is, so the helper also picks a case: `switch (plural(n, '1', '2', '0'))`.
6
+ Russian words have three forms: see `plural_ru`.
5
7
 
6
8
  ```js
7
- plural(1, '# apple', '# apples') // '1 apple'
8
- plural(2, '# apple', '# apples') // '2 apples'
9
- plural(11, '# apple', '# apples') // '11 apples'
10
- plural(21, '# apple', '# apples') // '21 apple'
11
- plural(0, '# apple', '# apples') // '0 apples'
12
- plural(0, '# apple', '# apples', 'No apples') // 'No apples'
9
+ plural(1, '# step', '# steps') // '1 step'
10
+ plural(2, '# step', '# steps') // '2 steps'
11
+ plural(11, '# step', '# steps') // '11 steps'
12
+ plural(21, '# step', '# steps') // '21 steps'
13
+ plural(0, '# step', '# steps') // '0 steps'
14
+ plural(0, '# step', '# steps', 'no steps') // 'no steps'
15
+ plural(21, '1', '2', '0') // '2'
13
16
  ```
@@ -0,0 +1,27 @@
1
+ function plural_ru(n, one, few, many, zero)
2
+ {
3
+ if (n === 0 && typeof zero === 'string') {
4
+ return zero;
5
+ }
6
+ return plural_ru_form(n, one, few, many).split('#').join(n);
7
+ }
8
+
9
+ // Russian counts in three forms: one (1, 21, 101), few (2-4, 22-24, 102-104)
10
+ // and many (0, 5-20, 25-30, 111-114). A fraction takes the few form: 1.5 яблока.
11
+ function plural_ru_form(n, one, few, many)
12
+ {
13
+ if (!Number.isInteger(n)) {
14
+ return few;
15
+ }
16
+ const mod10 = Math.abs(n) % 10;
17
+ const mod100 = Math.abs(n) % 100;
18
+ if (mod10 === 1 && mod100 !== 11) {
19
+ return one;
20
+ }
21
+ if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) {
22
+ return few;
23
+ }
24
+ return many;
25
+ }
26
+
27
+ module.exports = plural_ru;
@@ -0,0 +1,21 @@
1
+ Russian pluralizer: `plural_ru(n, one, few, many, zero)`. A counted word in
2
+ Russian has three forms, and `n` picks one: `one` when it ends in 1 but not in
3
+ 11 (1, 21, 101), `few` when it ends in 2, 3 or 4 but not in 12, 13 or 14 (2, 23,
4
+ 104), `many` otherwise (0, 5, 11, 19, 100). A fraction takes the `few` form
5
+ (1.5 яблока). Every `#` in the chosen form is replaced with `n`. The optional
6
+ `zero` string is returned as-is when `n === 0`. A form without `#` comes back
7
+ as it is, so the helper also picks a case: `switch (plural_ru(n, '1', '2', '5', '0'))`.
8
+ English words have two forms: see `plural`.
9
+
10
+ ```js
11
+ plural_ru(1, '# яблоко', '# яблока', '# яблок') // '1 яблоко'
12
+ plural_ru(2, '# яблоко', '# яблока', '# яблок') // '2 яблока'
13
+ plural_ru(5, '# яблоко', '# яблока', '# яблок') // '5 яблок'
14
+ plural_ru(11, '# яблоко', '# яблока', '# яблок') // '11 яблок'
15
+ plural_ru(21, '# яблоко', '# яблока', '# яблок') // '21 яблоко'
16
+ plural_ru(22, '# яблоко', '# яблока', '# яблок') // '22 яблока'
17
+ plural_ru(1.5, '# яблоко', '# яблока', '# яблок') // '1.5 яблока'
18
+ plural_ru(0, '# яблоко', '# яблока', '# яблок') // '0 яблок'
19
+ plural_ru(0, '# яблоко', '# яблока', '# яблок', 'Нет яблок') // 'Нет яблок'
20
+ plural_ru(22, '1', '2', '5', '0') // '2'
21
+ ```
@@ -64,6 +64,10 @@ async function redis_poll_zshift_callback_rpush(options)
64
64
  log(`[${log_waiter_error_parse}] ${error.message}`);
65
65
  continue;
66
66
  }
67
+ if (!message || typeof message !== 'object') {
68
+ log(`[${log_waiter_error_parse}] message is not an object; skipped`);
69
+ continue;
70
+ }
67
71
 
68
72
  try {
69
73
  const uid = message.uid;
@@ -5,9 +5,10 @@ type, value}` to `redis_output_queue` — `type: 'resolve'` with the callback's
5
5
  return value, or `'reject'` with the error stack/message. An empty poll
6
6
  sleeps a random 500–1500ms; the loop runs at most 86400 iterations.
7
7
 
8
- - Unparseable messages and messages without `uid` are skipped; a missing or
9
- past `expires_at` rejects the message (payloads carry signed urls with
10
- limited lifetime).
8
+ - Unparseable messages, valid JSON that is not an object (`null`, numbers,
9
+ strings), and messages without `uid` are skipped; a missing or past
10
+ `expires_at` rejects the message (payloads carry signed urls with limited
11
+ lifetime).
11
12
  - `user_friendly_status(value)` rpushes `{uid, version, type:
12
13
  'user_friendly_status', value}` to the output queue; a generic
13
14
  `'Started...'` status is always pushed first so the consumer starts
@@ -73,6 +73,10 @@ async function redis_poll_zshift_spawn_rpush(options)
73
73
  log(`[${log_waiter_error_parse}] ${error.message}`);
74
74
  continue;
75
75
  }
76
+ if (!message || typeof message !== 'object') {
77
+ log(`[${log_waiter_error_parse}] message is not an object; skipped`);
78
+ continue;
79
+ }
76
80
 
77
81
  try {
78
82
  const uid = message.uid;
@@ -12,9 +12,10 @@ iterations.
12
12
  `'Started...'` status is always pushed first. stdout/stderr are logged
13
13
  line by line under the `log_worker_stdout`/`log_worker_stderr` tags.
14
14
  - Only exit code 0 is a success — a signal-killed child (code `null`) fails.
15
- - Unparseable messages and messages without `uid` are skipped; a missing or
16
- past `expires_at` fails the message (payloads carry signed urls with
17
- limited lifetime).
15
+ - Unparseable messages, valid JSON that is not an object (`null`, numbers,
16
+ strings), and messages without `uid` are skipped; a missing or past
17
+ `expires_at` fails the message (payloads carry signed urls with limited
18
+ lifetime).
18
19
  - `options.log` must provide `spawn()`; queue access goes through
19
20
  `redis_zshift`/`redis_rpush`, so any redis client from redis@1 to redis@6
20
21
  works. See `redis_poll_zshift_callback_rpush` for the in-process
package/src/shell.js CHANGED
@@ -7,7 +7,7 @@ function shell(args, options)
7
7
  if (error) {
8
8
  reject(error);
9
9
  }
10
- else if (stderr) {
10
+ else if (stderr.length) {
11
11
  reject(new Error(`Process terminated with the following STDERR:\n\n${stderr}`));
12
12
  }
13
13
  else {
package/src/shell.md CHANGED
@@ -2,7 +2,8 @@ Basic helper for running external commands via `execFile`.
2
2
 
3
3
  - Fails if the command exits with non-zero code.
4
4
  - Fails if anything is printed to `stderr`.
5
- - Resolves with `stdout` (string), exactly as produced.
5
+ - Resolves with `stdout` (string), exactly as produced; a `Buffer` with
6
+ `{encoding: 'buffer'}`.
6
7
 
7
8
  ## Signature
8
9
 
package/src/shell_json.js CHANGED
@@ -7,7 +7,7 @@ function shell_json(args, options)
7
7
  if (error) {
8
8
  reject(error);
9
9
  }
10
- else if (stderr) {
10
+ else if (stderr.length) {
11
11
  reject(new Error(`Process terminated with the following STDERR:\n\n${stderr}`));
12
12
  }
13
13
  else {
package/src/shell_json.md CHANGED
@@ -3,7 +3,8 @@ Basic helper for running external commands via `execFile`, and parse result by J
3
3
  - Fails if the command exits with non-zero code.
4
4
  - Fails if anything is printed to `stderr`.
5
5
  - Fails if stdout is not valid JSON.
6
- - Resolves with parsed JSON value.
6
+ - Resolves with parsed JSON value (`{encoding: 'buffer'}` is accepted;
7
+ `JSON.parse` stringifies the `Buffer`).
7
8
 
8
9
  ## Signature
9
10
 
@@ -1,4 +1,5 @@
1
1
  const stream = require('stream');
2
+ const {StringDecoder} = require('string_decoder');
2
3
 
3
4
  /**
4
5
  * Split the input stream by consecutive delimiters in `chars`
@@ -6,10 +7,13 @@ const stream = require('stream');
6
7
  function stream_strpbrk(chars = '\r\n')
7
8
  {
8
9
  const pending = [];
10
+ // StringDecoder buffers a multi-byte character split across chunk
11
+ // boundaries; plain buf.toString() would corrupt it into �.
12
+ const decoder = new StringDecoder('utf8');
9
13
  return new stream.Transform({
10
14
  objectMode: true,
11
15
  transform: function (str, encoding, callback) {
12
- str = Buffer.isBuffer(str) ? str.toString() : str;
16
+ str = Buffer.isBuffer(str) ? decoder.write(str) : str;
13
17
 
14
18
  let off = 0;
15
19
  // leading delimiters
@@ -67,6 +71,10 @@ function stream_strpbrk(chars = '\r\n')
67
71
  callback();
68
72
  },
69
73
  flush: function (callback) {
74
+ const tail = decoder.end();
75
+ if (tail) {
76
+ pending.push(tail);
77
+ }
70
78
  if (pending.length) {
71
79
  this.push(''.concat(...pending.splice(0)));
72
80
  }
@@ -17,7 +17,12 @@ function stream_xml_analyze()
17
17
  onopentag: function (name, attrs) {
18
18
  path.push(name);
19
19
  path_str = path.join(' > ');
20
- analytics[path_str] ??= 0;
20
+ // Element names come from the outside world: `??=` on a plain
21
+ // object would see Object.prototype.constructor and produce NaN
22
+ // for a root element named `constructor`.
23
+ if (!Object.hasOwn(analytics, path_str)) {
24
+ Object.defineProperty(analytics, path_str, {value: 0, enumerable: true, writable: true, configurable: true});
25
+ }
21
26
  analytics[path_str]++;
22
27
  },
23
28
  onclosetag: function (name) {
@@ -99,22 +99,30 @@ function guess(node)
99
99
 
100
100
  node.children.forEach(function (child) {
101
101
  const tmp = stream_xml_parse.guess(child);
102
- if (child.name in out) {
102
+ // Element names come from the outside world: `in` and plain
103
+ // assignment would let `__proto__`, `constructor`, `toString`...
104
+ // hit Object.prototype instead of becoming own data properties.
105
+ if (Object.hasOwn(out, child.name)) {
103
106
  if (Array.isArray(out[child.name])) {
104
107
  out[child.name].push(tmp);
105
108
  }
106
109
  else {
107
- out[child.name] = [out[child.name], tmp];
110
+ define(out, child.name, [out[child.name], tmp]);
108
111
  }
109
112
  }
110
113
  else {
111
- out[child.name] = tmp;
114
+ define(out, child.name, tmp);
112
115
  }
113
116
  });
114
117
 
115
118
  return out;
116
119
  }
117
120
 
121
+ function define(obj, name, value)
122
+ {
123
+ Object.defineProperty(obj, name, {value, enumerable: true, writable: true, configurable: true});
124
+ }
125
+
118
126
  function ashtml(node)
119
127
  {
120
128
  return node.raw.map(fn).filter(v => v).join('').trim();
package/src/urlparts.js CHANGED
@@ -43,7 +43,10 @@ function urlparts(input)
43
43
  }
44
44
 
45
45
  if (resource.startsWith('//')) {
46
- const slash_pos = resource.indexOf('/', 2);
46
+ // WHATWG parsers treat `\` as `/` for special schemes, so the
47
+ // authority ends at the first of either.
48
+ const slash_offset = resource.slice(2).search(/[/\\]/);
49
+ const slash_pos = slash_offset === -1 ? -1 : slash_offset + 2;
47
50
  const authority = slash_pos === -1
48
51
  ? resource.slice(2)
49
52
  : resource.slice(2, slash_pos);
package/src/urlparts.md CHANGED
@@ -5,6 +5,12 @@ delimiters, while `query` and `fragment` are always parsed plain objects.
5
5
  Malformed URL text is accepted and parsed on a best-effort basis. A non-string
6
6
  argument returns the empty stable shape instead of throwing.
7
7
 
8
+ **Warning:** `host` (like every other component) is a tolerant best-effort
9
+ value intended for display and lenient parsing. It is not guaranteed to match
10
+ the host a WHATWG client (browsers, Node's `URL`, Axios) would connect to for
11
+ malformed input, so it must not be used for allow-lists, redirect-target
12
+ validation, or SSRF decisions. Use `new URL(input)` for security checks.
13
+
8
14
  ```js
9
15
  urlparts('/users?a=1#top')
10
16
  // {