import { html as H, type ConnectorMeta } from '@rareindex/connectors';
import { parseGradeFromTitle } from '@rareindex/taxonomy';
import type { AssetAttributes, CurrencyCode, NormalizedAuctionLot, NormalizedCatalogItem, NormalizedListing } from '@rareindex/shared';
import { lotAttributes, money } from '../../firecrawl/_carlib/index.js';
/**
* Helpers shared by the memorabilia / toys / dealer connectors (SCP, Hake's, Morphy, Lelands, BBTS,
* Noble Knight, Trainz). Source vocabulary stays here; canonical models stay clean.
*/
export const BOT_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) RareIndexBot/0.1 (+https://www.rareindex.io/about; market-data research)';
// ---------------------------------------------------------------------------------------------
// Plain HTTP helpers for sites whose paging/selection is an ASP.NET postback (what a browser does:
// a form POST carrying the page's own hidden fields; no authentication involved).
// ---------------------------------------------------------------------------------------------
export interface HttpResult {
status: number;
text: string;
cookies: string[];
}
export async function httpText(url: string, opts: { method?: 'GET' | 'POST'; form?: Record; cookie?: string; referer?: string; timeoutMs?: number } = {}): Promise {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 45_000);
try {
const headers: Record = { 'user-agent': BOT_UA, accept: 'text/html,application/xhtml+xml,*/*;q=0.8', 'accept-language': 'en-US,en;q=0.9' };
if (opts.cookie) headers.cookie = opts.cookie;
if (opts.referer) headers.referer = opts.referer;
let body: string | undefined;
if (opts.method === 'POST') {
headers['content-type'] = 'application/x-www-form-urlencoded';
body = new URLSearchParams(opts.form ?? {}).toString();
}
const res = await fetch(url, { method: opts.method ?? 'GET', headers, body, signal: ctrl.signal, redirect: 'follow' });
const text = await res.text();
const cookies: string[] = [];
const anyHeaders = res.headers as Headers & { getSetCookie?: () => string[] };
if (typeof anyHeaders.getSetCookie === 'function') cookies.push(...anyHeaders.getSetCookie());
else {
const sc = res.headers.get('set-cookie');
if (sc) cookies.push(sc);
}
return { status: res.status, text, cookies };
} finally {
clearTimeout(timer);
}
}
/** Cookie header from Set-Cookie lines (name=value only). */
export function cookieHeader(setCookies: string[], previous = ''): string {
const jar = new Map();
for (const part of previous.split(';')) {
const [k, ...v] = part.trim().split('=');
if (k) jar.set(k, v.join('='));
}
for (const sc of setCookies) {
const first = sc.split(';')[0] ?? '';
const [k, ...v] = first.trim().split('=');
if (k) jar.set(k, v.join('='));
}
return [...jar.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
}
/** ASP.NET hidden inputs (__VIEWSTATE, __EVENTVALIDATION, …) plus any other hidden fields. */
export function hiddenFields(htmlText: string): Record {
const out: Record = {};
const re = /]+type="hidden"[^>]*>/gi;
let m: RegExpExecArray | null;
while ((m = re.exec(htmlText))) {
const tag = m[0];
const name = tag.match(/name="([^"]+)"/)?.[1];
if (!name) continue;
const value = tag.match(/value="([^"]*)"/)?.[1] ?? '';
out[name] = value.replace(/"/g, '"').replace(/&/g, '&');
}
return out;
}
/** Selected value of a