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 <cursoragent@cursor.com>
This commit is contained in:
parent
93b060bc8b
commit
a05dbb2733
2 changed files with 57 additions and 16 deletions
37
README.md
37
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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue