openvibers/openvibe.tools Commits
Read maps provider keys from config, drop baked-in USDA demo key (#2)
Changes
@@@ -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 codenpsApiKey: process.env.NPS_API_KEY || '',openChargeMapKey: process.env.OPEN_CHARGE_MAP_KEY || '',@@@ -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,};@@@ -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 failureconst 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;nnew file mode 100644@@@ -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); });
@@ -7,7 +7,7 @@ module.exports = {
@@ -7,7 +7,7 @@ module.exports = {
baseUrl: process.env.BASE_URL || 'https://maps.openvibe.tools',
baseUrl: process.env.BASE_URL || 'https://maps.openvibe.tools',
// API Keys (optional — free tiers work without most of these)
// 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 || '',
npsApiKey: process.env.NPS_API_KEY || '',
openChargeMapKey: process.env.OPEN_CHARGE_MAP_KEY || '',
openChargeMapKey: process.env.OPEN_CHARGE_MAP_KEY || '',
@@ -1024,45 +1024,6 @@ async function scrapeWalmartPrices(searchTerm) {
@@ -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) ────────────────
// ─── Meal Assignments (which items fit which meals) ────────────────
const MEAL_TAGS = {
const MEAL_TAGS = {
// Map food IDs to meal suitability: breakfast, lunch, dinner, snack
// Map food IDs to meal suitability: breakfast, lunch, dinner, snack
@@ -1387,7 +1348,6 @@ module.exports = {
@@ -1387,7 +1348,6 @@ module.exports = {
findNearbyStores,
findNearbyStores,
findFoodBanks,
findFoodBanks,
scrapeWalmartPrices,
scrapeWalmartPrices,
- getNutritionData,
optimizeMealPlan,
optimizeMealPlan,
getAllFoods,
getAllFoods,
};
};
@@ -9,9 +9,8 @@
@@ -9,9 +9,8 @@
const axios = require('axios');
const axios = require('axios');
const { haversine } = require('./utils');
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).
// 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';
const BASE_URL = 'https://ridb.recreation.gov/api/v1';
let _ridbDisabled = false; // Disable after first 401 to avoid duplicate errors
let _ridbDisabled = false; // Disable after first 401 to avoid duplicate errors
@@ -27,10 +26,8 @@ const CAMPING_ACTIVITY_IDS = [
@@ -27,10 +26,8 @@ const CAMPING_ACTIVITY_IDS = [
/**
/**
* Search RIDB for facilities near lat/lon within radiusMiles
* 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
if (_ridbDisabled) return []; // Skip after auth failure
const results = [];
const results = [];
@@ -46,7 +43,7 @@ async function search(lat, lon, radiusMiles) {
@@ -46,7 +43,7 @@ async function search(lat, lon, radiusMiles) {
offset: 0,
offset: 0,
},
},
headers: {
headers: {
- apikey: API_KEY,
+ apikey: apiKey,
Accept: 'application/json',
Accept: 'application/json',
},
},
timeout: 15000,
timeout: 15000,
@@ -100,7 +97,7 @@ async function search(lat, lon, radiusMiles) {
@@ -100,7 +97,7 @@ async function search(lat, lon, radiusMiles) {
limit: 25,
limit: 25,
},
},
headers: {
headers: {
- apikey: API_KEY,
+ apikey: apiKey,
Accept: 'application/json',
Accept: 'application/json',
},
},
timeout: 15000,
timeout: 15000,
@@ -146,9 +143,9 @@ async function search(lat, lon, radiusMiles) {
@@ -146,9 +143,9 @@ async function search(lat, lon, radiusMiles) {
/**
/**
* Get detailed facility info from RIDB
* Get detailed facility info from RIDB
*/
*/
-async function getDetail(facilityId) {
+async function getDetail(facilityId, apiKey) {
const { data } = await axios.get(`${BASE_URL}/facilities/${facilityId}`, {
const { data } = await axios.get(`${BASE_URL}/facilities/${facilityId}`, {
- headers: { apikey: API_KEY, Accept: 'application/json' },
+ headers: { apikey: apiKey, Accept: 'application/json' },
timeout: 10000,
timeout: 10000,
});
});
return data;
return data;
new file mode 100644
new file mode 100644
@@ -0,0 +1,73 @@
@@ -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); });