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 66c95b0ab..4e9e08f37 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 @@ -678,6 +678,27 @@ describe('detectProviderImportCandidates', () => { } }); + it('omits the portal when several MACs meet portals on different hosts', () => { + // Two panels + two MACs in one paste: no deterministic owner for + // either portal, so the candidates carry the MAC alone instead of + // prefilling half of them with the wrong panel. + const candidates = detectProviderImportCandidates( + [ + 'http://panel-a.example.com/c/', + 'MAC: 00:1A:79:AA:AA:AA', + 'http://panel-b.example.com/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(); + expect(candidate.confidence).toBe('low'); + } + }); + it('surfaces every MAC of a long multi-account list', () => { const macs = Array.from( { length: 6 }, @@ -779,15 +800,18 @@ describe('detectProviderImportCandidates', () => { expect(only(candidates, 'm3u-url')).toHaveLength(1); }); - it('caps the number of returned candidates', () => { - const urls = Array.from( + it('surfaces every link of a long list and caps only pathological pastes', () => { + const twelve = Array.from( { length: 12 }, (_, index) => `https://lists.example.com/list-${index}.m3u` ).join('\n'); + expect(detectProviderImportCandidates(twelve)).toHaveLength(12); - expect( - detectProviderImportCandidates(urls).length - ).toBeLessThanOrEqual(16); + const twenty = Array.from( + { length: 20 }, + (_, index) => `https://lists.example.com/list-${index}.m3u` + ).join('\n'); + expect(detectProviderImportCandidates(twenty)).toHaveLength(16); }); }); }); 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 6f7c98032..ddc7f2b03 100644 --- a/libs/shared/interfaces/src/lib/provider-import-detection.util.ts +++ b/libs/shared/interfaces/src/lib/provider-import-detection.util.ts @@ -115,7 +115,21 @@ export function detectProviderImportCandidates( // label belongs to — and a device ID submitted with the wrong MAC is // pinned by the portal permanently. Multi-MAC candidates therefore carry // portal + MAC only, and the user supplies identity per account. - const portal = pickStalkerPortalUrl(urls, labeled); + // Several MACs meeting portal-shaped URLs on DIFFERENT hosts is two + // accounts from two panels in one paste — there is no deterministic way + // to say which MAC belongs to which portal, so those candidates carry the + // MAC alone instead of prefilling half of them with the wrong panel. + // Same-host URL pairs (the Real/Panel lines of scanner dumps) stay + // unambiguous, as does one portal shared by a whole MAC list. + const stalkerHosts = new Set( + urls + .filter((url) => url.role === 'stalker') + .map((url) => url.parsed.hostname) + ); + const portalAmbiguous = macs.length > 1 && stalkerHosts.size > 1; + const portal = portalAmbiguous + ? null + : pickStalkerPortalUrl(urls, labeled); const identity: LabeledFields = macs.length === 1 ? labeled : {}; for (const macAddress of macs) { candidates.push( 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 9584ad776..92be049e6 100644 --- a/libs/shared/interfaces/src/lib/provider-import-scan.util.ts +++ b/libs/shared/interfaces/src/lib/provider-import-scan.util.ts @@ -52,9 +52,10 @@ const HOSTISH_PATTERN = /^[a-z0-9][a-z0-9.-]*(?::\d{2,5})?$/i; const WRAPPING_CHARS = /^["'`«<([]+|["'`»>)\]]+$/g; -const MAX_URLS = 8; -// High enough that a real multi-account handout is never silently truncated; -// only a pathological paste (hex dumps, logs) hits it. +// 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. +const MAX_URLS = 16; const MAX_MACS = 12; interface FieldMatcher {