1// Public value normalization and item-reference links follow the ownerâs Vault inspection. 2const VALUE_PRICE = /^\$[0-9]{1,7}(?:,[0-9]{3})*(?:\.[0-9]{2})?$/; 3const VALUE_URL = /^https:\/\/[A-Za-z0-9.-]+(?::[0-9]+)?(?:\/[^\s]*)?$/; 4const JET_DOT_COM = /(?:^|[^a-z0-9])jet\.com(?:[^a-z0-9]|$)/i; 5const REFERENCE_ONLY_MARKETPLACES = new Set(['ebay', 'ebay.com', 'mercari', 'mercari.com']); 6 7function normalizeGtin(value) { 8 if (typeof value !== 'string' || !/^(?:[0-9]{8}|[0-9]{12,14})$/.test(value.trim())) return null; 9 const gtin = value.trim(); 10 const digits = [...gtin].map(Number); 11 const checkDigit = digits.pop(); 12 let sum = 0; 13 let weight = 3; 14 for (let index = digits.length - 1; index >= 0; index -= 1) { 15 sum += digits[index] * weight; 16 weight = weight === 3 ? 1 : 3; 17 } 18 return (10 - (sum % 10)) % 10 === checkDigit ? gtin : null; 19} 20 21function isJetMerchant(source, url) { 22 const normalizedSource = source.trim().toLowerCase(); 23 if (normalizedSource === 'jet' || normalizedSource === 'jet.com' || JET_DOT_COM.test(normalizedSource)) return true; 24 if (!url) return false; 25 try { 26 const host = new URL(url).hostname.toLowerCase(); 27 return host === 'jet.com' || host.endsWith('.jet.com'); 28 } catch { 29 return false; 30 } 31} 32 33function isReferenceOnlyMerchant(source, url) { 34 if (REFERENCE_ONLY_MARKETPLACES.has(source.trim().toLowerCase())) return true; 35 if (!url) return false; 36 try { 37 const host = new URL(url).hostname.toLowerCase(); 38 return host === 'ebay.com' || host.endsWith('.ebay.com') || host === 'mercari.com' || host.endsWith('.mercari.com'); 39 } catch { 40 return false; 41 } 42} 43 44function itemSourceLinks(value) { 45 const gtin = normalizeGtin(value); 46 if (gtin === null) return Object.freeze([]); 47 const encoded = encodeURIComponent(gtin); 48 return Object.freeze([ 49 { source: 'Barcode Lookup', url: `https://www.barcodelookup.com/${encoded}` }, 50 { source: 'Barcode Spider', url: `https://www.barcodespider.com/${encoded}` }, 51 { source: "Brian's Toys", url: `https://www.brianstoys.com/catalogsearch/result/?q=${encoded}` }, 52 { source: 'eBay', url: `https://www.ebay.com/sch/i.html?_nkw=${encoded}&_sop=16` }, 53 { source: 'Google Images', url: `https://www.google.com/search?q=${encoded}&source=lnms&sa=X&udm=2` }, 54 { source: 'Mercari', url: `https://www.mercari.com/search/?keyword=${encoded}` }, 55 ].sort((left, right) => left.source.localeCompare(right.source, 'en', { sensitivity: 'base' })) 56 .map((link) => Object.freeze(link))); 57} 58 59function normalizeValueInformation(payload) { 60 if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null; 61 const allowedRoot = new Set(['schema_version', 'checked_at', 'offers']); 62 if (Object.keys(payload).some((key) => !allowedRoot.has(key)) || !Array.isArray(payload.offers)) return null; 63 const offers = []; 64 for (const candidate of payload.offers.slice(0, 24)) { 65 if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) continue; 66 const allowedOffer = new Set(['source', 'title', 'price', 'currency', 'url']); 67 if (Object.keys(candidate).some((key) => !allowedOffer.has(key))) continue; 68 const source = typeof candidate.source === 'string' ? candidate.source.trim().slice(0, 120) : ''; 69 const title = typeof candidate.title === 'string' ? candidate.title.trim().slice(0, 240) : ''; 70 const price = typeof candidate.price === 'string' ? candidate.price.trim() : ''; 71 const currency = candidate.currency === 'USD' ? 'USD' : null; 72 const url = typeof candidate.url === 'string' && VALUE_URL.test(candidate.url) ? candidate.url : null; 73 if (!source || !title || !VALUE_PRICE.test(price) || currency === null 74 || isJetMerchant(source, url) || isReferenceOnlyMerchant(source, url)) continue; 75 offers.push(Object.freeze({ source, title, price, currency, url })); 76 } 77 if (offers.length === 0) return null; 78 offers.sort((left, right) => left.source.localeCompare(right.source, 'en', { sensitivity: 'base' }) 79 || left.title.localeCompare(right.title, 'en', { sensitivity: 'base' }) 80 || left.price.localeCompare(right.price, 'en', { numeric: true })); 81 const checkedAt = typeof payload.checked_at === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(payload.checked_at) 82 ? payload.checked_at 83 : null; 84 return Object.freeze({ checkedAt, offers: Object.freeze(offers) }); 85} 86 87export {normalizeGtin,itemSourceLinks,normalizeValueInformation};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.