← back to Fineartamerica Price Sync

lib/faa-http.js

70 lines

// FREE, login-free FAA per-config cost fetcher (cracked 2026-07-16).
//
// FAA's framed-print configurator is a STATEFUL session: each option selection
// POSTs an `action=set*/select*` to
//   /queries/queryUpdateProductConfiguratorProductPrint.php
// which mutates the session's config and returns {status, price}. So to price
// one variant we replay its setter sequence in a single cookie session and read
// the price off the final (setPrintSize) response. No browser, no login.
//
// Proven: 20x24 CRQ13/PM918 = $195.00, 7x8 = $74.00, 34x40 = $350.00.

const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36';
const PRICE_URL = 'https://fineartamerica.com/queries/queryUpdateProductConfiguratorProductPrint.php';

function parseCookies(setCookies) {
  // node fetch exposes combined Set-Cookie via getSetCookie()
  return (setCookies || []).map(c => c.split(';')[0]).join('; ');
}

// Open a configurator session for one FAA product page; capture cookies + ids.
async function openSession(pageUrl) {
  const res = await fetch(pageUrl + (pageUrl.includes('?') ? '&' : '?') + 'product=framed-print',
    { headers: { 'User-Agent': UA } });
  const cookie = parseCookies(res.headers.getSetCookie ? res.headers.getSetCookie() : []);
  const html = await res.text();
  const g = n => {
    const m = html.match(new RegExp(`id='${n}'\\s+value='([^']*)'`)) ||
              html.match(new RegExp(`name='${n}'\\s+value='([^']*)'`));
    return m ? m[1] : '';
  };
  return { pageUrl, cookie, sessionId: g('sessionId'), uniqueId: g('uniqueId'), pageArtworkId: g('artworkId') };
}

async function post(session, params) {
  const res = await fetch(PRICE_URL, {
    method: 'POST',
    headers: {
      'User-Agent': UA,
      'X-Requested-With': 'XMLHttpRequest',
      'Content-Type': 'application/x-www-form-urlencoded',
      'Referer': session.pageUrl + '?product=framed-print',
      ...(session.cookie ? { Cookie: session.cookie } : {}),
    },
    body: `sessionId=${session.sessionId}&uniqueId=${session.uniqueId}&${params}`,
  });
  const txt = await res.text();
  try { return JSON.parse(txt); } catch { return { status: 'error', raw: txt.slice(0, 120) }; }
}

// Price one parsed framed-print config. Replays the setter sequence; returns the
// FAA fulfillment cost (number) or null on failure.
async function priceFramedConfig(session, cfg) {
  const artworkId = cfg.artworkid || session.pageArtworkId;
  const base = `productId=printframed&artworkId=${artworkId}`;
  const steps = [
    `${base}&action=selectPaper&paperId=${cfg.paperid}`,
    `${base}&action=selectFrame&frameId=${cfg.frameid}`,
    `${base}&action=selectMat1&mat1Id=${cfg.mat1id}`,
    `${base}&action=selectMatWidth&matWidth=${cfg.mat1width || 2}`,
    `${base}&action=selectFinish&finishId=${cfg.finishid}`,
    `${base}&action=setPrintSize&width=${cfg.imagewidth}&height=${cfg.imageheight}`,
  ];
  let last = null;
  for (const s of steps) last = await post(session, s);
  if (!last || last.status !== 'success' || last.price == null) return null;
  return parseFloat(last.price);
}

module.exports = { openSession, priceFramedConfig, PRICE_URL };