From a05dbb27335dfc34b84dad9321c958a286f1d96e Mon Sep 17 00:00:00 2001 From: jmcqueen Date: Mon, 6 Jul 2026 15:06:58 -0400 Subject: [PATCH] Route Google API calls through remote agent with insecure TLS - validateAddress and getTimeZone now use proxyRequest instead of native fetch so the request originates from the on-prem IP, satisfying the API_KEY_IP_ADDRESS_BLOCKED restriction on the Google key - Both calls carry insecure:true because the agent's network path runs SSL-inspecting proxies that substitute an internal-CA chain (throws SELF_SIGNED_CERT_IN_CHAIN otherwise); scoped per-request, consistent with SIW - Fix latent bug in getTimeZone: use GET (per Google docs) instead of POST - Rename README section to "Remote agent (SIW + Google)" and document the SSL-inspection nuance Co-authored-by: Cursor --- README.md | 37 ++++++++++++++++++++++++++----------- src/integrations/google.js | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 42576a5..41fbdff 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ greetings, build auto-attendants, and clean up user licensing. - A Webex bot token, a Webex integration (service account) with the scopes currently used by admin API calls, a Twilio lookup account, an SIW basic-auth user, and a Google API key with Address Validation + Time Zone enabled. -- An on-prem host that can reach Store Info Web, to run the remote agent - (see [Remote SIW agent](#remote-siw-agent) below). +- An on-prem host that can reach Store Info Web and Google APIs from an + IP-whitelisted subnet, to run the remote agent (see + [Remote agent (SIW + Google)](#remote-agent-siw--google) below). - Optional: a Google service-account JSON key. Only the REST API key is required today, but if/when you add code that uses `google-auth-library`, save the JSON at `config/google-service-account.json` and set @@ -57,21 +58,35 @@ The volume mount preserves `config/wbxTokens.json` across restarts so the refresh cron doesn't lose state. The `-p 8080:8080` publishes the WebSocket port so the remote SIW agent can connect back to the bot. -## Remote SIW agent +## Remote agent (SIW + Google) -Store Info Web only accepts connections from inside the corporate network, -but the bot runs in the cloud. To bridge the gap, the bot hosts a -WebSocket server; a small on-prem agent dials in and proxies HTTP requests -back and forth. +Some services can only be reached from inside the corporate network: + +- **Store Info Web** — only accepts connections from internal IPs, and + presents an internal-CA TLS cert Node's default trust store doesn't know + about. +- **Google Maps / Address Validation** — the API key is IP-restricted, so + requests direct from the cloud bot IP get `API_KEY_IP_ADDRESS_BLOCKED + (403)`. + +To bridge the gap, the bot hosts a WebSocket server; a small on-prem agent +dials in and proxies HTTP requests back and forth. Both SIW and Google +calls go over this bridge so they originate from the on-prem IP. The agent itself is intentionally generic — it just proxies whatever -`{method, url, headers, auth, body}` payload arrives — and lives in -[`docker/remote-agent/`](docker/remote-agent) as a self-contained Docker -bundle you can build here and ship to the on-prem host. +`{method, url, headers, auth, body, insecure?}` payload arrives — and +lives in [`docker/remote-agent/`](docker/remote-agent) as a self-contained +Docker bundle you can build here and ship to the on-prem host. The +`insecure: true` flag is scoped per request. Both SIW and Google set it, +because the corporate network the agent lives on runs SSL-inspecting +proxies that intercept outbound HTTPS with an internal-CA chain — without +the flag Node throws `SELF_SIGNED_CERT_IN_CHAIN` even for Google's public +certs. Trust is delegated to that proxy by policy, so the scoped bypass is +consistent across all proxied traffic. Once the agent is connected, the bot logs `Remote agent connected` and any `/buildStore`, `/stageStore`, `/migrateStore` command will succeed. If the -agent is not connected, the SIW-dependent commands fail immediately with +agent is not connected, those commands fail immediately with `No remote SIW agent connected` rather than silently timing out. ### Deploying the agent diff --git a/src/integrations/google.js b/src/integrations/google.js index 6972ece..c9eafa4 100644 --- a/src/integrations/google.js +++ b/src/integrations/google.js @@ -1,8 +1,21 @@ import { config } from '../config.js'; -import { requestJson } from '../http.js'; +import { proxyRequest } from '../services/websocket.js'; + +// All Google API calls are routed through the on-prem remote agent so the +// request originates from the whitelisted IP subnet. The API key is +// restricted by IP; calls direct from the cloud bot IP get 403 with +// API_KEY_IP_ADDRESS_BLOCKED. The remote agent runs inside the corporate +// network, so its egress IP matches the key's allow-list. +// +// `insecure: true` is set even though Google's public certs are trusted, +// because the corporate network the agent lives on intercepts outbound +// HTTPS with an SSL-inspecting proxy that presents an internal-CA chain. +// Without the flag Node throws SELF_SIGNED_CERT_IN_CHAIN on any outbound +// TLS. Trust is already delegated to that proxy by being on this network, +// so the scoped per-request bypass is consistent with SIW. /** - * Google Address Validation API. Returns the raw response body. + * Google Address Validation API. Returns the parsed response body. */ export async function validateAddress(street, city, state, postalCode, country) { const body = { @@ -15,18 +28,31 @@ export async function validateAddress(street, city, state, postalCode, country) }, }; const url = `https://addressvalidation.googleapis.com/v1:validateAddress?key=${encodeURIComponent(config.google.apiKey)}`; - return requestJson('POST', url, body); + const response = await proxyRequest({ + method: 'POST', + url, + headers: { 'Content-Type': 'application/json' }, + body, + insecure: true, + }); + return response?.data; } /** - * Google Time Zone API for a given lat/lon at "now". + * Google Time Zone API for a given lat/lon at "now". Uses GET as documented + * by Google — parameters are entirely in the query string. */ export async function getTimeZone(latitude, longitude) { const timestamp = Math.floor(Date.now() / 1000); const url = `https://maps.googleapis.com/maps/api/timezone/json?location=${latitude}%2C${longitude}` + `×tamp=${timestamp}&key=${encodeURIComponent(config.google.apiKey)}`; - return requestJson('POST', url); + const response = await proxyRequest({ + method: 'GET', + url, + insecure: true, + }); + return response?.data; } /**