openvibers/openvibe.tools Commits

Read maps provider keys from config, drop baked-in USDA demo key (#2)

OP OpenVibers committed 2 Oct 2026 · 1e158da931 · 4 files, +81 −51

local checks ✓ 2/2 · CI ✓ 3/3 · review SHIP by DeepSeek harness

https://github.com/OpenVibers/OpenVibe.Tools/pull/2

Changes

ddiff --git a/apps/maps/server/config.js b/apps/maps/server/config.js
iindex 4d43920..1004f38 100644
---- a/apps/maps/server/config.js
++++ b/apps/maps/server/config.js
@@@ -7,7 +7,7 @@ module.exports = {
baseUrl: process.env.BASE_URL || 'https://maps.openvibe.tools',
// API Keys (optional — free tiers work without most of these)
-- ridbApiKey: process.env.RIDB_API_KEY || '', // sources/ridb.js reads RIDB_API_KEY itself; never a key in the code
++ ridbApiKey: process.env.RIDB_API_KEY || '', // passed to sources/ridb.js; never a key in the code
npsApiKey: process.env.NPS_API_KEY || '',
openChargeMapKey: process.env.OPEN_CHARGE_MAP_KEY || '',
ddiff --git a/apps/maps/server/sources/grocery.js b/apps/maps/server/sources/grocery.js
iindex 693772e..9fe5260 100644
---- a/apps/maps/server/sources/grocery.js
++++ b/apps/maps/server/sources/grocery.js
@@@ -1024,45 +1024,6 @@ async function scrapeWalmartPrices(searchTerm) {
}
}
--// ─── USDA FoodData Central Nutrition Lookup ────────────────────────
--async function getNutritionData(foodName) {
-- try {
-- const resp = await axios.get('https://api.nal.usda.gov/fdc/v1/foods/search', {
-- params: {
-- api_key: 'DEMO_KEY',
-- query: foodName,
-- pageSize: 1,
-- dataType: 'Survey (FNDDS)',
-- },
-- timeout: 8000,
-- });
--
-- if (resp.data?.foods?.[0]) {
-- const food = resp.data.foods[0];
-- const nutrients = {};
-- for (const n of (food.foodNutrients || [])) {
-- nutrients[n.nutrientName] = { value: n.value, unit: n.unitName };
-- }
-- return {
-- description: food.description,
-- calories: nutrients['Energy']?.value || 0,
-- protein: nutrients['Protein']?.value || 0,
-- carbs: nutrients['Carbohydrate, by difference']?.value || 0,
-- fat: nutrients['Total lipid (fat)']?.value || 0,
-- fiber: nutrients['Fiber, total dietary']?.value || 0,
-- vitaminC: nutrients['Vitamin C, total ascorbic acid']?.value || 0,
-- calcium: nutrients['Calcium, Ca']?.value || 0,
-- iron: nutrients['Iron, Fe']?.value || 0,
-- potassium: nutrients['Potassium, K']?.value || 0,
-- };
-- }
-- return null;
-- } catch (err) {
-- console.warn('USDA API error:', err.message);
-- return null;
-- }
--}
--
// ─── Meal Assignments (which items fit which meals) ────────────────
const MEAL_TAGS = {
// Map food IDs to meal suitability: breakfast, lunch, dinner, snack
@@@ -1387,7 +1348,6 @@ module.exports = {
findNearbyStores,
findFoodBanks,
scrapeWalmartPrices,
-- getNutritionData,
optimizeMealPlan,
getAllFoods,
};
ddiff --git a/apps/maps/server/sources/ridb.js b/apps/maps/server/sources/ridb.js
iindex ed67ddd..1f155a3 100644
---- a/apps/maps/server/sources/ridb.js
++++ b/apps/maps/server/sources/ridb.js
@@@ -9,9 +9,8 @@
const axios = require('axios');
const { haversine } = require('./utils');
--// RIDB_API_KEY from the environment (/etc/openvibe/tools.env in production); never a key in the code.
++// The RIDB key is passed in by the caller (server/config.js); never a key in the code.
// Without one, RIDB is skipped (search answers the other sources).
--const API_KEY = process.env.RIDB_API_KEY || '';
const BASE_URL = 'https://ridb.recreation.gov/api/v1';
let _ridbDisabled = false; // Disable after first 401 to avoid duplicate errors
@@@ -27,10 +26,8 @@ const CAMPING_ACTIVITY_IDS = [
/**
* Search RIDB for facilities near lat/lon within radiusMiles
*/
--let _warnedNoKey = false;
--
--async function search(lat, lon, radiusMiles) {
-- if (!API_KEY) { if (!_warnedNoKey) { _warnedNoKey = true; console.warn('[RIDB] RIDB_API_KEY is not set: RIDB results are skipped'); } return []; }
++async function search(lat, lon, radiusMiles, apiKey) {
++ if (!apiKey) return [];
if (_ridbDisabled) return []; // Skip after auth failure
const results = [];
@@@ -46,7 +43,7 @@ async function search(lat, lon, radiusMiles) {
offset: 0,
},
headers: {
-- apikey: API_KEY,
++ apikey: apiKey,
Accept: 'application/json',
},
timeout: 15000,
@@@ -100,7 +97,7 @@ async function search(lat, lon, radiusMiles) {
limit: 25,
},
headers: {
-- apikey: API_KEY,
++ apikey: apiKey,
Accept: 'application/json',
},
timeout: 15000,
@@@ -146,9 +143,9 @@ async function search(lat, lon, radiusMiles) {
/**
* Get detailed facility info from RIDB
*/
--async function getDetail(facilityId) {
++async function getDetail(facilityId, apiKey) {
const { data } = await axios.get(`${BASE_URL}/facilities/${facilityId}`, {
-- headers: { apikey: API_KEY, Accept: 'application/json' },
++ headers: { apikey: apiKey, Accept: 'application/json' },
timeout: 10000,
});
return data;
ddiff --git a/apps/maps/test/provider-keys.test.js b/apps/maps/test/provider-keys.test.js
nnew file mode 100644
iindex 0000000..8429116
---- /dev/null
++++ b/apps/maps/test/provider-keys.test.js
@@@ -0,0 +1,73 @@
++'use strict';
++// Provider keys live in the satellite's config (apps/maps/server/config.js) and are handed to each
++// source — never read from the environment inside a source module, never a key in the code.
++// Here the transport the sources use (axios) is stubbed, so nothing touches the network:
++// 1. the RIDB key the caller passes reaches the RIDB request,
++// 2. that key never appears in the search results a caller gets back,
++// 3. with no RIDB key, RIDB sends nothing and search still answers (an empty list),
++// 4. no file under apps/ carries the USDA demo API key placeholder.
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++
++// Stub axios before the source module is loaded: both hold the same cached module object.
++const axios = require('axios');
++const requests = [];
++axios.get = async (url, opts = {}) => { requests.push({ url, opts }); return { data: {} }; };
++
++const ridb = require('../server/sources/ridb');
++
++const RIDB_KEY = 'sentinel-ridb-key-not-a-secret';
++
++(async () => {
++ let failures = 0;
++ const check = async (name, fn) => {
++ try { await fn(); console.log(` ✓ ${name}`); }
++ catch (e) { failures++; console.log(` ✗ ${name}\n ${String(e.stack || e.message).split('\n').slice(0, 8).join('\n ')}`); }
++ };
++
++ await check('the injected RIDB key reaches the RIDB request', async () => {
++ requests.length = 0;
++ await ridb.search(47.6062, -122.3321, 25, RIDB_KEY);
++ assert.ok(requests.length > 0, 'RIDB sent a request');
++ for (const r of requests) {
++ assert.ok(/recreation\.gov/.test(r.url), `request goes to RIDB: ${r.url}`);
++ assert.strictEqual(r.opts.headers && r.opts.headers.apikey, RIDB_KEY, `apikey header on ${r.url}`);
++ }
++ });
++
++ await check('the RIDB key never appears in the search results a caller gets back', async () => {
++ requests.length = 0;
++ const results = await ridb.search(47.6062, -122.3321, 25, RIDB_KEY);
++ assert.ok(Array.isArray(results), 'search answers with a list');
++ assert.ok(!JSON.stringify(results).includes(RIDB_KEY), 'the key is not in the results');
++ });
++
++ await check('with no RIDB key, RIDB sends no request and search still answers', async () => {
++ for (const key of ['', undefined, null]) {
++ requests.length = 0;
++ const results = await ridb.search(47.6062, -122.3321, 25, key);
++ assert.strictEqual(requests.length, 0, `no request sent for key ${JSON.stringify(key)}`);
++ assert.deepStrictEqual(results, [], 'search answers with an empty list');
++ }
++ });
++
++ await check('no file under apps/ carries the USDA demo API key', async () => {
++ const root = path.join(__dirname, '..', '..');
++ const needle = ['DEMO', 'KEY'].join('_'); // split so this file does not match itself
++ const hits = [];
++ (function walk(dir) {
++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
++ if (e.name === 'node_modules' || e.name === '.git') continue;
++ const p = path.join(dir, e.name);
++ if (e.isDirectory()) walk(p);
++ else if (fs.readFileSync(p).includes(needle)) hits.push(path.relative(root, p));
++ }
++ })(root);
++ assert.deepStrictEqual(hits, [], `files carrying the demo key: ${hits.join(', ')}`);
++ });
++
++ if (failures) { console.log(`\n${failures} failure(s)`); process.exit(1); }
++ console.log('\nprovider keys come from config: all checks passed');
++ process.exit(0);
++})().catch((e) => { console.error(e); process.exit(1); });