Files
hanzo-dev 675717ac94 carry our own name everywhere but the attribution
The fork still wore upstream's brand throughout: "World Monitor" in 754 places
of prose and metadata, WORLD MONITOR in the blog logo and footer, and a byline
reading "by Someone.ceo" — another company's name signing our pages. Blog
assets, OG images and post directories were named for it too, and the OpenAPI
schema types carried it in every identifier.

Files are renamed, references follow them, and the localStorage and slug names
match. Deleted .claudedocs/sentry-triage, an AI-written summary file of the kind
that belongs in LLM.md if anywhere.

NOTICE and LICENSE are untouched by design. NOTICE names the upstream project,
the fork point and the original author; LICENSE keeps his copyright line, which
MIT requires. That is where credit belongs — not in our product's chrome.
2026-08-20 16:13:20 -07:00

146 lines
5.4 KiB
JavaScript

// World Bank API proxy (Web API handler for Edge + sidecar compatibility)
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
export const config = {
runtime: 'edge',
};
const TECH_INDICATORS = {
'IT.NET.USER.ZS': 'Internet Users (% of population)',
'IT.CEL.SETS.P2': 'Mobile Subscriptions (per 100 people)',
'IT.NET.BBND.P2': 'Fixed Broadband Subscriptions (per 100 people)',
'IT.NET.SECR.P6': 'Secure Internet Servers (per million people)',
'GB.XPD.RSDV.GD.ZS': 'R&D Expenditure (% of GDP)',
'IP.PAT.RESD': 'Patent Applications (residents)',
'IP.PAT.NRES': 'Patent Applications (non-residents)',
'IP.TMK.TOTL': 'Trademark Applications',
'TX.VAL.TECH.MF.ZS': 'High-Tech Exports (% of manufactured exports)',
'BX.GSR.CCIS.ZS': 'ICT Service Exports (% of service exports)',
'TM.VAL.ICTG.ZS.UN': 'ICT Goods Imports (% of total goods imports)',
'SE.TER.ENRR': 'Tertiary Education Enrollment (%)',
'SE.XPD.TOTL.GD.ZS': 'Education Expenditure (% of GDP)',
'NY.GDP.MKTP.KD.ZG': 'GDP Growth (annual %)',
'NY.GDP.PCAP.CD': 'GDP per Capita (current US$)',
'NE.EXP.GNFS.ZS': 'Exports of Goods & Services (% of GDP)',
};
const TECH_COUNTRIES = [
'USA', 'CHN', 'JPN', 'DEU', 'KOR', 'GBR', 'IND', 'ISR', 'SGP', 'TWN',
'FRA', 'CAN', 'SWE', 'NLD', 'CHE', 'FIN', 'IRL', 'AUS', 'BRA', 'IDN',
'ARE', 'SAU', 'QAT', 'BHR', 'EGY', 'TUR',
'MYS', 'THA', 'VNM', 'PHL',
'ESP', 'ITA', 'POL', 'CZE', 'DNK', 'NOR', 'AUT', 'BEL', 'PRT', 'EST',
'MEX', 'ARG', 'CHL', 'COL',
'ZAF', 'NGA', 'KEN',
];
export default async function handler(request) {
const CORS = getCorsHeaders(request);
if (isDisallowedOrigin(request)) {
return new Response(JSON.stringify({ error: 'Origin not allowed' }), { status: 403, headers: CORS });
}
function json(data, status = 200, extra = {}) {
return new Response(JSON.stringify(data), {
status,
headers: { 'Content-Type': 'application/json', ...CORS, ...extra },
});
}
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: CORS });
}
const url = new URL(request.url);
const indicator = url.searchParams.get('indicator');
const country = url.searchParams.get('country');
const countries = url.searchParams.get('countries');
const years = url.searchParams.get('years') || '5';
const action = url.searchParams.get('action');
if (action === 'indicators') {
return json({ indicators: TECH_INDICATORS, defaultCountries: TECH_COUNTRIES }, 200, { 'Cache-Control': 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=3600' });
}
if (!indicator) {
return json({ error: 'Missing indicator parameter', availableIndicators: Object.keys(TECH_INDICATORS) }, 400);
}
try {
let countryList = country || countries || TECH_COUNTRIES.join(';');
if (countries) {
countryList = countries.split(',').join(';');
}
const currentYear = new Date().getFullYear();
const startYear = currentYear - parseInt(years);
const wbUrl = `https://api.worldbank.org/v2/country/${countryList}/indicator/${indicator}?format=json&date=${startYear}:${currentYear}&per_page=1000`;
const response = await fetch(wbUrl, {
headers: {
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0 (compatible; HanzoWorld/1.0; +https://world.hanzo.ai)',
},
});
if (!response.ok) {
throw new Error(`World Bank API error: ${response.status}`);
}
const data = await response.json();
if (!data || !Array.isArray(data) || data.length < 2 || !data[1]) {
return json({
indicator,
indicatorName: TECH_INDICATORS[indicator] || indicator,
metadata: { page: 1, pages: 1, total: 0 },
byCountry: {},
latestByCountry: {},
timeSeries: [],
}, 200, { 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' });
}
const [metadata, records] = data;
const transformed = {
indicator,
indicatorName: TECH_INDICATORS[indicator] || (records[0]?.indicator?.value || indicator),
metadata: { page: metadata.page, pages: metadata.pages, total: metadata.total },
byCountry: {},
latestByCountry: {},
timeSeries: [],
};
for (const record of records || []) {
const countryCode = record.countryiso3code || record.country?.id;
const countryName = record.country?.value;
const year = record.date;
const value = record.value;
if (!countryCode || value === null) continue;
if (!transformed.byCountry[countryCode]) {
transformed.byCountry[countryCode] = { code: countryCode, name: countryName, values: [] };
}
transformed.byCountry[countryCode].values.push({ year, value });
if (!transformed.latestByCountry[countryCode] || year > transformed.latestByCountry[countryCode].year) {
transformed.latestByCountry[countryCode] = { code: countryCode, name: countryName, year, value };
}
transformed.timeSeries.push({ countryCode, countryName, year, value });
}
for (const c of Object.values(transformed.byCountry)) {
c.values.sort((a, b) => a.year - b.year);
}
transformed.timeSeries.sort((a, b) => b.year - a.year || a.countryCode.localeCompare(b.countryCode));
return json(transformed, 200, { 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600' });
} catch (error) {
return json({ error: error.message, indicator }, 500);
}
}