openvibers/openvibe.tools Commits
Secrets never reach a visitor of the gateway or the maps satellite, by test (roadmap WS-R task 5: internal-secret leak). apps/_shared/test/no-network.js is a preload for a started satellite: every connection or lookup that is not loopback fails at once, so third-party calls take their error paths without the internet.
Changes
nnew file mode 100644@@@ -0,0 +1,45 @@++'use strict';++// Preload (node -r) for tests that start a satellite whose code calls third-party APIs: every++// connection to anything but loopback fails at once (ECONNREFUSED) and every lookup of a name other++// than localhost fails (ENOTFOUND), so the process's error paths run without touching the internet.++const dns = require('dns');++const net = require('net');++++const LOCAL = new Set(['127.0.0.1', '::1', 'localhost', '::ffff:127.0.0.1']);++const isLocal = (h) => h == null || LOCAL.has(String(h).replace(/^\[|\]$/g, '').toLowerCase());++const notFound = (host) => Object.assign(new Error(`getaddrinfo ENOTFOUND ${host} (no network in tests)`), { code: 'ENOTFOUND', hostname: host });++++const realLookup = dns.lookup;++dns.lookup = function (host, options, cb) {++ if (typeof options === 'function') { cb = options; options = {}; }++ if (isLocal(host)) return realLookup.call(this, host, options, cb);++ process.nextTick(() => cb(notFound(host)));++ return {};++};++const realPromiseLookup = dns.promises.lookup;++dns.promises.lookup = async function (host, options) {++ if (isLocal(host)) return realPromiseLookup.call(this, host, options);++ throw notFound(host);++};++const realConnect = net.Socket.prototype.connect;++net.Socket.prototype.connect = function (...args) {++ let o = args[0];++ if (Array.isArray(o)) o = o[0];++ const host = o && typeof o === 'object' ? (o.path != null ? null : o.host) : (typeof args[1] === 'string' ? args[1] : null);++ if (o && typeof o === 'object' && o.path != null) return realConnect.apply(this, args);++ if (!isLocal(host)) {++ const err = Object.assign(new Error(`connect ECONNREFUSED ${host} (no network in tests)`), { code: 'ECONNREFUSED', syscall: 'connect' });++ process.nextTick(() => this.destroy(err));++ return this;++ }++ return realConnect.apply(this, args);++};++if (typeof globalThis.fetch === 'function') {++ const realFetch = globalThis.fetch;++ globalThis.fetch = (url, opts) => {++ let host = null;++ try { host = new URL(String((url && url.url) || url)).hostname; } catch { /* */ }++ if (!isLocal(host)) return Promise.reject(Object.assign(new TypeError('fetch failed'), { cause: notFound(host) }));++ return realFetch(url, opts);++ };++}nnew file mode 100644@@@ -0,0 +1,91 @@++'use strict';++/**++ * The gateway's secrets never reach a visitor (roadmap WS-R task 5, the internal-secret class). The++ * gateway starts with a sentinel as its Network OAuth client secret, its internal API keys and its++ * Events subscription secret, and with no network (the no-network preload: every third-party call++ * and lookup fails at once, so the net tools and the sign-in, registry and search calls all take++ * their error paths). Then its pages, APIs, every tool in its registry (description and schema) and++ * every net tool with hosts, internal addresses and nonsense are requested; and the sign-in callback++ * with a forged code (it posts the client secret to Network). No body or header may carry a sentinel,++ * nor may the process's output.++ */++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++const { startApp } = require('../../_shared/test/spawn');++++const SECRETS = {++ OV_OAUTH_CLIENT_SECRET: 'sentinel-not-a-secret-tools-oauth-client',++ INTERNAL_API_KEY: 'sentinel-not-a-secret-tools-internal',++ OV_INTERNAL_KEY: 'sentinel-not-a-secret-tools-ov-internal',++ TOOLS_EVENTS_SECRET: 'sentinel-not-a-secret-tools-events-subscription',++};++const NET_TOOLS = ['blacklist', 'dnsprop', 'dns', 'headers', 'ip', 'ipv4', 'ipv6', 'lookup', 'ping', 'port', 'rdap', 'rdns', 'redirects', 'robots', 'sitemap', 'smtp', 'ssl', 'uptime', 'whois'];++let seq = 0;++const client = () => { seq++; return { 'x-forwarded-for': `198.18.${(seq >> 8) & 255}.${seq & 255}` }; };++++(async () => {++ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tools-sec-'));++ const gw = await startApp('gateway', {++ ...SECRETS, DATA_DIR: tmp, OV_OAUTH_CLIENT_ID: 'tools',++ OV_DOMAINS_URL: 'http://127.0.0.1:9/api/domains', OV_REGISTRY_URL: 'http://127.0.0.1:9/registry', OV_COMMUNITY_INTERNAL_URL: 'http://127.0.0.1:9',++ NODE_OPTIONS: `--require ${path.join(__dirname, '..', '..', '_shared', 'test', 'no-network.js')}`,++ });++ 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 ')}`); } };++ const get = async (p, opts = {}) => {++ const ac = new AbortController();++ const timer = setTimeout(() => ac.abort(), 15000);++ try {++ const r = await fetch(gw.base + p, { redirect: 'manual', signal: ac.signal, ...opts, headers: { ...client(), ...(opts.headers || {}) } });++ const text = await r.text().catch(() => '');++ return { status: r.status, text: text + JSON.stringify([...r.headers.entries()]) };++ } catch { return { status: 0, text: '' }; } finally { clearTimeout(timer); }++ };++ try {++ const paths = ['/', '/all-tools', '/tools', '/developers', '/updates', '/tos', '/llms.txt', '/robots.txt', '/sitemap.xml', '/search?q=x', '/me', '/login', '/logout',++ '/callback?code=forged&state=forged', '/auth/callback?code=forged&state=forged', '/api/health', '/api/ready', '/api/brand', '/api/catalog.json', '/api/v1/openapi.json',++ '/api/v1/me/recent-tools', '/api/internal/analytics', '/api/nope', '/nope', '/.env', '/api/%', '/api/pastes/x', '/webhook/bins/nope', '/myip', '/opengraph?url=http://127.0.0.1/',++ '/api/v1/tools', '/api/v1/tools?q=x'];++ const tools = await get('/api/v1/tools');++ let ids = [];++ try { ids = (JSON.parse(tools.text.slice(0, tools.text.lastIndexOf('[['))).tools || []).map((t) => t.id); } catch { /* listed below */ }++ for (const id of ids) paths.push(`/api/v1/tools/${encodeURIComponent(id)}`, `/api/v1/tools/${encodeURIComponent(id)}/schema`, `/tool/${encodeURIComponent(id)}`);++ for (const tool of NET_TOOLS) {++ for (const target of ['example.com', '127.0.0.1', '169.254.169.254', "'\"<x>"]) paths.push(`/${tool}/${encodeURIComponent(target)}`, `/api/net/${tool}?target=${encodeURIComponent(target)}&host=${encodeURIComponent(target)}`);++ }++ const found = [];++ const statuses = {};++ const limited = [];++ await check(`every page, API, registered tool and net tool (${paths.length} paths): no sentinel`, async () => {++ for (const p of paths) {++ const r = await get(p);++ statuses[r.status] = (statuses[r.status] || 0) + 1;++ if (r.status === 429) limited.push(p);++ for (const [k, v] of Object.entries(SECRETS)) if (r.text.includes(v)) found.push(`GET ${p} → ${r.status} carries ${k}`);++ }++ for (const p of ['/api/pastes/x', '/api/net/port', '/auth/fedcm', '/webhook/bins']) {++ const r = await get(p, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{"broken": ' });++ for (const [k, v] of Object.entries(SECRETS)) if (r.text.includes(v)) found.push(`POST ${p} → ${r.status} carries ${k}`);++ }++ console.log(` (tools listed: ${ids.length}; answers ${JSON.stringify(statuses)})`);++ assert.ok(ids.length > 10, 'the tool registry was listed');++ // The net tools also throttle per target (the same host asked too often): only those may be 429.++ const netTool = new RegExp(`^/(api/net/)?(${NET_TOOLS.join('|')})[/?]`);++ assert.deepStrictEqual(limited.filter((p) => !netTool.test(p)), [], 'rate-limited outside the net tools');++ assert.deepStrictEqual(found, []);++ });++ await check('the process output carries no sentinel', () => {++ const out = gw.output();++ for (const [k, v] of Object.entries(SECRETS)) assert.ok(!out.includes(v), `a log line carries ${k}`);++ });++ } finally {++ gw.kill('SIGTERM');++ await gw.exited;++ fs.rmSync(tmp, { recursive: true, force: true });++ }++ if (failures) { console.log(`\n${failures} failure(s)`); process.exit(1); }++ console.log('\ngateway security-secrets: all checks passed');++ process.exit(0);++})().catch((e) => { console.error(e); process.exit(1); });nnew file mode 100644@@@ -0,0 +1,76 @@++'use strict';++/**++ * The maps satellite's provider keys never reach a visitor (roadmap WS-R task 5, the internal-secret++ * class). Maps calls RIDB, NPS and OpenChargeMap with keys from its environment, and /api/search++ * reports each source's failure to the visitor (sourceMeta[].error). The satellite starts with a++ * sentinel in each key and with no network (test/no-network.js preloaded: every third-party call++ * fails at once), so every source takes its error path; then every route is requested with places++ * and nonsense coordinates. No body or header may carry a key; nor may the process's output.++ */++const assert = require('assert');++const path = require('path');++const { startApp } = require('../../_shared/test/spawn');++++const KEYS = {++ RIDB_API_KEY: 'sentinel-not-a-secret-ridb',++ NPS_API_KEY: 'sentinel-not-a-secret-nps',++ OPEN_CHARGE_MAP_KEY: 'sentinel-not-a-secret-ocm',++ INTERNAL_API_KEY: 'sentinel-not-a-secret-internal',++ OV_INTERNAL_KEY: 'sentinel-not-a-secret-ov-internal',++};++++// One loopback proxy hop is trusted (guard/ip.js): a fresh client address per request keeps the++// per-address limit out of what is being read.++let seq = 0;++const client = () => { seq++; return { 'x-forwarded-for': `198.18.${(seq >> 8) & 255}.${seq & 255}` }; };++++(async () => {++ const maps = await startApp('maps', { ...KEYS, NODE_OPTIONS: `--require ${path.join(__dirname, '..', '..', '_shared', 'test', 'no-network.js')}` }, undefined, { readyPath: '/api/ready' });++ 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 ')}`); } };++ try {++ const places = ['lat=47.6062&lon=-122.3321&radius=25&q=seattle', 'lat=37.7749&lon=-122.4194&radius=100&q=park&state=CA', 'lat=999&lon=abc&radius=-1&q=%27%22%3Cx%3E', 'q=' + 'x'.repeat(300)];++ const paths = ['/api/search', '/api/search/stream', '/api/geocode', '/api/weather', '/api/terrain', '/api/food-banks', '/api/foods', '/api/stores', '/api/meal-plan',++ '/api/internal/analytics', '/api/internal/analytics/bots', '/api/ready', '/api/nope', '/', '/nope'];++ let answered = 0;++ const found = [];++ const statuses = {};++ await check('every route, with places and nonsense: no provider or internal key in a body or header', async () => {++ for (const p of paths) {++ for (const q of places) {++ let r = null;++ try {++ const ac = new AbortController();++ const timer = setTimeout(() => ac.abort(), 15000);++ r = await fetch(`${maps.base}${p}?${q}`, { signal: ac.signal, headers: client() });++ const text = await r.text().catch(() => '');++ clearTimeout(timer);++ answered++;++ statuses[r.status] = (statuses[r.status] || 0) + 1;++ const all = text + JSON.stringify([...r.headers.entries()]);++ for (const [k, v] of Object.entries(KEYS)) if (all.includes(v)) found.push(`GET ${p}?${q.slice(0, 40)} → ${r.status} carries ${k}`);++ } catch { /* a stream cut off by the timeout carries what it carried */ }++ }++ }++ assert.ok(answered >= paths.length * 2, `${answered} answers`);++ assert.ok(statuses[429] === undefined, `rate-limited answers: ${statuses[429]}`);++ assert.deepStrictEqual(found, []);++ });++ await check('the search really ran its sources into their error paths (the keys were in use)', async () => {++ const r = await fetch(`${maps.base}/api/search?${places[0]}`, { headers: client() });++ const body = await r.json().catch(() => ({}));++ const meta = JSON.stringify(body.sourceMeta || body);++ assert.match(meta, /RIDB|NPS|OpenChargeMap/, meta.slice(0, 300));++ });++ await check('the process output carries no key', async () => {++ const out = maps.output();++ for (const [k, v] of Object.entries(KEYS)) assert.ok(!out.includes(v), `a log line carries ${k}`);++ });++ } finally {++ maps.kill('SIGTERM');++ await maps.exited;++ }++ if (failures) { console.log(`\n${failures} failure(s)`); process.exit(1); }++ console.log('\nmaps security-secrets: all checks passed');++ process.exit(0);++})().catch((e) => { console.error(e); process.exit(1); });
new file mode 100644
new file mode 100644
@@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
+'use strict';
+// Preload (node -r) for tests that start a satellite whose code calls third-party APIs: every
+// connection to anything but loopback fails at once (ECONNREFUSED) and every lookup of a name other
+// than localhost fails (ENOTFOUND), so the process's error paths run without touching the internet.
+const dns = require('dns');
+const net = require('net');
+
+const LOCAL = new Set(['127.0.0.1', '::1', 'localhost', '::ffff:127.0.0.1']);
+const isLocal = (h) => h == null || LOCAL.has(String(h).replace(/^\[|\]$/g, '').toLowerCase());
+const notFound = (host) => Object.assign(new Error(`getaddrinfo ENOTFOUND ${host} (no network in tests)`), { code: 'ENOTFOUND', hostname: host });
+
+const realLookup = dns.lookup;
+dns.lookup = function (host, options, cb) {
+ if (typeof options === 'function') { cb = options; options = {}; }
+ if (isLocal(host)) return realLookup.call(this, host, options, cb);
+ process.nextTick(() => cb(notFound(host)));
+ return {};
+};
+const realPromiseLookup = dns.promises.lookup;
+dns.promises.lookup = async function (host, options) {
+ if (isLocal(host)) return realPromiseLookup.call(this, host, options);
+ throw notFound(host);
+};
+const realConnect = net.Socket.prototype.connect;
+net.Socket.prototype.connect = function (...args) {
+ let o = args[0];
+ if (Array.isArray(o)) o = o[0];
+ const host = o && typeof o === 'object' ? (o.path != null ? null : o.host) : (typeof args[1] === 'string' ? args[1] : null);
+ if (o && typeof o === 'object' && o.path != null) return realConnect.apply(this, args);
+ if (!isLocal(host)) {
+ const err = Object.assign(new Error(`connect ECONNREFUSED ${host} (no network in tests)`), { code: 'ECONNREFUSED', syscall: 'connect' });
+ process.nextTick(() => this.destroy(err));
+ return this;
+ }
+ return realConnect.apply(this, args);
+};
+if (typeof globalThis.fetch === 'function') {
+ const realFetch = globalThis.fetch;
+ globalThis.fetch = (url, opts) => {
+ let host = null;
+ try { host = new URL(String((url && url.url) || url)).hostname; } catch { /* */ }
+ if (!isLocal(host)) return Promise.reject(Object.assign(new TypeError('fetch failed'), { cause: notFound(host) }));
+ return realFetch(url, opts);
+ };
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
+'use strict';
+/**
+ * The gateway's secrets never reach a visitor (roadmap WS-R task 5, the internal-secret class). The
+ * gateway starts with a sentinel as its Network OAuth client secret, its internal API keys and its
+ * Events subscription secret, and with no network (the no-network preload: every third-party call
+ * and lookup fails at once, so the net tools and the sign-in, registry and search calls all take
+ * their error paths). Then its pages, APIs, every tool in its registry (description and schema) and
+ * every net tool with hosts, internal addresses and nonsense are requested; and the sign-in callback
+ * with a forged code (it posts the client secret to Network). No body or header may carry a sentinel,
+ * nor may the process's output.
+ */
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { startApp } = require('../../_shared/test/spawn');
+
+const SECRETS = {
+ OV_OAUTH_CLIENT_SECRET: 'sentinel-not-a-secret-tools-oauth-client',
+ INTERNAL_API_KEY: 'sentinel-not-a-secret-tools-internal',
+ OV_INTERNAL_KEY: 'sentinel-not-a-secret-tools-ov-internal',
+ TOOLS_EVENTS_SECRET: 'sentinel-not-a-secret-tools-events-subscription',
+};
+const NET_TOOLS = ['blacklist', 'dnsprop', 'dns', 'headers', 'ip', 'ipv4', 'ipv6', 'lookup', 'ping', 'port', 'rdap', 'rdns', 'redirects', 'robots', 'sitemap', 'smtp', 'ssl', 'uptime', 'whois'];
+let seq = 0;
+const client = () => { seq++; return { 'x-forwarded-for': `198.18.${(seq >> 8) & 255}.${seq & 255}` }; };
+
+(async () => {
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tools-sec-'));
+ const gw = await startApp('gateway', {
+ ...SECRETS, DATA_DIR: tmp, OV_OAUTH_CLIENT_ID: 'tools',
+ OV_DOMAINS_URL: 'http://127.0.0.1:9/api/domains', OV_REGISTRY_URL: 'http://127.0.0.1:9/registry', OV_COMMUNITY_INTERNAL_URL: 'http://127.0.0.1:9',
+ NODE_OPTIONS: `--require ${path.join(__dirname, '..', '..', '_shared', 'test', 'no-network.js')}`,
+ });
+ 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 ')}`); } };
+ const get = async (p, opts = {}) => {
+ const ac = new AbortController();
+ const timer = setTimeout(() => ac.abort(), 15000);
+ try {
+ const r = await fetch(gw.base + p, { redirect: 'manual', signal: ac.signal, ...opts, headers: { ...client(), ...(opts.headers || {}) } });
+ const text = await r.text().catch(() => '');
+ return { status: r.status, text: text + JSON.stringify([...r.headers.entries()]) };
+ } catch { return { status: 0, text: '' }; } finally { clearTimeout(timer); }
+ };
+ try {
+ const paths = ['/', '/all-tools', '/tools', '/developers', '/updates', '/tos', '/llms.txt', '/robots.txt', '/sitemap.xml', '/search?q=x', '/me', '/login', '/logout',
+ '/callback?code=forged&state=forged', '/auth/callback?code=forged&state=forged', '/api/health', '/api/ready', '/api/brand', '/api/catalog.json', '/api/v1/openapi.json',
+ '/api/v1/me/recent-tools', '/api/internal/analytics', '/api/nope', '/nope', '/.env', '/api/%', '/api/pastes/x', '/webhook/bins/nope', '/myip', '/opengraph?url=http://127.0.0.1/',
+ '/api/v1/tools', '/api/v1/tools?q=x'];
+ const tools = await get('/api/v1/tools');
+ let ids = [];
+ try { ids = (JSON.parse(tools.text.slice(0, tools.text.lastIndexOf('[['))).tools || []).map((t) => t.id); } catch { /* listed below */ }
+ for (const id of ids) paths.push(`/api/v1/tools/${encodeURIComponent(id)}`, `/api/v1/tools/${encodeURIComponent(id)}/schema`, `/tool/${encodeURIComponent(id)}`);
+ for (const tool of NET_TOOLS) {
+ for (const target of ['example.com', '127.0.0.1', '169.254.169.254', "'\"<x>"]) paths.push(`/${tool}/${encodeURIComponent(target)}`, `/api/net/${tool}?target=${encodeURIComponent(target)}&host=${encodeURIComponent(target)}`);
+ }
+ const found = [];
+ const statuses = {};
+ const limited = [];
+ await check(`every page, API, registered tool and net tool (${paths.length} paths): no sentinel`, async () => {
+ for (const p of paths) {
+ const r = await get(p);
+ statuses[r.status] = (statuses[r.status] || 0) + 1;
+ if (r.status === 429) limited.push(p);
+ for (const [k, v] of Object.entries(SECRETS)) if (r.text.includes(v)) found.push(`GET ${p} → ${r.status} carries ${k}`);
+ }
+ for (const p of ['/api/pastes/x', '/api/net/port', '/auth/fedcm', '/webhook/bins']) {
+ const r = await get(p, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{"broken": ' });
+ for (const [k, v] of Object.entries(SECRETS)) if (r.text.includes(v)) found.push(`POST ${p} → ${r.status} carries ${k}`);
+ }
+ console.log(` (tools listed: ${ids.length}; answers ${JSON.stringify(statuses)})`);
+ assert.ok(ids.length > 10, 'the tool registry was listed');
+ // The net tools also throttle per target (the same host asked too often): only those may be 429.
+ const netTool = new RegExp(`^/(api/net/)?(${NET_TOOLS.join('|')})[/?]`);
+ assert.deepStrictEqual(limited.filter((p) => !netTool.test(p)), [], 'rate-limited outside the net tools');
+ assert.deepStrictEqual(found, []);
+ });
+ await check('the process output carries no sentinel', () => {
+ const out = gw.output();
+ for (const [k, v] of Object.entries(SECRETS)) assert.ok(!out.includes(v), `a log line carries ${k}`);
+ });
+ } finally {
+ gw.kill('SIGTERM');
+ await gw.exited;
+ fs.rmSync(tmp, { recursive: true, force: true });
+ }
+ if (failures) { console.log(`\n${failures} failure(s)`); process.exit(1); }
+ console.log('\ngateway security-secrets: all checks passed');
+ process.exit(0);
+})().catch((e) => { console.error(e); process.exit(1); });
new file mode 100644
new file mode 100644
@@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
+'use strict';
+/**
+ * The maps satellite's provider keys never reach a visitor (roadmap WS-R task 5, the internal-secret
+ * class). Maps calls RIDB, NPS and OpenChargeMap with keys from its environment, and /api/search
+ * reports each source's failure to the visitor (sourceMeta[].error). The satellite starts with a
+ * sentinel in each key and with no network (test/no-network.js preloaded: every third-party call
+ * fails at once), so every source takes its error path; then every route is requested with places
+ * and nonsense coordinates. No body or header may carry a key; nor may the process's output.
+ */
+const assert = require('assert');
+const path = require('path');
+const { startApp } = require('../../_shared/test/spawn');
+
+const KEYS = {
+ RIDB_API_KEY: 'sentinel-not-a-secret-ridb',
+ NPS_API_KEY: 'sentinel-not-a-secret-nps',
+ OPEN_CHARGE_MAP_KEY: 'sentinel-not-a-secret-ocm',
+ INTERNAL_API_KEY: 'sentinel-not-a-secret-internal',
+ OV_INTERNAL_KEY: 'sentinel-not-a-secret-ov-internal',
+};
+
+// One loopback proxy hop is trusted (guard/ip.js): a fresh client address per request keeps the
+// per-address limit out of what is being read.
+let seq = 0;
+const client = () => { seq++; return { 'x-forwarded-for': `198.18.${(seq >> 8) & 255}.${seq & 255}` }; };
+
+(async () => {
+ const maps = await startApp('maps', { ...KEYS, NODE_OPTIONS: `--require ${path.join(__dirname, '..', '..', '_shared', 'test', 'no-network.js')}` }, undefined, { readyPath: '/api/ready' });
+ 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 ')}`); } };
+ try {
+ const places = ['lat=47.6062&lon=-122.3321&radius=25&q=seattle', 'lat=37.7749&lon=-122.4194&radius=100&q=park&state=CA', 'lat=999&lon=abc&radius=-1&q=%27%22%3Cx%3E', 'q=' + 'x'.repeat(300)];
+ const paths = ['/api/search', '/api/search/stream', '/api/geocode', '/api/weather', '/api/terrain', '/api/food-banks', '/api/foods', '/api/stores', '/api/meal-plan',
+ '/api/internal/analytics', '/api/internal/analytics/bots', '/api/ready', '/api/nope', '/', '/nope'];
+ let answered = 0;
+ const found = [];
+ const statuses = {};
+ await check('every route, with places and nonsense: no provider or internal key in a body or header', async () => {
+ for (const p of paths) {
+ for (const q of places) {
+ let r = null;
+ try {
+ const ac = new AbortController();
+ const timer = setTimeout(() => ac.abort(), 15000);
+ r = await fetch(`${maps.base}${p}?${q}`, { signal: ac.signal, headers: client() });
+ const text = await r.text().catch(() => '');
+ clearTimeout(timer);
+ answered++;
+ statuses[r.status] = (statuses[r.status] || 0) + 1;
+ const all = text + JSON.stringify([...r.headers.entries()]);
+ for (const [k, v] of Object.entries(KEYS)) if (all.includes(v)) found.push(`GET ${p}?${q.slice(0, 40)} → ${r.status} carries ${k}`);
+ } catch { /* a stream cut off by the timeout carries what it carried */ }
+ }
+ }
+ assert.ok(answered >= paths.length * 2, `${answered} answers`);
+ assert.ok(statuses[429] === undefined, `rate-limited answers: ${statuses[429]}`);
+ assert.deepStrictEqual(found, []);
+ });
+ await check('the search really ran its sources into their error paths (the keys were in use)', async () => {
+ const r = await fetch(`${maps.base}/api/search?${places[0]}`, { headers: client() });
+ const body = await r.json().catch(() => ({}));
+ const meta = JSON.stringify(body.sourceMeta || body);
+ assert.match(meta, /RIDB|NPS|OpenChargeMap/, meta.slice(0, 300));
+ });
+ await check('the process output carries no key', async () => {
+ const out = maps.output();
+ for (const [k, v] of Object.entries(KEYS)) assert.ok(!out.includes(v), `a log line carries ${k}`);
+ });
+ } finally {
+ maps.kill('SIGTERM');
+ await maps.exited;
+ }
+ if (failures) { console.log(`\n${failures} failure(s)`); process.exit(1); }
+ console.log('\nmaps security-secrets: all checks passed');
+ process.exit(0);
+})().catch((e) => { console.error(e); process.exit(1); });