// HaulProof API client for Node 18+ (no packages). Usage: HP_KEY=hp_test_... node haulproof.mjs // Shows: tiers for a list, a full record, a bulk watchlist, the nightly tier file (download + parse), a webhook + test. import { createGunzip } from "node:zlib"; import { Readable } from "node:stream"; import { createInterface } from "node:readline"; const BASE = process.env.HP_BASE ?? "https://haulproof.ca"; const KEY = process.env.HP_KEY; if (!KEY) throw new Error("set HP_KEY"); async function api(path, init = {}) { for (let attempt = 0; ; attempt++) { const res = await fetch(BASE + path, { ...init, headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json", ...init.headers } }); if (res.status === 429 && attempt < 3) { await new Promise((r) => setTimeout(r, 1000 * Number(res.headers.get("retry-after") ?? 1))); continue; } const body = res.headers.get("content-type")?.includes("json") ? await res.json() : await res.text(); if (!res.ok) throw new Error(`${res.status} ${JSON.stringify(body)}`); return { body, res }; } } /** Tiers for any number of identifiers, 100 per call. */ export async function tiers(ids) { const out = []; for (let i = 0; i < ids.length; i += 100) out.push(...(await api(`/v1/tiers?ids=${encodeURIComponent(ids.slice(i, i + 100).join(","))}`)).body.data); return out; } /** Minimal RFC 4180 CSV line parser (quoted fields, doubled quotes). */ function parseCsvLine(line) { const out = []; let cur = "", q = false; for (let i = 0; i < line.length; i++) { const ch = line[i]; if (q) { if (ch === '"' && line[i + 1] === '"') { cur += '"'; i++; } else if (ch === '"') q = false; else cur += ch; } else if (ch === '"') q = true; else if (ch === ",") { out.push(cur); cur = ""; } else cur += ch; } out.push(cur); return out; } /** Streams the first tier file the key can see (the full file has ~2.2 million rows): calls onRow for each row as an * object and returns the row count. Only the current line is held in memory. */ export async function tierFile(onRow = () => {}) { const files = (await api("/v1/tier-files")).body.data.filter((f) => f.url); if (!files.length) return 0; const res = await fetch(files[0].url); const lines = createInterface({ input: Readable.fromWeb(res.body).pipe(createGunzip()), crlfDelay: Infinity }); let cols = null, n = 0; for await (const line of lines) { if (!line) continue; if (!cols) { cols = parseCsvLine(line); continue; } onRow(Object.fromEntries(parseCsvLine(line).map((v, i) => [cols[i], v]))); n++; } return n; } const sandbox = KEY.startsWith("hp_test_"); const ids = sandbox ? ["99990001", "MC990004", "us:99990007"] : ["264184", "MC133655", "R-048945-1"]; console.log("tiers:", (await tiers(ids)).map((t) => `${t.id} -> ${t.tier ?? t.error}`).join("; ")); const rec = await api(`/v1/carriers/${ids[0]}`); console.log("record:", rec.body.data.name, rec.body.data.tier, "cache:", rec.res.headers.get("x-hp-cache") ?? "n/a", "as_of:", rec.body.as_of ?? "n/a"); let first = null; const count = await tierFile((row) => { first ??= row; }); console.log("tier file rows:", count, first ? `first: ${first.key} ${first.name} ${first.tier}` : ""); if (!sandbox) { const w = (await api("/v1/watchlists", { method: "POST", body: JSON.stringify({ name: `node sample ${Date.now()}` }) })).body.data; console.log("bulk add:", JSON.stringify((await api(`/v1/watchlists/${w.id}/items/bulk`, { method: "POST", body: JSON.stringify({ items: [...ids, "not a carrier"] }) })).body.data)); await fetch(`${BASE}/v1/watchlists/${w.id}`, { method: "DELETE", headers: { Authorization: `Bearer ${KEY}` } }); // the sample's list only } else console.log("bulk add: needs a live key (watchlists hold real carriers)"); if (process.env.HP_WEBHOOK_URL) { const h = (await api("/v1/webhooks", { method: "POST", body: JSON.stringify({ url: process.env.HP_WEBHOOK_URL, min_severity: "warning" }) })).body.data; console.log("webhook secret (keep it):", h.secret.slice(0, 10) + "…"); console.log("test send:", JSON.stringify((await api(`/v1/webhooks/${h.id}/test`, { method: "POST" })).body.data)); await fetch(`${BASE}/v1/webhooks/${h.id}`, { method: "DELETE", headers: { Authorization: `Bearer ${KEY}` } }); }