Skip to content
6 changes: 3 additions & 3 deletions README-fa.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

### 🛡️ ECH
<ul dir="rtl" style="direction: rtl; text-align: right;">
<li><b>جای‌گذاری و ارتقا</b>: لینک تکی یا چندگانه (تک‌به‌تک در هر خط) از <code>vless://</code> یا <code>trojan://</code> را جای‌گذاری کنید و لینک‌های ارتقایافته با پارامترهای <code>fp</code> و <code>ech</code> دریافت کنید.</li>
<li><b>جای‌گذاری و ارتقا</b>: لینک تکی یا چندگانه (تک‌به‌تک در هر خط) از <code>vless://</code> یا <code>trojan://</code> را جای‌گذاری کنید و لینک‌های ارتقایافته با پارامترهای <code>fp</code> و <code>ech</code> دریافت کنید. اگر یک اشتراک رمزگذاری‌شده (یک بلوک base64، بلوک چندخطی یا base64 خط‌به‌خط) را اینجا جای‌گذاری کنید، به همه کانفیگ‌های VLESS/Trojan داخل آن باز می‌شود و به‌جای یک لینک ادغام‌شده، همه را ارتقا می‌دهد.</li>
<li><b>ورودی اشتراک</b>: لینک اشتراک را برای دانلود در مرورگر جای‌گذاری کنید — یا محتوای آن را (متن ساده یا base64) جای‌گذاری کنید — تا همه کانفیگ‌های VLESS و Trojan داخل آن با همان گزینه‌های ECH ارتقا یابند. تغییر سرور در اینجا نادیده گرفته می‌شود (هر کانفیگ آدرس خودش را نگه می‌دارد)؛ با تغییر گزینه‌های ECH، کانفیگ‌های گرفته‌شده بدون دانلود مجدد ارتقا می‌یابند.</li>
<li><b>تغییر سرور</b>: فیلد سرور (IP/دامنه) فقط برای کانفیگ تکی به‌صورت خودکار از URL استخراج می‌شود و قابل ویرایش است. در حالت چند کانفیگ، این فیلد خالی می‌ماند و فقط اگر مقدار دلخواه وارد کنید روی همه کانفیگ‌ها اعمال می‌شود (پشتیبانی از IPv4، IPv6 و دامنه).</li>
<li><b>اثر انگشت (Fingerprint)</b>: پیش‌فرض <code>chrome</code> با گزینه‌های <code>unsafe</code>، <code>firefox</code>، <code>safari</code>، <code>random</code> و <code>none</code>.</li>
Expand All @@ -28,13 +28,13 @@

### 🧬 Fragment + Fingerprint
<ul dir="rtl" style="direction: rtl; text-align: right;">
<li><b>جای‌گذاری و ارتقا</b>: لینک تکی یا چندگانه (تک‌به‌تک در هر خط) از <code>vless://</code> یا <code>trojan://</code> را جای‌گذاری کنید و لینک‌های ارتقایافته با پارامترهای <code>cs</code>، <code>fm</code> و <code>fp</code> دریافت کنید.</li>
<li><b>جای‌گذاری و ارتقا</b>: لینک تکی یا چندگانه (تک‌به‌تک در هر خط) از <code>vless://</code> یا <code>trojan://</code> را جای‌گذاری کنید و لینک‌های ارتقایافته با پارامترهای <code>cs</code>، <code>fm</code> و <code>fp</code> دریافت کنید. اگر یک اشتراک رمزگذاری‌شده (یک بلوک base64، بلوک چندخطی یا base64 خط‌به‌خط) را اینجا جای‌گذاری کنید، به همه کانفیگ‌های VLESS/Trojan داخل آن باز می‌شود و به‌جای یک لینک ادغام‌شده، همه را ارتقا می‌دهد.</li>
<li><b>پشتیبانی از چندین کانفیگ همزمان</b>: امکان جای‌گذاری لیست انبوه کانفیگ‌ها جهت پردازش، اعتبارسنجی و ارتقای دسته‌جمعی بدون تداخل در remark کانفیگ‌ها.</li>
<li><b>تغییر سرور</b>: فیلد سرور (IP/دامنه) فقط برای کانفیگ تکی به‌صورت خودکار از URL استخراج می‌شود و قابل ویرایش است. در حالت چند کانفیگ، این فیلد خالی می‌ماند و فقط اگر مقدار دلخواه وارد کنید روی همه کانفیگ‌ها اعمال می‌شود (پشتیبانی از IPv4، IPv6 و دامنه).</li>
<li><b>اثر انگشت (Fingerprint)</b>: پیش‌فرض <code>unsafe</code> با گزینه‌های <code>chrome</code>، <code>firefox</code>، <code>safari</code>، <code>random</code> و <code>none</code>.</li>
<li><b>نسخه‌های فرگمنت (Fragment)</b>: فیلد <code>fm</code> دارای دو preset است — <code>v1</code> کلاسیک (<code>5,94,1</code> / <code>109,1</code> / split برابر <code>355</code>) و <code>v2</code> جدید (پیش‌فرض: <code>0,104,1</code> / <code>114,1</code> / split برابر <code>11</code>). با سلکتور <code>Fragment Version</code> جابه‌جا شوید؛ فیلد همچنان دستی قابل ویرایش است.</li>
<li><b>آگاه از TLS</b>: پارامترهای <code>cs</code> و <code>fm</code> فقط زمانی اضافه می‌شوند که کانفیگ از امنیت <code>tls</code> استفاده کند. با خالی کردن هر فیلد، آن پارامتر حذف می‌شود.</li>
<li><b>ساب‌تب اشتراک</b>: لینک اشتراک را مستقیم در مرورگر دریافت کنید، رمزگشایی (متن ساده یا base64) و ارتقای دسته‌جمعی کانفیگ‌های VLESS و Trojan داخل آن — یا محتوایش را جای‌گذاری کنید / فایل <code>.txt</code> ذخیره‌شده را روی کارت بیندازید تا دانلود لازم نباشد. از همان گزینه‌های <code>fp</code> / <code>cs</code> / <code>fm</code> بالا استفاده می‌شود؛ تغییر سرور نادیده گرفته می‌شود (هر کانفیگ آدرس خودش را نگه می‌دارد).</li>
<li><b>ساب‌تب اشتراک</b>: لینک اشتراک را مستقیم در مرورگر دریافت کنید، رمزگشایی (متن ساده، یک بلوک base64، یا base64 خط‌به‌خط) و ارتقای دسته‌جمعی کانفیگ‌های VLESS و Trojan داخل آن — یا محتوایش را جای‌گذاری کنید / فایل <code>.txt</code> ذخیره‌شده را روی کارت بیندازید تا دانلود لازم نباشد. از همان گزینه‌های <code>fp</code> / <code>cs</code> / <code>fm</code> بالا استفاده می‌شود؛ تغییر سرور نادیده گرفته می‌شود (هر کانفیگ آدرس خودش را نگه می‌دارد).</li>
<li><b>providerهای بسته</b>: اگر provider هدر CORS نفرستد، خطا ۳ قدم راهنمایی به‌همراه دکمه <b>Open link</b> برای باز کردن لینک در تب جدید و کپی نشان می‌دهد.</li>
<li><b>خروجی</b>: کپی در کلیپ‌بورد، دانلود <code>.txt</code> (هر خط یک کانفیگ) یا <code>base64 .txt</code> (فرمت استاندارد بدنه اشتراک).</li>
<li><b>بدون ECH</b>: پارامتر <code>ech</code> فقط در تب <b>ECH</b> اعمال می‌شود — به کانفیگ‌های اشتراک اضافه نمی‌گردد.</li>
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ All processing happens in your browser. No data is sent to any server — fetchi
## 🚀 Features

### 🛡️ ECH
- **Paste & Enhance**: Paste single or multiple `vless://` or `trojan://` URLs (one per line) and get enhanced links with `fp` and `ech` parameters added.
- **Paste & Enhance**: Paste single or multiple `vless://` or `trojan://` URLs (one per line) and get enhanced links with `fp` and `ech` parameters added. An encoded subscription pasted here (one whole base64 blob, a line-wrapped blob, or per-line base64) is expanded into every VLESS/Trojan config it holds instead of collapsing into a single link.
- **Subscription Input**: Paste a subscription link to fetch it in your browser — or paste its contents (plain or base64) — to batch-enhance every VLESS/Trojan config inside with the same ECH options. The server override is ignored here (each config keeps its own address); changing the ECH options re-enhances the fetched configs without re-fetching.
- **Server Override**: The server (IP/domain) field is auto-filled from the URL for a single config and is user-editable. With multiple configs the field stays empty and only applies to all configs if you type a custom value (supports IPv4, IPv6 and domains).
- **Fingerprint**: Default `chrome`, with `unsafe`, `firefox`, `safari`, `random` and `none` options.
Expand All @@ -25,13 +25,13 @@ All processing happens in your browser. No data is sent to any server — fetchi
- **Protocol Support**: **VLESS** and **Trojan**.

### 🧬 Fragment + Fingerprint
- **Paste & Enhance**: Paste single or multiple `vless://` or `trojan://` URLs (one per line) and get enhanced links with `cs`, `fm` and `fp` parameters added.
- **Paste & Enhance**: Paste single or multiple `vless://` or `trojan://` URLs (one per line) and get enhanced links with `cs`, `fm` and `fp` parameters added. An encoded subscription pasted here (one whole base64 blob, a line-wrapped blob, or per-line base64) is expanded into every VLESS/Trojan config it holds instead of collapsing into a single link.
- **Multi-Config Support**: Paste bulk proxy URL lists to parse, validate, and batch enhance all URLs cleanly without remark collision.
- **Server Override**: The server (IP/domain) field is auto-filled from the URL for a single config and is user-editable. With multiple configs the field stays empty and only applies to all configs if you type a custom value (supports IPv4, IPv6 and domains).
- **Fingerprint**: Default `unsafe`, with `chrome`, `firefox`, `safari`, `random` and `none` options.
- **Fragment presets**: `fm` has `v1` (classic: `5,94,1` / `109,1` / split `355`) and `v2` (new, default: `0,104,1` / `114,1` / split `11`). Switch via the `Fragment Version` selector; the field stays editable for manual tweaks.
- **TLS-aware**: `cs` and `fm` are only added when the config uses `tls` security. Clear a field to skip that parameter.
- **Subscription sub-tab**: Paste a subscription link to fetch it directly in your browser, decode it (plain or base64) and batch-enhance every VLESS/Trojan config inside — or paste its contents / drop a saved `.txt` file to skip fetching. Uses the same `fp` / `cs` / `fm` options above; the server override is ignored (each config keeps its own address).
- **Subscription sub-tab**: Paste a subscription link to fetch it directly in your browser, decode it (plain text, one base64 blob, or per-line base64) and batch-enhance every VLESS/Trojan config inside — or paste its contents / drop a saved `.txt` file to skip fetching. Uses the same `fp` / `cs` / `fm` options above; the server override is ignored (each config keeps its own address).
- **Blocked Providers**: If the provider sends no CORS headers, the error shows 3 quick steps and an **Open link** button opens the URL in a new tab for copying.
- **Export**: Copy to clipboard, download as `.txt` (one per line) or base64 `.txt` (standard subscription body).
- **No ECH here**: ECH is applied only in the **ECH** tab — subscription entries never get an `ech` parameter.
Expand Down
103 changes: 84 additions & 19 deletions pure.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,13 +252,22 @@ function subIsLink(v) {
return !/\s/.test(v) && /^https?:\/\/\S+$/i.test(v);
}

// Mirrors extractSubConfigs() instead of guessing from the first line or a
// length threshold: any line with a known scheme counts, and a short base64
// blob is accepted if it actually decodes to a config.
// Mirrors extractSubConfigs(): plain scheme lines count, the whole-body
// blob counts when its decode holds a config, and per-line base64 blobs
// count when EVERY line decodes to at least one config. The per-line rule
// matters for padded chunks — their concatenation fails the whole-body
// decode (mid-stream '=' padding), so without this branch the subscription
// tab would reject bodies the bulk box accepts through expandBulkLines().
function subLooksLikeContent(v) {
if (extractLines(v).some(l => SUB_SCHEME.test(l))) return true;
const whole = safeAtob(v.replace(/\s+/g, ''));
return !!whole && extractLines(whole).some(l => SUB_SCHEME.test(l));
if (whole && extractLines(whole).some(l => SUB_SCHEME.test(l))) return true;
const lines = extractLines(v);
if (lines.length === 0) return false;
return lines.every(line => {
const decoded = safeAtob(line.replace(/^\/\/.*$/, '').trim());
return !!decoded && extractLines(decoded).some(l => SUB_SCHEME.test(l));
});
}

// A single http(s) URL with an explicit port but no path, query or hash is
Expand Down Expand Up @@ -308,8 +317,8 @@ const SUB_TOTAL_MS = 5 * 60 * 1000;
const SUB_TOO_LARGE_MSG = 'That file is too large — drop a plain-text subscription file (.txt), usually a few hundred KB at most.';

// Returns every proxy line found in a subscription body, whether it arrives
// as plain text, as one base64 blob, or as a mix of plain lines and
// per-line base64 blobs.
// as plain text, as one base64 blob (optionally line-wrapped), or as per-line
// base64 blobs.
function extractSubConfigs(body) {
let text = (body || '').trim();
if (!text) return [];
Expand Down Expand Up @@ -337,24 +346,72 @@ function extractSubConfigs(body) {
return out;
}

// Base64 body: try the whole payload first (standard subscription format),
// then line by line for providers that send a base64 blob per config.
// Base64 body: try the whole payload first (standard subscription
// format, possibly wrapped across lines). This is also what keeps
// line-wrapped blobs correct: each wrapped chunk is a slice of the ONE
// underlying decoded text, and a slice that begins or ends mid-config
// still matches the scheme regex (a cut-off head like "vless://u@" or a
// tail like "ss://u@..."), so a per-line-everywhere test cannot
// distinguish the two shapes by itself. A genuinely per-line body whose
// chunks lack padding is exactly the shape whose whole-body merge
// DEGENERATES: the concatenation decodes to a single swallowed line, so
// the whole-body list holds at most one scheme line. Per-line therefore
// wins only when the whole-body decode fails or is degenerate (≤1
// scheme line), and the multi-config whole-body result always takes
// precedence.
const whole = safeAtob(text.replace(/\s+/g, ''));
if (whole) {
const decodedLines = extractLines(whole);
if (decodedLines.some(l => SUB_SCHEME.test(l))) {
return decodedLines;
const wholeLines = whole ? extractLines(whole) : [];
const wholeConfigs = wholeLines.filter(l => SUB_SCHEME.test(l));
if (wholeConfigs.length > 1) {
return wholeLines;
}

const perLine = [];
let everyLineIsConfig = lines.length > 0;
for (const line of lines) {
const decoded = safeAtob(line.replace(/^\/\/.*$/, '').trim());
if (!decoded) {
everyLineIsConfig = false;
continue;
}
const configs = extractLines(decoded).filter(l => SUB_SCHEME.test(l));
if (configs.length === 0) everyLineIsConfig = false;
perLine.push(...configs);
}
if (everyLineIsConfig && perLine.length > 0) return perLine;

if (whole && wholeLines.some(l => SUB_SCHEME.test(l))) {
return wholeLines;
}

// Last resort: some providers base64 each config on its own line. Only
// Last resort: keep whatever per-line configs were decoded above. Only
// decoded lines that actually hold a proxy scheme count — otherwise any
// base64-looking word (an HTML file, a comment) becomes a "config".
return extractLines(text)
.map(l => safeAtob(l.replace(/^\/\/.*$/, '').trim()))
.filter(Boolean)
.flatMap(extractLines)
.filter(l => SUB_SCHEME.test(l));
return perLine;
}

// The bulk URL box ("Paste URL(s)") advertises a one-per-line list, but users
// routinely paste an encoded subscription instead. Expand that here: a whole
// blob, a line-wrapped blob or per-line blobs all decode into their individual
// config lines via extractSubConfigs(). Without it the single blob line reaches
// parseProxyURLSingle(), whose base64 fallback recurses on the decoded body —
// and new URL() silently strips the line breaks, collapsing every config into
// the first one's remark ("bulk paste shows one link"). extractSubConfigs
// keeps raw base64 lines that decode to configs AND appends the decoded
// copies; the subscription tab's renderer skips such raw lines before
// parsing, but the bulk box would parse both copies — so drop them here,
// where every bulk consumer funnels through. Junk that decodes
// to nothing counts as no config and stays, so genuinely invalid input
// still surfaces the usual per-line parse error instead of looking empty.
function expandBulkLines(raw) {
const value = (raw || '').trim();
if (!value) return [];
const expanded = extractSubConfigs(value).filter(line => {
if (SUB_SCHEME.test(line)) return true;
const decoded = safeAtob(line.replace(/^\/\/.*$/, '').trim());
return !(decoded && extractLines(decoded).some(l => SUB_SCHEME.test(l)));
});
return expanded.length ? expanded : extractLines(value);
}

// Transport failures surface as TypeError in every browser, but the message
Expand Down Expand Up @@ -421,6 +478,14 @@ function parseProxyURLSingle(raw) {
try {
const decoded = safeAtob(url);
if (decoded) {
// A decoded value carrying more than one config line is a whole
// subscription body, not a single link. Recursing would let the
// WHATWG URL parser strip the line breaks and merge every config
// into the first one's remark — the "bulk paste shows one link"
// bug. Bulk callers expand it first (expandBulkLines).
if (extractLines(decoded).filter(l => SUB_SCHEME.test(l)).length > 1) {
return { error: 'That base64 value holds several configs — paste the list or use the Subscription input' };
}
if (decoded.includes('"add"')) {
return parseVmess('vmess://' + url);
}
Expand Down Expand Up @@ -1428,6 +1493,6 @@ if (typeof module !== 'undefined' && module.exports) {
maskString, truncateStr,
subIsLink, subLooksLikeContent, subLooksLikeProxy, subValidate,
SUB_IDLE_MS, SUB_TOTAL_MS, SUB_TOO_LARGE_MSG,
extractSubConfigs, subIsBlockedError, subErrorText
extractSubConfigs, expandBulkLines, subIsBlockedError, subErrorText
};
}
6 changes: 2 additions & 4 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -1193,8 +1193,7 @@
outputSection.style.display = 'none';
outputUrl.textContent = '';
outputRemark.textContent = '';
const val = urlInput.value.trim();
const lines = extractLines(val);
const lines = expandBulkLines(urlInput.value);
let parsedList = [];
let unsupportedCount = 0;
let invalidCount = 0;
Expand Down Expand Up @@ -1269,8 +1268,7 @@
}

function onTabUrlEnhance() {
const val = urlInput.value.trim();
const lines = extractLines(val);
const lines = expandBulkLines(urlInput.value);
if (lines.length === 0) {
outputSection.style.display = 'none';
return;
Expand Down
Loading
Loading