11import { saveConfig } from "../config.js" ;
2+ import { extractApiKey } from "../auth.js" ;
23
34interface LoginOptions {
45 json ?: boolean ;
@@ -10,11 +11,6 @@ interface LoginResult {
1011 error ?: string ;
1112}
1213
13- const BASE_URL = "https://www.marktguru.at" ;
14- const API_BASE = "https://api.marktguru.at/api/v1" ;
15- const DEFAULT_ZIP_CODE = "1010" ;
16- const MAX_SCRIPTS = 20 ;
17-
1814function output ( result : LoginResult , json : boolean ) : void {
1915 if ( json ) {
2016 console . log ( JSON . stringify ( result ) ) ;
@@ -26,166 +22,16 @@ function output(result: LoginResult, json: boolean): void {
2622 }
2723}
2824
29- async function maybeGetHeaders ( ) : Promise < Record < string , string > > {
30- try {
31- const { HeaderGenerator } = await import ( "header-generator" ) ;
32- const generator = new HeaderGenerator ( {
33- browsers : [ { name : "chrome" , minVersion : 110 } ] ,
34- devices : [ "desktop" ] ,
35- operatingSystems : [ "macos" ] ,
36- } ) ;
37- return generator . getHeaders ( { httpVersion : "2" } ) ;
38- } catch {
39- return {
40- "user-agent" :
41- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" ,
42- "accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" ,
43- "accept-language" : "en-US,en;q=0.9" ,
44- "accept-encoding" : "gzip, deflate, br" ,
45- } ;
46- }
47- }
48-
49- async function fetchText ( url : string , headers : Record < string , string > ) : Promise < string > {
50- const controller = new AbortController ( ) ;
51- const timeout = setTimeout ( ( ) => controller . abort ( ) , 15000 ) ;
52- try {
53- const res = await fetch ( url , { headers, signal : controller . signal } ) ;
54- if ( ! res . ok ) {
55- throw new Error ( `HTTP ${ res . status } for ${ url } ` ) ;
56- }
57- return await res . text ( ) ;
58- } finally {
59- clearTimeout ( timeout ) ;
60- }
61- }
62-
63- async function fetchFirstOk ( urls : string [ ] , headers : Record < string , string > ) {
64- let lastError : unknown = null ;
65- for ( const url of urls ) {
66- try {
67- const text = await fetchText ( url , headers ) ;
68- return { url, text } ;
69- } catch ( error ) {
70- lastError = error ;
71- }
72- }
73- if ( lastError ) throw lastError ;
74- throw new Error ( "No URLs to fetch." ) ;
75- }
76-
77- function extractScriptUrls ( html : string ) : string [ ] {
78- const urls = new Set < string > ( ) ;
79- const regex = / < s c r i p t [ ^ > ] + s r c = [ " ' ] ( [ ^ " ' ] + ) [ " ' ] [ ^ > ] * > / gi;
80- let match : RegExpExecArray | null ;
81- while ( ( match = regex . exec ( html ) ) ) {
82- let src = match [ 1 ] ;
83- if ( src . startsWith ( "//" ) ) src = `https:${ src } ` ;
84- if ( src . startsWith ( "/" ) ) src = `${ BASE_URL } ${ src } ` ;
85- if ( src . startsWith ( "http" ) ) urls . add ( src ) ;
86- }
87- return [ ...urls ] ;
88- }
89-
90- function findCandidates ( text : string ) : string [ ] {
91- const candidates = new Set < string > ( ) ;
92-
93- const headerRegex = / x - a p i k e y \s * [ ' " ] ? \s * [: = ] \s * [ ' " ] ( [ ^ ' " ] { 10 , } ) [ ' " ] / gi;
94- let match : RegExpExecArray | null ;
95- while ( ( match = headerRegex . exec ( text ) ) ) {
96- candidates . add ( match [ 1 ] ) ;
97- }
98-
99- const apiKeyRegex = / a p i K e y \s * [: = ] \s * [ ' " ] ( [ ^ ' " ] { 10 , } ) [ ' " ] / gi;
100- while ( ( match = apiKeyRegex . exec ( text ) ) ) {
101- candidates . add ( match [ 1 ] ) ;
102- }
103-
104- const base64Regex = / [ A - Z a - z 0 - 9 + / ] { 40 , 80 } = { 0 , 2 } / g;
105- while ( ( match = base64Regex . exec ( text ) ) ) {
106- const value = match [ 0 ] ;
107- if ( value . length >= 40 && value . length <= 60 && value . includes ( "=" ) ) {
108- candidates . add ( value ) ;
109- }
110- }
111-
112- return [ ...candidates ] ;
113- }
114-
115- async function validateKey ( apiKey : string ) : Promise < boolean > {
116- const url = `${ API_BASE } /offers/search?as=web&q=test&limit=1&zipCode=${ DEFAULT_ZIP_CODE } ` ;
117- const controller = new AbortController ( ) ;
118- const timeout = setTimeout ( ( ) => controller . abort ( ) , 15000 ) ;
119- try {
120- const res = await fetch ( url , {
121- headers : {
122- "x-apikey" : apiKey ,
123- "accept" : "application/json" ,
124- } ,
125- signal : controller . signal ,
126- } ) ;
127- return res . ok ;
128- } finally {
129- clearTimeout ( timeout ) ;
130- }
131- }
132-
13325export async function login ( options : LoginOptions ) : Promise < void > {
13426 const json = options . json ?? false ;
13527 const log = ( msg : string ) => ! json && console . log ( msg ) ;
13628
13729 log ( "Extracting Marktguru API key (HTTP-only)...\n" ) ;
13830
13931 try {
140- const headers = await maybeGetHeaders ( ) ;
141-
142- const entryUrls = [
143- `${ BASE_URL } /` ,
144- `${ BASE_URL } /search` ,
145- `${ BASE_URL } /search?q=test` ,
146- `${ BASE_URL } /suche` ,
147- `${ BASE_URL } /suche?q=test` ,
148- ] ;
149-
150- log ( "→ Fetching entry HTML..." ) ;
151- const { url : entryUrl , text : html } = await fetchFirstOk ( entryUrls , headers ) ;
152- log ( `✓ Using entry URL: ${ entryUrl } ` ) ;
153-
154- const candidates = new Set ( findCandidates ( html ) ) ;
155-
156- const scripts = extractScriptUrls ( html ) . slice ( 0 , MAX_SCRIPTS ) ;
157- if ( scripts . length === 0 ) {
158- throw new Error ( "No scripts found to scan for API keys." ) ;
159- }
160-
161- log ( `→ Scanning ${ scripts . length } script(s)...` ) ;
162- for ( const scriptUrl of scripts ) {
163- try {
164- const text = await fetchText ( scriptUrl , headers ) ;
165- for ( const candidate of findCandidates ( text ) ) {
166- candidates . add ( candidate ) ;
167- }
168- } catch {
169- // Ignore script fetch failures
170- }
171- }
172-
173- for ( const candidate of candidates ) {
174- if ( await validateKey ( candidate ) ) {
175- await saveConfig ( { apiKey : candidate } ) ;
176- output ( { success : true , apiKey : candidate } , json ) ;
177- return ;
178- }
179- }
180-
181- output (
182- {
183- success : false ,
184- error : "Failed to capture a valid API key. The site may have changed." ,
185- } ,
186- json
187- ) ;
188- process . exit ( 1 ) ;
32+ const apiKey = await extractApiKey ( { log : json ? undefined : log } ) ;
33+ await saveConfig ( { apiKey } ) ;
34+ output ( { success : true , apiKey } , json ) ;
18935 } catch ( e ) {
19036 output ( { success : false , error : ( e as Error ) . message } , json ) ;
19137 process . exit ( 1 ) ;
0 commit comments