@doppelgangerdev/doppelganger 0.4.0 → 0.4.3

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 (33) hide show
  1. package/README.md +1 -1
  2. package/agent.js +49 -40
  3. package/dist/assets/index-D68YZVOp.js +19 -0
  4. package/dist/assets/index-WbwoTnJa.css +1 -0
  5. package/dist/index.html +3 -3
  6. package/headful.js +23 -23
  7. package/package.json +1 -1
  8. package/proxy-rotation.js +245 -230
  9. package/scrape.js +25 -25
  10. package/server.js +343 -296
  11. package/dist/assets/index-B7ntJ0pF.js +0 -18
  12. package/dist/assets/index-OOTrc7_f.css +0 -1
  13. package/dist/screenshots/agent_1768764625684.png +0 -0
  14. package/dist/screenshots/agent_1768765565909.png +0 -0
  15. package/dist/screenshots/agent_1768765581979.png +0 -0
  16. package/dist/screenshots/scrape_1768764416615.png +0 -0
  17. package/dist/screenshots/scrape_1768764452033.png +0 -0
  18. package/dist/screenshots/scrape_1768765064688.png +0 -0
  19. package/dist/screenshots/scrape_1768765090587.png +0 -0
  20. package/dist/screenshots/scrape_1768765100798.png +0 -0
  21. package/dist/screenshots/scrape_1768765114782.png +0 -0
  22. package/dist/screenshots/scrape_1768765127463.png +0 -0
  23. package/public/screenshots/agent_1768764625684.png +0 -0
  24. package/public/screenshots/agent_1768765565909.png +0 -0
  25. package/public/screenshots/agent_1768765581979.png +0 -0
  26. package/public/screenshots/scrape_1768764416615.png +0 -0
  27. package/public/screenshots/scrape_1768764452033.png +0 -0
  28. package/public/screenshots/scrape_1768765064688.png +0 -0
  29. package/public/screenshots/scrape_1768765090587.png +0 -0
  30. package/public/screenshots/scrape_1768765100798.png +0 -0
  31. package/public/screenshots/scrape_1768765114782.png +0 -0
  32. package/public/screenshots/scrape_1768765127463.png +0 -0
  33. package/public/screenshots/scrape_1768769463201.png +0 -0
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media(min-width:640px){.container{max-width:640px}}@media(min-width:768px){.container{max-width:768px}}@media(min-width:1024px){.container{max-width:1024px}}@media(min-width:1280px){.container{max-width:1280px}}@media(min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.\!visible{visibility:visible!important}.visible{visibility:visible}.collapse{visibility:collapse}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.bottom-6{bottom:1.5rem}.right-4{right:1rem}.right-5{right:1.25rem}.right-6{right:1.5rem}.top-4{top:1rem}.top-5{top:1.25rem}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.z-\[100\]{z-index:100}.z-\[190\]{z-index:190}.z-\[201\]{z-index:201}.z-\[210\]{z-index:210}.z-\[220\]{z-index:220}.z-\[80\]{z-index:80}.order-1{order:1}.mx-auto{margin-left:auto;margin-right:auto}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.list-item{display:list-item}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2\.5{height:.625rem}.h-20{height:5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-\[400px\]{height:400px}.h-\[80vh\]{height:80vh}.h-full{height:100%}.max-h-\[320px\]{max-height:320px}.max-h-\[70vh\]{max-height:70vh}.max-h-\[90vh\]{max-height:90vh}.min-h-0{min-height:0px}.min-h-\[1\.5rem\]{min-height:1.5rem}.min-h-\[120px\]{min-height:120px}.min-h-\[200px\]{min-height:200px}.min-h-\[320px\]{min-height:320px}.min-h-\[400px\]{min-height:400px}.min-h-\[420px\]{min-height:420px}.min-h-\[44px\]{min-height:44px}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-\[200px\]{width:200px}.w-\[400px\]{width:400px}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.table-auto{table-layout:auto}.-translate-y-0\.5{--tw-translate-y: -.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-\[1\.02\]{--tw-scale-x: 1.02;--tw-scale-y: 1.02;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-col-resize{cursor:col-resize}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-12>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(3rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(3rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-\[28px\]{border-radius:28px}.rounded-\[32px\]{border-radius:32px}.rounded-\[40px\]{border-radius:40px}.rounded-\[48px\]{border-radius:48px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-black\/20{border-color:#0003}.border-blue-500\/20{border-color:#3b82f633}.border-gray-500\/40{border-color:#6b728066}.border-green-400\/20{border-color:#4ade8033}.border-green-400\/60{border-color:#4ade8099}.border-green-500\/20{border-color:#22c55e33}.border-red-400\/70{border-color:#f87171b3}.border-red-500\/10{border-color:#ef44441a}.border-red-500\/20{border-color:#ef444433}.border-white{--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity, 1))}.border-white\/10{border-color:#ffffff1a}.border-white\/20{border-color:#fff3}.border-white\/5{border-color:#ffffff0d}.border-yellow-400\/60{border-color:#facc1599}.border-yellow-500\/10{border-color:#eab3081a}.border-t-black{--tw-border-opacity: 1;border-top-color:rgb(0 0 0 / var(--tw-border-opacity, 1))}.border-t-white{--tw-border-opacity: 1;border-top-color:rgb(255 255 255 / var(--tw-border-opacity, 1))}.bg-\[\#020202\]{--tw-bg-opacity: 1;background-color:rgb(2 2 2 / var(--tw-bg-opacity, 1))}.bg-\[\#050505\]{--tw-bg-opacity: 1;background-color:rgb(5 5 5 / var(--tw-bg-opacity, 1))}.bg-\[\#0b0b0b\]{--tw-bg-opacity: 1;background-color:rgb(11 11 11 / var(--tw-bg-opacity, 1))}.bg-amber-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-black\/40{background-color:#0006}.bg-black\/70{background-color:#000000b3}.bg-blue-500\/10{background-color:#3b82f61a}.bg-blue-500\/5{background-color:#3b82f60d}.bg-emerald-400{--tw-bg-opacity: 1;background-color:rgb(52 211 153 / var(--tw-bg-opacity, 1))}.bg-green-500\/10{background-color:#22c55e1a}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity, 1))}.bg-red-500\/5{background-color:#ef44440d}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-white\/10{background-color:#ffffff1a}.bg-white\/5{background-color:#ffffff0d}.bg-white\/\[0\.02\]{background-color:#ffffff05}.bg-white\/\[0\.03\]{background-color:#ffffff08}.bg-white\/\[0\.05\]{background-color:#ffffff0d}.bg-yellow-500\/5{background-color:#eab3080d}.fill-black{fill:#000}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-10{padding:2.5rem}.p-12{padding:3rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-3\.5{padding:.875rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-12{padding-left:3rem;padding-right:3rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-10{padding-bottom:2.5rem}.pb-12{padding-bottom:3rem}.pb-2{padding-bottom:.5rem}.pb-4{padding-bottom:1rem}.pl-1{padding-left:.25rem}.pr-2{padding-right:.5rem}.pr-4{padding-right:1rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.align-top{vertical-align:top}.font-mono{font-family:JetBrains Mono,monospace}.font-sans{font-family:Inter,sans-serif}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[14px\]{font-size:14px}.text-\[7px\]{font-size:7px}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-loose{line-height:2}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.tracking-\[0\.25em\]{letter-spacing:.25em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-\[0\.35em\]{letter-spacing:.35em}.tracking-\[0\.3em\]{letter-spacing:.3em}.tracking-\[0\.4em\]{letter-spacing:.4em}.tracking-tight{letter-spacing:-.025em}.tracking-tighter{letter-spacing:-.05em}.tracking-widest{letter-spacing:.1em}.text-amber-200{--tw-text-opacity: 1;color:rgb(253 230 138 / var(--tw-text-opacity, 1))}.text-amber-300{--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-blue-200\/80{color:#bfdbfecc}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-300\/60{color:#93c5fd99}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-cyan-300{--tw-text-opacity: 1;color:rgb(103 232 249 / var(--tw-text-opacity, 1))}.text-emerald-200{--tw-text-opacity: 1;color:rgb(167 243 208 / var(--tw-text-opacity, 1))}.text-emerald-300{--tw-text-opacity: 1;color:rgb(110 231 183 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-purple-300{--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.text-purple-500{--tw-text-opacity: 1;color:rgb(168 85 247 / var(--tw-text-opacity, 1))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-slate-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-white\/10{color:#ffffff1a}.text-white\/20{color:#fff3}.text-white\/40{color:#fff6}.text-white\/5{color:#ffffff0d}.text-white\/50{color:#ffffff80}.text-white\/60{color:#fff9}.text-white\/70{color:#ffffffb3}.text-white\/80{color:#fffc}.text-yellow-300{--tw-text-opacity: 1;color:rgb(253 224 71 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.accent-white{accent-color:#fff}.opacity-0{opacity:0}.opacity-20{opacity:.2}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-85{opacity:.85}.opacity-\[0\.02\]{opacity:.02}.opacity-\[0\.03\]{opacity:.03}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_0_8px_rgba\(251\,191\,36\,0\.6\)\]{--tw-shadow: 0 0 8px rgba(251,191,36,.6);--tw-shadow-colored: 0 0 8px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_30px_80px_rgba\(0\,0\,0\,0\.45\)\]{--tw-shadow: 0 30px 80px rgba(0,0,0,.45);--tw-shadow-colored: 0 30px 80px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-white\/10{--tw-shadow-color: rgb(255 255 255 / .1);--tw-shadow: var(--tw-shadow-colored)}.shadow-white\/5{--tw-shadow-color: rgb(255 255 255 / .05);--tw-shadow: var(--tw-shadow-colored)}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-blue-400\/60{--tw-ring-color: rgb(96 165 250 / .6)}.ring-white\/40{--tw-ring-color: rgb(255 255 255 / .4)}.grayscale{--tw-grayscale: grayscale(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur: blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-\[transform\,box-shadow\,opacity\,filter\,background-color\,border-color\]{transition-property:transform,box-shadow,opacity,filter,background-color,border-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-1000{transition-duration:1s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-\[cubic-bezier\(0\.22\,1\,0\.36\,1\)\]{transition-timing-function:cubic-bezier(.22,1,.36,1)}.will-change-transform{will-change:transform}:root{color-scheme:dark}body{margin:0;display:flex;min-width:320px;min-height:100vh;background-color:#020202}#root{width:100%}.glass{background:#0a0a0ae6;backdrop-filter:blur(64px);-webkit-backdrop-filter:blur(64px)}.glass-card{background:#ffffff04;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid rgba(255,255,255,.08);transition:all .3s cubic-bezier(.4,0,.2,1)}.glass-card:hover{background:#ffffff0a;border-color:#ffffff26}.shine-effect{position:relative;overflow:hidden}.shine-effect:after{content:"";position:absolute;top:-50%;left:-100%;width:33.333333%;height:200%;background:#fff3;transform:rotate(25deg);pointer-events:none;transition:none}.shine-effect:hover:after{left:150%;transition:left .7s ease-in-out}.custom-scrollbar::-webkit-scrollbar{width:4px;height:4px}.custom-scrollbar::-webkit-scrollbar-track{background:transparent}.custom-scrollbar::-webkit-scrollbar-thumb{background:#ffffff1a;border-radius:9999px}.custom-scrollbar::-webkit-scrollbar-thumb:hover{background:#fff3}.custom-select,select{-webkit-appearance:none!important;-moz-appearance:none!important;appearance:none!important;background-color:#000!important;color:#fff!important;border:1px solid rgba(255,255,255,.1)!important;border-radius:12px!important;padding:.4rem 2rem .4rem .75rem!important;cursor:pointer!important;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='white' stroke-width='3'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")!important;background-repeat:no-repeat!important;background-position:right .6rem center!important;background-size:10px!important;transition:all .2s ease!important}.custom-select:hover,select:hover{border-color:#ffffff4d}.custom-select option,select option{background:#000;color:#fff;padding:10px}.action-type-select{min-width:96px!important;box-sizing:border-box!important;border-radius:10px!important;border-color:#ffffff26!important;background-color:#0009!important;padding:.3rem 2.2rem .3rem .65rem!important;position:relative!important;z-index:9999!important;background-clip:padding-box!important}.action-type-select:focus{border-color:#ffffff59!important}.action-card-item.dragging{opacity:.5}.action-card-item.drag-over-top{border-top:2px solid rgba(59,130,246,.5)}.action-card-item.drag-over-bottom{border-bottom:2px solid rgba(59,130,246,.5)}.var-highlight{background:#3b82f633;color:#60a5fa;font-family:JetBrains Mono,monospace;padding:0 2px;border-radius:3px;font-weight:500}.var-highlight-undefined{background:#ef444433;color:#f87171;font-family:JetBrains Mono,monospace;padding:0 2px;border-radius:3px;font-weight:500}.var-highlight-default{background:#22c55e33;color:#4ade80;font-family:JetBrains Mono,monospace;padding:0 2px;border-radius:3px;font-weight:500}.rich-input-content{white-space:pre-wrap;word-wrap:break-word}.rich-input-content:empty:before{content:attr(data-placeholder);color:#6b728099;pointer-events:none}.rich-input-content:focus{outline:none}.code-editor{position:relative;width:100%;min-height:120px;background:#050505;border:1px solid rgba(255,255,255,.1);border-radius:14px;overflow:hidden}.code-editor-pre{position:absolute;inset:0;margin:0;padding:1rem;font-family:JetBrains Mono,monospace;font-size:11px;line-height:1.6;font-variant-ligatures:none;-moz-tab-size:4;-o-tab-size:4;tab-size:4;color:#93c5fdb3;white-space:pre;overflow:hidden;pointer-events:none}.code-editor-textarea{position:absolute;inset:0;width:100%;height:100%;resize:none;background:transparent;color:transparent;caret-color:#fff;border:none;padding:1rem;font-family:JetBrains Mono,monospace;font-size:11px;line-height:1.6;font-variant-ligatures:none;-moz-tab-size:4;-o-tab-size:4;tab-size:4;outline:none;overflow-x:auto;overflow-y:auto;scrollbar-width:none;-ms-overflow-style:none}.code-editor-textarea::-webkit-scrollbar{display:none}.code-editor-textarea-readonly{pointer-events:none}.code-editor-placeholder{color:#6b728099}.code-token-keyword{color:#93c5fd}.code-token-string{color:#86efac}.code-token-number{color:#fbbf24}.code-token-comment{color:#6b7280;font-style:italic}.code-token-boolean,.code-token-null{color:#fca5a5}.code-token-identifier{color:#e5e7eb}.code-token-key{color:#67e8f9}.code-token-tag{color:#60a5fa}.code-token-attr{color:#fcd34d}.code-token-punct{color:#fff9}.text-vertical{writing-mode:vertical-rl;text-orientation:mixed;transform:rotate(180deg)}.selection\:bg-white *::-moz-selection{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.selection\:bg-white *::selection{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.selection\:text-black *::-moz-selection{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.selection\:text-black *::selection{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.selection\:bg-white::-moz-selection{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.selection\:bg-white::selection{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.selection\:text-black::-moz-selection{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.selection\:text-black::selection{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.placeholder\:text-gray-600::-moz-placeholder{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.placeholder\:text-gray-600::placeholder{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.placeholder\:text-white\/10::-moz-placeholder{color:#ffffff1a}.placeholder\:text-white\/10::placeholder{color:#ffffff1a}.odd\:bg-white\/\[0\.02\]:nth-child(odd){background-color:#ffffff05}.focus-within\:border-white\/20:focus-within{border-color:#fff3}.focus-within\:border-white\/30:focus-within{border-color:#ffffff4d}.hover\:-translate-y-1:hover{--tw-translate-y: -.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-105:hover{--tw-scale-x: 1.05;--tw-scale-y: 1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-\[1\.02\]:hover{--tw-scale-x: 1.02;--tw-scale-y: 1.02;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-black\/60:hover{background-color:#0009}.hover\:bg-blue-500\/20:hover{background-color:#3b82f633}.hover\:bg-red-500\/10:hover{background-color:#ef44441a}.hover\:bg-white\/10:hover{background-color:#ffffff1a}.hover\:bg-white\/5:hover{background-color:#ffffff0d}.hover\:bg-white\/90:hover{background-color:#ffffffe6}.hover\:bg-white\/\[0\.05\]:hover{background-color:#ffffff0d}.hover\:bg-white\/\[0\.06\]:hover{background-color:#ffffff0f}.hover\:bg-yellow-500\/10:hover{background-color:#eab3081a}.hover\:text-gray-400:hover{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.hover\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.hover\:text-red-500:hover{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:opacity-80:hover{opacity:.8}.focus\:border-white\/30:focus{border-color:#ffffff4d}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:scale-95:active{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:scale-\[0\.98\]:active{--tw-scale-x: .98;--tw-scale-y: .98;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-60:disabled{opacity:.6}.disabled\:hover\:scale-100:hover:disabled{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.group\/item:hover .group-hover\/item\:opacity-100{opacity:1}@media(min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(min-width:1280px){.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}
package/dist/index.html CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  <head>
5
5
  <meta charset="UTF-8" />
6
- <link rel="icon" type="image/png" href="/icon.png" sizes="256x256" />
6
+ <link rel="icon" type="image/png" href="/icon.png" sizes="256x256" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>Doppelganger - Browser Automation for Everyone</title>
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
@@ -11,8 +11,8 @@
11
11
  <link
12
12
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap"
13
13
  rel="stylesheet">
14
- <script type="module" crossorigin src="/assets/index-B7ntJ0pF.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-OOTrc7_f.css">
14
+ <script type="module" crossorigin src="/assets/index-D68YZVOp.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-WbwoTnJa.css">
16
16
  </head>
17
17
 
18
18
  <body class="bg-[#020202] text-gray-100 font-sans h-full overflow-hidden selection:bg-white selection:text-black">
package/headful.js CHANGED
@@ -1,7 +1,7 @@
1
- const { chromium } = require('playwright');
2
- const fs = require('fs');
3
- const path = require('path');
4
- const { getProxySelection } = require('./proxy-rotation');
1
+ const { chromium } = require('playwright');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { getProxySelection } = require('./proxy-rotation');
5
5
 
6
6
  const STORAGE_STATE_PATH = path.join(__dirname, 'storage_state.json');
7
7
  const STORAGE_STATE_FILE = (() => {
@@ -49,9 +49,9 @@ async function handleHeadful(req, res) {
49
49
 
50
50
  activeSession = { status: 'starting', startedAt: Date.now() };
51
51
 
52
- const url = req.body.url || req.query.url || 'https://www.google.com';
53
- const rotateProxiesRaw = req.body.rotateProxies ?? req.query.rotateProxies;
54
- const rotateProxies = String(rotateProxiesRaw).toLowerCase() === 'true' || rotateProxiesRaw === true;
52
+ const url = req.body.url || req.query.url || 'https://www.google.com';
53
+ const rotateProxiesRaw = req.body.rotateProxies ?? req.query.rotateProxies;
54
+ const rotateProxies = String(rotateProxiesRaw).toLowerCase() === 'true' || rotateProxiesRaw === true;
55
55
 
56
56
  // We stick to the first UA in the list for headful mode to ensure consistency
57
57
  const selectedUA = userAgents[0];
@@ -60,22 +60,22 @@ async function handleHeadful(req, res) {
60
60
 
61
61
  let browser;
62
62
  try {
63
- const launchOptions = {
64
- headless: false,
65
- channel: 'chrome',
66
- args: [
67
- '--no-sandbox',
68
- '--disable-setuid-sandbox',
69
- '--window-size=1280,720',
70
- '--window-position=80,80'
71
- ]
72
- };
73
- const selection = getProxySelection(rotateProxies);
74
- if (selection.proxy) {
75
- launchOptions.proxy = selection.proxy;
76
- }
77
- console.log(`[PROXY] Mode: ${selection.mode}; Target: ${selection.proxy ? selection.proxy.server : 'host_ip'}`);
78
- browser = await chromium.launch(launchOptions);
63
+ const launchOptions = {
64
+ headless: false,
65
+ channel: 'chrome',
66
+ args: [
67
+ '--no-sandbox',
68
+ '--disable-setuid-sandbox',
69
+ '--window-size=1280,720',
70
+ '--window-position=80,80'
71
+ ]
72
+ };
73
+ const selection = getProxySelection(rotateProxies);
74
+ if (selection.proxy) {
75
+ launchOptions.proxy = selection.proxy;
76
+ }
77
+ console.log(`[PROXY] Mode: ${selection.mode}; Target: ${selection.proxy ? selection.proxy.server : 'host_ip'}`);
78
+ browser = await chromium.launch(launchOptions);
79
79
 
80
80
  const contextOptions = {
81
81
  viewport: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doppelgangerdev/doppelganger",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
4
4
  "main": "index.js",
5
5
  "bin": {
6
6
  "doppelganger": "bin/cli.js"
package/proxy-rotation.js CHANGED
@@ -1,161 +1,161 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const crypto = require('crypto');
4
-
5
- const DATA_PROXY_FILE = path.join(__dirname, 'data', 'proxies.json');
6
- const PROXY_FILES = [
7
- DATA_PROXY_FILE,
8
- path.join(__dirname, 'proxies.json')
9
- ];
10
-
11
- let cached = {
12
- file: null,
13
- mtimeMs: 0,
14
- config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false }
15
- };
16
- let rotationIndex = 0;
17
-
18
- const normalizeServer = (raw) => {
19
- if (!raw) return '';
20
- let server = String(raw).trim();
21
- if (!server) return '';
22
- if (!server.includes('://')) {
23
- server = `http://${server}`;
24
- }
25
- return server;
26
- };
27
-
28
- const createProxyId = (seed) => {
29
- const hash = crypto.createHash('sha1').update(String(seed)).digest('hex').slice(0, 12);
30
- return `proxy_${hash}`;
31
- };
32
-
33
- const normalizeProxy = (entry) => {
34
- if (!entry) return null;
35
- if (typeof entry === 'string') {
36
- let raw = entry.trim();
37
- if (!raw) return null;
38
- if (!raw.includes('://')) {
39
- raw = `http://${raw}`;
40
- }
41
- try {
42
- const parsed = new URL(raw);
43
- const server = `${parsed.protocol}//${parsed.host}`;
44
- const username = parsed.username ? decodeURIComponent(parsed.username) : undefined;
45
- const password = parsed.password ? decodeURIComponent(parsed.password) : undefined;
46
- return {
47
- id: createProxyId(`${server}|${username || ''}|${password || ''}`),
48
- server,
49
- username,
50
- password
51
- };
52
- } catch {
53
- return null;
54
- }
55
- }
56
- if (typeof entry === 'object') {
57
- const serverRaw = entry.server || entry.url || entry.proxy;
58
- const server = normalizeServer(serverRaw);
59
- if (!server) return null;
60
- const username = entry.username || entry.user;
61
- const password = entry.password || entry.pass;
62
- const id = entry.id || createProxyId(`${server}|${username || ''}|${password || ''}`);
63
- return {
64
- id,
65
- server,
66
- username,
67
- password,
68
- label: entry.label
69
- };
70
- }
71
- return null;
72
- };
73
-
74
- const loadProxyFile = (filePath) => {
75
- try {
76
- const raw = fs.readFileSync(filePath, 'utf8');
77
- const parsed = JSON.parse(raw);
78
- if (Array.isArray(parsed)) {
79
- return { proxies: parsed, defaultProxyId: null, includeDefaultInRotation: false };
80
- }
81
- const proxies = Array.isArray(parsed.proxies) ? parsed.proxies : [];
82
- const defaultProxyId = parsed.defaultProxyId || null;
83
- const includeDefaultInRotation = !!parsed.includeDefaultInRotation;
84
- return { proxies, defaultProxyId, includeDefaultInRotation };
85
- } catch {
86
- return { proxies: [], defaultProxyId: null, includeDefaultInRotation: false };
87
- }
88
- };
89
-
90
- const loadProxyConfig = () => {
91
- const filePath = PROXY_FILES.find((candidate) => {
92
- try {
93
- return fs.existsSync(candidate);
94
- } catch {
95
- return false;
96
- }
97
- });
98
-
99
- if (!filePath) {
100
- cached = { file: null, mtimeMs: 0, config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false } };
101
- return cached.config;
102
- }
103
-
104
- try {
105
- const stat = fs.statSync(filePath);
106
- const mtimeMs = stat.mtimeMs || 0;
107
- if (cached.file === filePath && cached.mtimeMs === mtimeMs) {
108
- return cached.config;
109
- }
110
- const rawConfig = loadProxyFile(filePath);
111
- const proxies = rawConfig.proxies.map(normalizeProxy).filter(Boolean);
112
- const defaultProxyId = rawConfig.defaultProxyId && proxies.some((proxy) => proxy.id === rawConfig.defaultProxyId)
113
- ? rawConfig.defaultProxyId
114
- : null;
115
- const config = {
116
- proxies,
117
- defaultProxyId,
118
- includeDefaultInRotation: !!rawConfig.includeDefaultInRotation
119
- };
120
- cached = { file: filePath, mtimeMs, config };
121
- return config;
122
- } catch {
123
- cached = { file: filePath, mtimeMs: 0, config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false } };
124
- return cached.config;
125
- }
126
- };
127
-
128
- const saveProxyConfig = (config) => {
129
- const target = DATA_PROXY_FILE;
130
- const payload = {
131
- defaultProxyId: config.defaultProxyId || null,
132
- proxies: Array.isArray(config.proxies) ? config.proxies : [],
133
- includeDefaultInRotation: !!config.includeDefaultInRotation
134
- };
135
- fs.writeFileSync(target, JSON.stringify(payload, null, 2));
136
- try {
137
- const stat = fs.statSync(target);
138
- cached = { file: target, mtimeMs: stat.mtimeMs || 0, config: payload };
139
- } catch {
140
- cached = { file: target, mtimeMs: 0, config: payload };
141
- }
142
- return payload;
143
- };
144
-
145
- const listProxies = () => {
146
- const config = loadProxyConfig();
147
- const hostEntry = {
148
- id: 'host',
149
- server: 'host_ip',
150
- label: 'Host IP (no proxy)'
151
- };
152
- return {
153
- proxies: [hostEntry, ...(config.proxies || [])],
154
- defaultProxyId: config.defaultProxyId || 'host',
155
- includeDefaultInRotation: !!config.includeDefaultInRotation
156
- };
157
- };
158
-
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+
5
+ const DATA_PROXY_FILE = path.join(__dirname, 'data', 'proxies.json');
6
+ const PROXY_FILES = [
7
+ DATA_PROXY_FILE,
8
+ path.join(__dirname, 'proxies.json')
9
+ ];
10
+
11
+ let cached = {
12
+ file: null,
13
+ mtimeMs: 0,
14
+ config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false }
15
+ };
16
+ let rotationIndex = 0;
17
+
18
+ const normalizeServer = (raw) => {
19
+ if (!raw) return '';
20
+ let server = String(raw).trim();
21
+ if (!server) return '';
22
+ if (!server.includes('://')) {
23
+ server = `http://${server}`;
24
+ }
25
+ return server;
26
+ };
27
+
28
+ const createProxyId = (seed) => {
29
+ const hash = crypto.createHash('sha1').update(String(seed)).digest('hex').slice(0, 12);
30
+ return `proxy_${hash}`;
31
+ };
32
+
33
+ const normalizeProxy = (entry) => {
34
+ if (!entry) return null;
35
+ if (typeof entry === 'string') {
36
+ let raw = entry.trim();
37
+ if (!raw) return null;
38
+ if (!raw.includes('://')) {
39
+ raw = `http://${raw}`;
40
+ }
41
+ try {
42
+ const parsed = new URL(raw);
43
+ const server = `${parsed.protocol}//${parsed.host}`;
44
+ const username = parsed.username ? decodeURIComponent(parsed.username) : undefined;
45
+ const password = parsed.password ? decodeURIComponent(parsed.password) : undefined;
46
+ return {
47
+ id: createProxyId(`${server}|${username || ''}|${password || ''}`),
48
+ server,
49
+ username,
50
+ password
51
+ };
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+ if (typeof entry === 'object') {
57
+ const serverRaw = entry.server || entry.url || entry.proxy;
58
+ const server = normalizeServer(serverRaw);
59
+ if (!server) return null;
60
+ const username = entry.username || entry.user;
61
+ const password = entry.password || entry.pass;
62
+ const id = entry.id || createProxyId(`${server}|${username || ''}|${password || ''}`);
63
+ return {
64
+ id,
65
+ server,
66
+ username,
67
+ password,
68
+ label: entry.label
69
+ };
70
+ }
71
+ return null;
72
+ };
73
+
74
+ const loadProxyFile = (filePath) => {
75
+ try {
76
+ const raw = fs.readFileSync(filePath, 'utf8');
77
+ const parsed = JSON.parse(raw);
78
+ if (Array.isArray(parsed)) {
79
+ return { proxies: parsed, defaultProxyId: null, includeDefaultInRotation: false };
80
+ }
81
+ const proxies = Array.isArray(parsed.proxies) ? parsed.proxies : [];
82
+ const defaultProxyId = parsed.defaultProxyId || null;
83
+ const includeDefaultInRotation = !!parsed.includeDefaultInRotation;
84
+ return { proxies, defaultProxyId, includeDefaultInRotation };
85
+ } catch {
86
+ return { proxies: [], defaultProxyId: null, includeDefaultInRotation: false };
87
+ }
88
+ };
89
+
90
+ const loadProxyConfig = () => {
91
+ const filePath = PROXY_FILES.find((candidate) => {
92
+ try {
93
+ return fs.existsSync(candidate);
94
+ } catch {
95
+ return false;
96
+ }
97
+ });
98
+
99
+ if (!filePath) {
100
+ cached = { file: null, mtimeMs: 0, config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false } };
101
+ return cached.config;
102
+ }
103
+
104
+ try {
105
+ const stat = fs.statSync(filePath);
106
+ const mtimeMs = stat.mtimeMs || 0;
107
+ if (cached.file === filePath && cached.mtimeMs === mtimeMs) {
108
+ return cached.config;
109
+ }
110
+ const rawConfig = loadProxyFile(filePath);
111
+ const proxies = rawConfig.proxies.map(normalizeProxy).filter(Boolean);
112
+ const defaultProxyId = rawConfig.defaultProxyId && proxies.some((proxy) => proxy.id === rawConfig.defaultProxyId)
113
+ ? rawConfig.defaultProxyId
114
+ : null;
115
+ const config = {
116
+ proxies,
117
+ defaultProxyId,
118
+ includeDefaultInRotation: !!rawConfig.includeDefaultInRotation
119
+ };
120
+ cached = { file: filePath, mtimeMs, config };
121
+ return config;
122
+ } catch {
123
+ cached = { file: filePath, mtimeMs: 0, config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false } };
124
+ return cached.config;
125
+ }
126
+ };
127
+
128
+ const saveProxyConfig = (config) => {
129
+ const target = DATA_PROXY_FILE;
130
+ const payload = {
131
+ defaultProxyId: config.defaultProxyId || null,
132
+ proxies: Array.isArray(config.proxies) ? config.proxies : [],
133
+ includeDefaultInRotation: !!config.includeDefaultInRotation
134
+ };
135
+ fs.writeFileSync(target, JSON.stringify(payload, null, 2));
136
+ try {
137
+ const stat = fs.statSync(target);
138
+ cached = { file: target, mtimeMs: stat.mtimeMs || 0, config: payload };
139
+ } catch {
140
+ cached = { file: target, mtimeMs: 0, config: payload };
141
+ }
142
+ return payload;
143
+ };
144
+
145
+ const listProxies = () => {
146
+ const config = loadProxyConfig();
147
+ const hostEntry = {
148
+ id: 'host',
149
+ server: 'host_ip',
150
+ label: 'Host IP (no proxy)'
151
+ };
152
+ return {
153
+ proxies: [hostEntry, ...(config.proxies || [])],
154
+ defaultProxyId: config.defaultProxyId || 'host',
155
+ includeDefaultInRotation: !!config.includeDefaultInRotation
156
+ };
157
+ };
158
+
159
159
  const addProxy = (entry) => {
160
160
  const normalized = normalizeProxy(entry);
161
161
  if (!normalized) return null;
@@ -165,84 +165,99 @@ const addProxy = (entry) => {
165
165
  return saveProxyConfig(next);
166
166
  };
167
167
 
168
- const updateProxy = (id, entry) => {
169
- if (!id) return null;
170
- const normalized = normalizeProxy(entry);
171
- if (!normalized) return null;
172
- const config = loadProxyConfig();
173
- const proxies = config.proxies.map((proxy) => {
174
- if (proxy.id !== id) return proxy;
175
- return { ...proxy, ...normalized, id };
176
- });
177
- if (!proxies.some((proxy) => proxy.id === id)) return null;
178
- return saveProxyConfig({ ...config, proxies });
179
- };
180
-
181
- const deleteProxy = (id) => {
182
- if (!id) return null;
183
- const config = loadProxyConfig();
184
- const proxies = config.proxies.filter((proxy) => proxy.id !== id);
185
- const defaultProxyId = config.defaultProxyId === id ? null : config.defaultProxyId;
186
- return saveProxyConfig({ proxies, defaultProxyId });
187
- };
188
-
189
- const setDefaultProxy = (id) => {
190
- const config = loadProxyConfig();
191
- if (!id) {
192
- return saveProxyConfig({ ...config, defaultProxyId: null });
193
- }
194
- if (!config.proxies.some((proxy) => proxy.id === id)) return null;
195
- return saveProxyConfig({ ...config, defaultProxyId: id });
196
- };
197
-
198
- const setIncludeDefaultInRotation = (enabled) => {
168
+ const addProxies = (entries) => {
169
+ if (!Array.isArray(entries)) return null;
170
+ const normalizedEntries = entries.map(normalizeProxy).filter(Boolean);
171
+ if (normalizedEntries.length === 0) return null;
199
172
  const config = loadProxyConfig();
200
- return saveProxyConfig({ ...config, includeDefaultInRotation: !!enabled });
201
- };
202
-
203
- const getNextProxy = (proxies) => {
204
- if (!proxies.length) return null;
205
- const selected = proxies[rotationIndex % proxies.length];
206
- rotationIndex += 1;
207
- return selected;
208
- };
209
-
210
- const getProxySelection = (rotateProxies) => {
211
- const config = loadProxyConfig();
212
- const proxies = config.proxies || [];
213
- const hostEntry = { id: 'host', server: 'host_ip', label: 'Host IP (no proxy)' };
214
- const pool = [hostEntry, ...proxies];
215
- const defaultProxy = config.defaultProxyId
216
- ? proxies.find((proxy) => proxy.id === config.defaultProxyId) || null
217
- : null;
218
- const defaultIsHost = !config.defaultProxyId;
219
- const includeDefaultInRotation = !!config.includeDefaultInRotation;
220
-
221
- if (rotateProxies) {
222
- let rotationPool = pool;
223
- if (!includeDefaultInRotation) {
224
- if (defaultIsHost) {
225
- rotationPool = pool.filter((proxy) => proxy.id !== 'host');
226
- } else {
227
- rotationPool = pool.filter((proxy) => proxy.id !== config.defaultProxyId);
228
- }
229
- }
230
- if (rotationPool.length > 0) {
231
- const picked = getNextProxy(rotationPool);
232
- return { proxy: picked && picked.id !== 'host' ? picked : null, mode: 'rotate' };
233
- }
234
- if (defaultProxy) return { proxy: defaultProxy, mode: 'default' };
235
- return { proxy: null, mode: 'host' };
236
- }
237
-
238
- if (defaultProxy) return { proxy: defaultProxy, mode: 'default' };
239
- return { proxy: null, mode: 'host' };
173
+ const additions = normalizedEntries.map((proxy) => ({
174
+ ...proxy,
175
+ id: `proxy_${crypto.randomBytes(6).toString('hex')}`
176
+ }));
177
+ const proxies = [...config.proxies, ...additions];
178
+ const next = { ...config, proxies };
179
+ return saveProxyConfig(next);
240
180
  };
241
-
181
+
182
+ const updateProxy = (id, entry) => {
183
+ if (!id) return null;
184
+ const normalized = normalizeProxy(entry);
185
+ if (!normalized) return null;
186
+ const config = loadProxyConfig();
187
+ const proxies = config.proxies.map((proxy) => {
188
+ if (proxy.id !== id) return proxy;
189
+ return { ...proxy, ...normalized, id };
190
+ });
191
+ if (!proxies.some((proxy) => proxy.id === id)) return null;
192
+ return saveProxyConfig({ ...config, proxies });
193
+ };
194
+
195
+ const deleteProxy = (id) => {
196
+ if (!id) return null;
197
+ const config = loadProxyConfig();
198
+ const proxies = config.proxies.filter((proxy) => proxy.id !== id);
199
+ const defaultProxyId = config.defaultProxyId === id ? null : config.defaultProxyId;
200
+ return saveProxyConfig({ proxies, defaultProxyId });
201
+ };
202
+
203
+ const setDefaultProxy = (id) => {
204
+ const config = loadProxyConfig();
205
+ if (!id) {
206
+ return saveProxyConfig({ ...config, defaultProxyId: null });
207
+ }
208
+ if (!config.proxies.some((proxy) => proxy.id === id)) return null;
209
+ return saveProxyConfig({ ...config, defaultProxyId: id });
210
+ };
211
+
212
+ const setIncludeDefaultInRotation = (enabled) => {
213
+ const config = loadProxyConfig();
214
+ return saveProxyConfig({ ...config, includeDefaultInRotation: !!enabled });
215
+ };
216
+
217
+ const getNextProxy = (proxies) => {
218
+ if (!proxies.length) return null;
219
+ const selected = proxies[rotationIndex % proxies.length];
220
+ rotationIndex += 1;
221
+ return selected;
222
+ };
223
+
224
+ const getProxySelection = (rotateProxies) => {
225
+ const config = loadProxyConfig();
226
+ const proxies = config.proxies || [];
227
+ const hostEntry = { id: 'host', server: 'host_ip', label: 'Host IP (no proxy)' };
228
+ const pool = [hostEntry, ...proxies];
229
+ const defaultProxy = config.defaultProxyId
230
+ ? proxies.find((proxy) => proxy.id === config.defaultProxyId) || null
231
+ : null;
232
+ const defaultIsHost = !config.defaultProxyId;
233
+ const includeDefaultInRotation = !!config.includeDefaultInRotation;
234
+
235
+ if (rotateProxies) {
236
+ let rotationPool = pool;
237
+ if (!includeDefaultInRotation) {
238
+ if (defaultIsHost) {
239
+ rotationPool = pool.filter((proxy) => proxy.id !== 'host');
240
+ } else {
241
+ rotationPool = pool.filter((proxy) => proxy.id !== config.defaultProxyId);
242
+ }
243
+ }
244
+ if (rotationPool.length > 0) {
245
+ const picked = getNextProxy(rotationPool);
246
+ return { proxy: picked && picked.id !== 'host' ? picked : null, mode: 'rotate' };
247
+ }
248
+ if (defaultProxy) return { proxy: defaultProxy, mode: 'default' };
249
+ return { proxy: null, mode: 'host' };
250
+ }
251
+
252
+ if (defaultProxy) return { proxy: defaultProxy, mode: 'default' };
253
+ return { proxy: null, mode: 'host' };
254
+ };
255
+
242
256
  module.exports = {
243
257
  getProxySelection,
244
258
  listProxies,
245
259
  addProxy,
260
+ addProxies,
246
261
  updateProxy,
247
262
  deleteProxy,
248
263
  setDefaultProxy,