@builtbyecho/public-api-finder 0.5.7 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/eval-hardening.mjs +1 -1
- package/scripts/eval-mutations.mjs +56 -0
- package/src/cli.js +61 -8
package/package.json
CHANGED
|
@@ -45,7 +45,7 @@ const cases = [
|
|
|
45
45
|
{ q: 'food barcode ingredients allergens nutrition no auth cors', args: ['--no-auth','--https'], topCategory: /food/i, mustName: /open food facts/i },
|
|
46
46
|
{ q: 'recipe from pantry ingredients avoid allergens api', args: [], topCategory: /food/i, mustName: /spoonacular|edamam|recipe|meal/i },
|
|
47
47
|
{ q: 'air quality by coordinates pollutant measurements', args: [], topCategory: /environment|science|weather/i, mustName: /openaq|air quality|aqicn/i },
|
|
48
|
-
{ q: 'historical currency exchange rates no auth cors', args: ['--no-auth','--https'], topCategory: /currency exchange/i, mustName: /frankfurter|currency/i, forbid:
|
|
48
|
+
{ q: 'historical currency exchange rates no auth cors', args: ['--no-auth','--https'], topCategory: /currency exchange/i, mustName: /frankfurter|currency/i, forbid: /\b(crypto|bitcoin|dex)\b/i },
|
|
49
49
|
{ q: 'public holidays by country no auth', args: ['--no-auth','--https'], topCategory: /calendar|date/i, mustName: /nager|holiday|calendarific/i },
|
|
50
50
|
{ q: 'vehicle vin decode recall safety data no auth', args: ['--no-auth'], topCategory: /transportation|government|open data/i, mustName: /nhtsa|vin|vehicle/i },
|
|
51
51
|
{ q: 'real estate property value rent estimate api', args: [], topCategory: /real estate|property|openapi|finance/i, mustName: /rentcast|attom|zillow|real estate|property/i },
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
const cases = [
|
|
5
|
+
{ q: 'free stock data for a frontend chart', args: ['--no-auth','--https'], expect: /finance/i, any: /stooq|portfolio|stock|finance/i, forbid: /weather|joke|anime/i },
|
|
6
|
+
{ q: 'api for checking token liquidity on dexes', args: [], expect: /cryptocurrency/i, any: /dexscreener|dexpaprika|geckoterminal|defillama/i },
|
|
7
|
+
{ q: 'browser safe weather forecast api', args: ['--no-auth','--https','--cors','Yes'], expect: /weather/i, any: /open-meteo|weather|pirate/i },
|
|
8
|
+
{ q: 'whois and dns lookup for a domain', args: [], expect: /security|development|openapi/i, any: /whois|dns|ssl/i },
|
|
9
|
+
{ q: 'send text messages to users api', args: [], expect: /communication|messaging|telecom|openapi/i, any: /twilio|sms|message|telnyx|vonage/i },
|
|
10
|
+
{ q: 'make pdf from html api', args: [], expect: /documents|development|openapi/i, any: /pdf|html/i },
|
|
11
|
+
{ q: 'find APIs for app screenshots', args: [], expect: /development|media|documents|openapi/i, any: /urlbox|microlink|screenshot|capture/i },
|
|
12
|
+
{ q: 'check if npm package has vulnerabilities', args: [], expect: /security|development|open data/i, any: /osv|nvd|vulnerability|npm/i },
|
|
13
|
+
{ q: 'lookup us census demographics by zip', args: [], expect: /government|open data|geocoding/i, any: /census|zippopotam|data.gov/i },
|
|
14
|
+
{ q: 'temporary email inbox for tests', args: ['--no-auth','--https'], expect: /email/i, any: /mail|email|inbox/i },
|
|
15
|
+
{ q: 'login users with magic link api', args: [], expect: /authentication|security|openapi/i, any: /stytch|magic|auth0|clerk|passwordless/i },
|
|
16
|
+
{ q: 'mock webhooks during development', args: [], expect: /development|test data|openapi/i, any: /webhook|beeceptor|requestbin|mock/i },
|
|
17
|
+
{ q: 'public holidays calendar for germany', args: ['--no-auth','--https'], expect: /calendar|date/i, any: /nager|holiday|calendar/i },
|
|
18
|
+
{ q: 'random user data for frontend seed demo', args: ['--no-auth','--https'], expect: /test data/i, any: /random user|jsonplaceholder|dummyjson|fake/i },
|
|
19
|
+
{ q: 'recipe nutrition search by ingredients', args: [], expect: /food/i, any: /spoonacular|edamam|open food|recipe|nutrition/i },
|
|
20
|
+
{ q: 'flight arrivals by airport code', args: [], expect: /travel|transportation|openapi/i, any: /aviation|flight|airport|amadeus/i },
|
|
21
|
+
{ q: 'rent estimate property valuation api', args: [], expect: /real estate|property|finance|openapi/i, any: /rentcast|attom|property|real estate/i },
|
|
22
|
+
{ q: 'container image tags registry api', args: [], expect: /development|security|openapi/i, any: /docker|registry|container/i },
|
|
23
|
+
{ q: 'open graph card preview for url', args: [], expect: /development|media|utility|openapi/i, any: /microlink|open graph|link preview|metadata/i },
|
|
24
|
+
{ q: 'speech transcription from uploaded audio', args: [], expect: /ai|audio|machine learning|openapi/i, any: /assemblyai|deepgram|whisper|transcription/i },
|
|
25
|
+
// Over-filter / tight filter checks: should not pad unrelated junk.
|
|
26
|
+
{ q: 'stock quote no auth cors yes', args: ['--no-auth','--https','--cors','Yes'], any: /portfolio|stooq|stock|finance/i, forbid: /weather|joke|tvmaze|anime|jobs/i, max: 5, allowEmpty: true },
|
|
27
|
+
{ q: 'crypto exchange orderbook no auth cors yes', args: ['--no-auth','--https','--cors','Yes'], expect: /cryptocurrency/i, any: /0x|coinpaprika|dexpaprika|dex|crypto/i, forbid: /weather|stock|joke/i, max: 5 },
|
|
28
|
+
{ q: 'oauth login no auth cors yes', args: ['--no-auth','--https','--cors','Yes'], forbid: /weather|joke|food|crypto|stock|calendar|holiday/i, max: 5, allowEmpty: true },
|
|
29
|
+
{ q: 'webhook testing no auth cors yes', args: ['--no-auth','--https','--cors','Yes'], any: /webhook|beeceptor|jsonplaceholder|mock/i, forbid: /weather|crypto|stock/i, max: 5 },
|
|
30
|
+
{ q: 'medical diagnosis api no auth cors yes', args: ['--no-auth','--https','--cors','Yes'], forbid: /weather|crypto|stock|joke|tvmaze|anime|email|animals|food/i, max: 5, allowEmpty: true },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function checkRows(rows, c) {
|
|
34
|
+
const checks = [];
|
|
35
|
+
const top = rows[0];
|
|
36
|
+
if (!top && !c.allowEmpty) checks.push('no results');
|
|
37
|
+
if (top && c.expect && !c.expect.test(top.category || '')) checks.push(`top category ${top.category} !~ ${c.expect}`);
|
|
38
|
+
if (c.any && !(c.allowEmpty && rows.length === 0) && !rows.slice(0, 3).some(r => c.any.test(`${r.name} ${r.category} ${r.description} ${r.url}`))) checks.push(`top3 missing ${c.any}`);
|
|
39
|
+
if (c.forbid && rows.some(r => c.forbid.test(`${r.name} ${r.category} ${r.description}`))) checks.push(`forbidden ${c.forbid}`);
|
|
40
|
+
if (c.max && rows.length > c.max) checks.push(`too many rows ${rows.length} > ${c.max}`);
|
|
41
|
+
return checks;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let failures = 0;
|
|
45
|
+
for (const [i, c] of cases.entries()) {
|
|
46
|
+
const res = spawnSync(process.execPath, ['src/cli.js', c.q, ...c.args, '--limit', String(c.max || 5), '--json'], { encoding: 'utf8' });
|
|
47
|
+
const rows = res.status === 0 ? JSON.parse(res.stdout || '[]') : [];
|
|
48
|
+
const checks = checkRows(rows, c);
|
|
49
|
+
const ok = checks.length === 0;
|
|
50
|
+
if (!ok) failures++;
|
|
51
|
+
console.log(`${ok ? 'PASS' : 'FAIL'} ${i + 1}. ${c.q}`);
|
|
52
|
+
rows.slice(0, 3).forEach((r, idx) => console.log(` ${idx + 1}. ${r.name} | ${r.category} | auth=${r.auth} | cors=${r.cors} | score=${r.score}`));
|
|
53
|
+
if (!ok) console.log(` Reasons: ${checks.join('; ')}`);
|
|
54
|
+
}
|
|
55
|
+
console.log(`\n${cases.length - failures}/${cases.length} mutation cases passed`);
|
|
56
|
+
process.exitCode = failures ? 1 : 0;
|
package/src/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ const SOURCES = {
|
|
|
13
13
|
};
|
|
14
14
|
const CACHE_PATH = process.env.PUBLIC_API_FINDER_CACHE || join(homedir(), '.cache', 'public-api-finder', 'all.json');
|
|
15
15
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
16
|
-
const DATA_VERSION =
|
|
16
|
+
const DATA_VERSION = 17;
|
|
17
17
|
|
|
18
18
|
const ENRICHMENT_FIELDS = [
|
|
19
19
|
'tags',
|
|
@@ -747,7 +747,17 @@ function applyQueryHints(args) {
|
|
|
747
747
|
if (!args.cors && /\b(cors|frontend-safe|browser-safe|frontend safe|browser safe)\b/.test(query)) args.cors = 'Yes';
|
|
748
748
|
}
|
|
749
749
|
|
|
750
|
-
const SEARCH_STOPWORDS = new Set(['a', 'an', 'and', 'api', 'apis', 'for', 'from', 'in', 'no', 'of', 'on', 'or', 'the', 'to', 'with']);
|
|
750
|
+
const SEARCH_STOPWORDS = new Set(['a', 'an', 'and', 'api', 'apis', 'auth', 'cors', 'for', 'from', 'in', 'key', 'no', 'of', 'on', 'or', 'the', 'to', 'with', 'yes']);
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
function normalizeSearchQuery(query) {
|
|
754
|
+
return String(query || '')
|
|
755
|
+
.replace(/\b(no auth|without auth|no api key|no key|unauthenticated)\b/gi, ' ')
|
|
756
|
+
.replace(/\b(cors|cors yes|frontend-safe|browser-safe|frontend safe|browser safe|https)\b/gi, ' ')
|
|
757
|
+
.replace(/\b(api|apis)\b/gi, ' ')
|
|
758
|
+
.replace(/\s+/g, ' ')
|
|
759
|
+
.trim();
|
|
760
|
+
}
|
|
751
761
|
|
|
752
762
|
function tokenSet(text) {
|
|
753
763
|
return new Set(String(text).toLowerCase().match(/[a-z0-9]+/g)?.filter(t => t.length > 1 && !SEARCH_STOPWORDS.has(t)) || []);
|
|
@@ -852,10 +862,11 @@ function intentPenalty(entry, queryText) {
|
|
|
852
862
|
if (/\b(school district|school boundary|district boundary|reverse geocoding|maps routing|routing api|open data demographics|demographics|census data)\b/.test(queryText) && /\b(linkedin|jobs scraper|lead|sales|recruiting|amazon product scraper|tiktok profile scraper)\b/.test(text)) {
|
|
853
863
|
penalty += 120;
|
|
854
864
|
}
|
|
855
|
-
|
|
865
|
+
const stockIntent = /\b(stock|stocks|stock quote|stock prices|equity|equities|ticker|tickers)\b/.test(queryText) && !/\b(not stocks?|not stock|non stocks?|non stock)\b/.test(queryText);
|
|
866
|
+
if (stockIntent && !/finance|financial|stock|stocks|equity|ticker|market data|portfolio|stooq|finnhub|polygon|alpha vantage|twelve data/.test(text)) {
|
|
856
867
|
penalty += 80;
|
|
857
868
|
}
|
|
858
|
-
if (
|
|
869
|
+
if (stockIntent && /\b(quotable|joke|entertainment|food|weather|test data|placeholder)\b/.test(text)) {
|
|
859
870
|
penalty += 140;
|
|
860
871
|
}
|
|
861
872
|
if (/\bcrypto\b/.test(queryText) && /\bnot stocks?\b/.test(queryText) && /\b(finance|financial|stock|stocks|equity|ticker|portfolio|stooq|finnhub|polygon|sec edgar|predscope|valueray|econdb)\b/.test(text)) {
|
|
@@ -880,6 +891,16 @@ function intentPenalty(entry, queryText) {
|
|
|
880
891
|
penalty += 70;
|
|
881
892
|
}
|
|
882
893
|
|
|
894
|
+
if (/\b(crypto|cryptocurrency|dex|orderbook)\b/.test(queryText) && /\b(currency exchange|frankfurter|national bank|vatcomply|exchange rates only|fiat)\b/.test(text) && !/\b(crypto|cryptocurrency|dex|token|blockchain|defi|coinpaprika|0x)\b/.test(text)) {
|
|
895
|
+
penalty += 160;
|
|
896
|
+
}
|
|
897
|
+
if (/\b(oauth|login|auth|authentication|passwordless)\b/.test(queryText) && /\b(calendar|holiday|nameday|nager|non-working days|food|weather|crypto|stock)\b/.test(text) && !/\b(auth|oauth|openid|login|identity|passwordless)\b/.test(text)) {
|
|
898
|
+
penalty += 140;
|
|
899
|
+
}
|
|
900
|
+
if (/\b(medical diagnosis|diagnosis|clinical|symptom checker)\b/.test(queryText) && /\b(food|crypto|currency|weather|joke|tv|geocoding|zippopotam)\b/.test(text) && !/\b(medical|health|clinical|diagnosis|symptom|fhir)\b/.test(text)) {
|
|
901
|
+
penalty += 180;
|
|
902
|
+
}
|
|
903
|
+
|
|
883
904
|
if (!cat.includes('cryptocurrency') && /\b(wallet address|identicon|avatar|profile picture)\b/.test(queryText) && /\b(avatar|identicon|profile picture)\b/.test(text)) {
|
|
884
905
|
penalty -= 20;
|
|
885
906
|
}
|
|
@@ -1137,8 +1158,38 @@ function sourceMatches(entry, source) {
|
|
|
1137
1158
|
return (entry.sources || [entry.source]).some(s => String(s).toLowerCase() === source.toLowerCase());
|
|
1138
1159
|
}
|
|
1139
1160
|
|
|
1161
|
+
|
|
1162
|
+
function passesIntentGate(entry, queryText) {
|
|
1163
|
+
const text = `${entry.name || ''} ${entry.category || ''} ${entry.description || ''} ${enrichedText(entry)}`.toLowerCase();
|
|
1164
|
+
if (/\b(currency exchange|exchange rates|fiat exchange|forex rates)\b/.test(queryText) && !/\bcrypto|bitcoin|dex|token\b/.test(queryText)) {
|
|
1165
|
+
if (/\bcurrency exchange\b/.test(String(entry.category || '').toLowerCase())) return true;
|
|
1166
|
+
if (/\b(frankfurter|national bank|vatcomply)\b/.test(String(entry.name || '').toLowerCase())) return true;
|
|
1167
|
+
if (/\b(crypto|crypto-currencies|cryptocurrency|stocks?|portfolio optimizer|econdb)\b/.test(text)) return false;
|
|
1168
|
+
return /\b(currency conversion|forex rates)\b/.test(text);
|
|
1169
|
+
}
|
|
1170
|
+
if (/\b(stock|stocks|stock quote|stock prices|equity|equities|ticker|tickers)\b/.test(queryText) && !/\b(not stocks?|not stock|non stocks?|non stock)\b/.test(queryText)) {
|
|
1171
|
+
return /\b(finance|financial|currency exchange)\b/.test(String(entry.category || '').toLowerCase())
|
|
1172
|
+
|| /\b(stooq|portfolio optimizer|alpha vantage|finnhub|polygon|twelve data|sec edgar|econdb|tradier|predscope|valueray)\b/.test(text);
|
|
1173
|
+
}
|
|
1174
|
+
if (/\b(oauth|openid|passwordless|magic link)\b/.test(queryText) || /\blogin\b.*\b(auth|users?)\b/.test(queryText)) {
|
|
1175
|
+
return /\b(authentication|security)\b/.test(String(entry.category || '').toLowerCase())
|
|
1176
|
+
|| /\b(auth0|clerk|stytch|magic|oauth|openid|identity|passwordless|social auth|login)\b/.test(text);
|
|
1177
|
+
}
|
|
1178
|
+
if (/\b(medical diagnosis|diagnosis|clinical|symptom checker)\b/.test(queryText)) {
|
|
1179
|
+
return /\b(medical|health|clinical|diagnosis|symptom|fhir)\b/.test(text);
|
|
1180
|
+
}
|
|
1181
|
+
if (/\b(crypto|cryptocurrency|dex|orderbook)\b/.test(queryText) && !/\b(not crypto|non crypto|fiat only)\b/.test(queryText)) {
|
|
1182
|
+
if (!/\bcryptocurrency\b/.test(String(entry.category || '').toLowerCase()) && /\b(finance|financial|currency exchange)\b/.test(String(entry.category || '').toLowerCase())) return false;
|
|
1183
|
+
return /\b(crypto|cryptocurrency|dex|defi|token|blockchain|coin|coinpaprika|0x|dexpaprika|geckoterminal|dexscreener)\b/.test(text);
|
|
1184
|
+
}
|
|
1185
|
+
return true;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1140
1188
|
function filterEntries(entries, args) {
|
|
1141
|
-
const
|
|
1189
|
+
const queryText = String(args.query || '').toLowerCase();
|
|
1190
|
+
const searchText = normalizeSearchQuery(args.query);
|
|
1191
|
+
const q = tokenSet(searchText);
|
|
1192
|
+
if (args.noAuth && (/\b(oauth|openid|passwordless|magic link)\b/.test(queryText) || /\blogin\b.*\b(auth|users?)\b/.test(queryText))) return [];
|
|
1142
1193
|
return entries.flatMap(e => {
|
|
1143
1194
|
if (args.category && !String(e.category || '').toLowerCase().includes(args.category.toLowerCase())) return [];
|
|
1144
1195
|
if (args.source && !sourceMatches(e, args.source)) return [];
|
|
@@ -1146,13 +1197,15 @@ function filterEntries(entries, args) {
|
|
|
1146
1197
|
if (args.https && !e.https) return [];
|
|
1147
1198
|
if (args.openapi && !e.openapiUrl) return [];
|
|
1148
1199
|
if (args.cors && String(e.cors || '').toLowerCase() !== args.cors.toLowerCase()) return [];
|
|
1200
|
+
if (q.size && !passesIntentGate(e, queryText)) return [];
|
|
1149
1201
|
const matched = q.size ? textScore(e, q) : 1;
|
|
1150
1202
|
const domain = q.size ? domainAdjustment(e, q) : 0;
|
|
1151
|
-
const targeted = q.size ? targetedBoost(e,
|
|
1203
|
+
const targeted = q.size ? targetedBoost(e, queryText) : 0;
|
|
1152
1204
|
if (q.size && matched === 0 && domain <= 0 && targeted <= 0) return [];
|
|
1153
|
-
|
|
1205
|
+
let s = q.size ? score(e, q, searchText.toLowerCase()) : 1;
|
|
1154
1206
|
if (q.size && s <= 0) return [];
|
|
1155
|
-
|
|
1207
|
+
const finalScore = s + (e.sourceWeight || 0);
|
|
1208
|
+
return [{ ...e, score: finalScore }];
|
|
1156
1209
|
}).sort((a, b) => b.score - a.score || String(a.category).localeCompare(String(b.category)) || String(a.name).localeCompare(String(b.name))).slice(0, args.limit);
|
|
1157
1210
|
}
|
|
1158
1211
|
|