diff --git a/libs/shared/interfaces/src/lib/provider-import-detection.util.spec.ts b/libs/shared/interfaces/src/lib/provider-import-detection.util.spec.ts index bad0e4477..13af6d886 100644 --- a/libs/shared/interfaces/src/lib/provider-import-detection.util.spec.ts +++ b/libs/shared/interfaces/src/lib/provider-import-detection.util.spec.ts @@ -699,6 +699,23 @@ describe('detectProviderImportCandidates', () => { } }); + it('treats same-host portals on different ports as different panels', () => { + const candidates = detectProviderImportCandidates( + [ + 'http://panel.example.com:8080/c/', + 'MAC: 00:1A:79:AA:AA:AA', + 'http://panel.example.com:9090/c/', + 'MAC: 00:1A:79:BB:BB:BB', + ].join('\n') + ); + + const stalker = only(candidates, 'stalker'); + expect(stalker).toHaveLength(2); + for (const candidate of stalker) { + expect(candidate.portalUrl).toBeUndefined(); + } + }); + it('treats several MACs with generic URLs on different hosts as ambiguous too', () => { // Without portal-shaped URLs the picker falls back to a generic // URL — the ambiguity guard must cover that pool as well, or two @@ -721,14 +738,15 @@ describe('detectProviderImportCandidates', () => { it('surfaces every MAC of a long multi-account list', () => { const macs = Array.from( - { length: 6 }, - (_, index) => `00:1A:79:0${index}:0${index}:0${index}` + { length: 20 }, + (_, index) => + `00:1A:79:${String(index).padStart(2, '0')}:11:22` ); const candidates = detectProviderImportCandidates( ['http://long.example.com/c/', ...macs].join('\n') ); - expect(only(candidates, 'stalker')).toHaveLength(6); + expect(only(candidates, 'stalker')).toHaveLength(20); }); it('gives labeled credentials to the stalker candidate, not a competing xtream guess', () => { diff --git a/libs/shared/interfaces/src/lib/provider-import-detection.util.ts b/libs/shared/interfaces/src/lib/provider-import-detection.util.ts index 326994822..299907735 100644 --- a/libs/shared/interfaces/src/lib/provider-import-detection.util.ts +++ b/libs/shared/interfaces/src/lib/provider-import-detection.util.ts @@ -69,9 +69,10 @@ export interface ProviderImportCandidate { const GET_PHP_PATH_PATTERN = /\/get\.php$/i; const EXTM3U_HEADER_PATTERN = /^\s*#EXTM3U/i; -// Guard against pathological pastes, not against real handouts: multi-account -// MAC lists with a dozen entries are legitimate and must all surface. -const MAX_CANDIDATES = 16; +// DOM-safety guard against pathological pastes, not a business limit: sized +// above the scanners' combined caps (64 MACs + 16 URLs), so every candidate +// a real multi-account handout produces survives the final slice. +const MAX_CANDIDATES = 96; /** * Scans a pasted provider message and returns ranked import candidates. @@ -132,9 +133,13 @@ export function detectProviderImportCandidates( : labeledHostUrl(labeled) !== undefined ? [] : urls.filter((url) => url.role === 'generic'); + // Compared by ORIGIN, not hostname: two panels on one DNS name but + // different ports are different panels, while URL normalization drops a + // default port, so the Real/Panel `:80`-vs-bare pairs of scanner dumps + // still compare equal. const portalAmbiguous = macs.length > 1 && - new Set(portalPool.map((url) => url.parsed.hostname)).size > 1; + new Set(portalPool.map((url) => url.parsed.origin)).size > 1; const portal = portalAmbiguous ? null : pickStalkerPortalUrl(urls, labeled); diff --git a/libs/shared/interfaces/src/lib/provider-import-scan.util.ts b/libs/shared/interfaces/src/lib/provider-import-scan.util.ts index 92be049e6..d86585f06 100644 --- a/libs/shared/interfaces/src/lib/provider-import-scan.util.ts +++ b/libs/shared/interfaces/src/lib/provider-import-scan.util.ts @@ -52,11 +52,13 @@ const HOSTISH_PATTERN = /^[a-z0-9][a-z0-9.-]*(?::\d{2,5})?$/i; const WRAPPING_CHARS = /^["'`«<([]+|["'`»>)\]]+$/g; -// Both caps guard against pathological pastes only, and stay aligned with -// the assembler's candidate cap so a real multi-link or multi-account -// handout is never truncated by the scanner before assembly even sees it. +// Both caps are DOM-safety guards against pathological pastes (a log file +// full of MAC-shaped hex would otherwise render thousands of cards), sized +// far beyond any real handout — reseller and scanner dumps top out around a +// couple dozen accounts. The assembler's candidate cap leaves headroom for +// both scanners together, so neither truncates real input before assembly. const MAX_URLS = 16; -const MAX_MACS = 12; +const MAX_MACS = 64; interface FieldMatcher { field: keyof LabeledFields;