diff --git a/README.md b/README.md index fb06df4..0ac0bcd 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,14 @@ it, lives in your local signer instead - see `VECTOR_SIGNER_SPEND_LIMIT_PER_TX` > echoes a token value. Commas delimit entries and cannot be escaped, so generate > tokens from a comma-free charset (hex / base64url / alphanumeric). +> **Error responses do not echo configured endpoints.** A failed Ogmios, Koios, or +> submit-API request is reported to the caller by service name and status only - +> for example "Ogmios request failed (queryLedgerState/utxo): 503 Service +> Unavailable" - never the configured `VECTOR_OGMIOS_URL` / `VECTOR_KOIOS_URL` / +> `VECTOR_SUBMIT_URL` value or the raw response body. Full detail goes to the +> server's own `console.error` log for operators; a caller only ever sees the +> service name and status. + ### Mainnet endpoints | Variable | Mainnet value | diff --git a/packages/builder/src/vector/agent-network.ts b/packages/builder/src/vector/agent-network.ts index a4eda7f..e242fc9 100644 --- a/packages/builder/src/vector/agent-network.ts +++ b/packages/builder/src/vector/agent-network.ts @@ -81,7 +81,7 @@ ${capList} **Troubleshooting Tips:** 1. Verify the DID format: did:vector:agent:{policyId}:{nftAssetName} 2. The agent may not be registered or may have deregistered -3. Check that Koios is reachable at ${VECTOR_KOIOS_URL}`, +3. Verify the configured Koios endpoint is reachable`, }], }; } @@ -133,7 +133,7 @@ ${capList} text: `Failed to discover agents: ${err instanceof Error ? err.message : String(err)} **Troubleshooting Tips:** -1. Check that Ogmios is reachable at ${VECTOR_OGMIOS_URL} +1. Verify the configured Ogmios endpoint is reachable 2. The registry may be empty - no agents have registered yet 3. Try without filters to list all agents`, }], @@ -189,7 +189,7 @@ The DID above is bound to the exact wallet UTxO this build consumed - if that UT **Troubleshooting Tips:** 1. Ensure the changeAddress wallet holds at least 12 AP3X (10 deposit + fees) 2. Each wallet can register multiple agents (different one-shot UTxOs) -3. Verify Ogmios endpoint is reachable at ${VECTOR_OGMIOS_URL}`, +3. Verify the configured Ogmios endpoint is reachable`, }], }; } diff --git a/packages/builder/src/vector/self-improvement.ts b/packages/builder/src/vector/self-improvement.ts index b7f270e..2ca49fa 100644 --- a/packages/builder/src/vector/self-improvement.ts +++ b/packages/builder/src/vector/self-improvement.ts @@ -197,7 +197,7 @@ Each batch UTxO holds ~30 AP3X for adoption rewards.`, **Troubleshooting Tips:** 1. Verify the module contracts are deployed on this network -2. Check that Ogmios is reachable at ${VECTOR_OGMIOS_URL} +2. Verify the configured Ogmios endpoint is reachable 3. The script addresses may not have any UTxOs yet`, }], }; diff --git a/packages/builder/src/vector/vector.ts b/packages/builder/src/vector/vector.ts index 028d4c3..087e0a9 100644 --- a/packages/builder/src/vector/vector.ts +++ b/packages/builder/src/vector/vector.ts @@ -590,7 +590,7 @@ ${txList} text: `Failed to get transaction history: ${error.message} **Troubleshooting Tips:** -1. Ensure Koios is configured and reachable: ${VECTOR_KOIOS_URL} +1. Ensure the configured Koios endpoint is reachable 2. Verify the address is valid 3. Check the block explorer for this address`, }], diff --git a/packages/shared/src/provider.ts b/packages/shared/src/provider.ts index 8f48852..e5c716c 100644 --- a/packages/shared/src/provider.ts +++ b/packages/shared/src/provider.ts @@ -78,6 +78,32 @@ export function koiosIndicatesConfirmed(data: unknown): boolean { return typeof entry?.num_confirmations === 'number' && entry.num_confirmations > 0; } +/** + * Format a downstream HTTP or JSON-RPC failure into a message safe to return + * to an untrusted caller: names the service, the operation, and the status - + * never the endpoint URL, and never the raw response body. + * + * On a hosted, multi-tenant deployment the configured Ogmios/Koios/submit-api + * endpoint is operator infrastructure, not caller information - and a raw + * response body from a misconfigured or unreachable endpoint (a reverse-proxy + * error page, for instance) can itself echo back hostnames or other detail + * that has no business reaching a caller. Call sites console.error the full + * detail (status, body, and which endpoint) themselves before throwing this - + * full detail to the operator's log, a clean summary to the caller. That + * split is the sanitisation. + * + * Exported and pure so it can be tested without a network round trip - same + * rationale as parseSubmitResponse above. + */ +export function formatServiceError( + service: string, + operation: string, + status: number | string, + statusText?: string, +): string { + return `${service} request failed (${operation}): ${status}${statusText ? ` ${statusText}` : ''}`; +} + export class OgmiosProvider implements Provider { private ogmiosUrl: string; private submitUrl: string; @@ -107,12 +133,14 @@ export class OgmiosProvider implements Provider { if (!response.ok) { const text = await response.text(); - throw new Error(`Ogmios RPC error (${response.status}): ${text}`); + console.error(`[OgmiosProvider] Ogmios RPC error (${method}) at ${this.ogmiosUrl}: ${response.status} ${response.statusText} - ${text}`); + throw new Error(formatServiceError('Ogmios', method, response.status, response.statusText)); } const json = await response.json(); if (json.error) { - throw new Error(`Ogmios RPC error: ${JSON.stringify(json.error)}`); + console.error(`[OgmiosProvider] Ogmios RPC error (${method}) at ${this.ogmiosUrl}:`, json.error); + throw new Error(formatServiceError('Ogmios', method, json.error?.code ?? 'error', json.error?.message ?? 'RPC error')); } return json.result; @@ -333,7 +361,8 @@ export class OgmiosProvider implements Provider { }); if (!response.ok) { const text = await response.text(); - throw new Error(`Koios tx_status query failed (${response.status}): ${text}`); + console.error(`[OgmiosProvider] Koios tx_status query failed at ${this.koiosUrl}: ${response.status} ${response.statusText} - ${text}`); + throw new Error(formatServiceError('Koios', 'tx_status', response.status, response.statusText)); } return response.json(); } @@ -445,7 +474,8 @@ export class OgmiosProvider implements Provider { if (!txListResponse.ok) { const text = await txListResponse.text(); - throw new Error(`Koios address_txs query failed (${txListResponse.status}): ${text}`); + console.error(`[OgmiosProvider] Koios address_txs query failed at ${this.koiosUrl}: ${txListResponse.status} ${txListResponse.statusText} - ${text}`); + throw new Error(formatServiceError('Koios', 'address_txs', txListResponse.status, txListResponse.statusText)); } const txList: any[] = await txListResponse.json(); @@ -512,7 +542,8 @@ export class OgmiosProvider implements Provider { if (!response.ok) { const errorText = await response.text(); - throw new Error(`Transaction submission failed (${response.status}): ${errorText}`); + console.error(`[OgmiosProvider] Transaction submission failed at ${this.submitUrl}: ${response.status} ${response.statusText} - ${errorText}`); + throw new Error(formatServiceError('Submit API', 'submitTx', response.status, response.statusText)); } const result = await response.text(); diff --git a/packages/shared/test/provider-error-format.test.ts b/packages/shared/test/provider-error-format.test.ts new file mode 100644 index 0000000..e7a4e93 --- /dev/null +++ b/packages/shared/test/provider-error-format.test.ts @@ -0,0 +1,69 @@ +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; +import { formatServiceError } from '../src/provider.ts'; +import { VECTOR_OGMIOS_URL, VECTOR_KOIOS_URL, VECTOR_SUBMIT_URL } from '../src/config.ts'; + +// The concern (audit E): a hosted deployment's internal Ogmios/Koios/submit-api +// endpoint reaching an untrusted caller through error text - either the +// configured URL itself, or a raw response body from a misconfigured/internal +// endpoint (a reverse-proxy error page, for instance) that can echo back +// hostnames or other operator-only detail. formatServiceError is the pure +// function every throw site in provider.ts now routes its caller-visible +// message through; the full detail (status, body, and which endpoint) goes to +// console.error instead, at each call site. + +describe('formatServiceError', () => { + test('names the service, operation, and HTTP status', () => { + assert.equal( + formatServiceError('Ogmios', 'queryLedgerState/utxo', 503, 'Service Unavailable'), + 'Ogmios request failed (queryLedgerState/utxo): 503 Service Unavailable', + ); + }); + + test('covers Koios and the submit API with the same shape', () => { + assert.equal( + formatServiceError('Koios', 'tx_status', 502, 'Bad Gateway'), + 'Koios request failed (tx_status): 502 Bad Gateway', + ); + assert.equal( + formatServiceError('Submit API', 'submitTx', 400, 'Bad Request'), + 'Submit API request failed (submitTx): 400 Bad Request', + ); + }); + + test('omits the status text when the caller has none (a JSON-RPC error code, not an HTTP status)', () => { + assert.equal( + formatServiceError('Ogmios', 'queryLedgerState/utxo', -32602), + 'Ogmios request failed (queryLedgerState/utxo): -32602', + ); + }); + + test('never contains any configured endpoint URL', () => { + // Structural, not incidental: the function has no parameter for a URL or + // a response body, so it cannot forward either - but pin it against the + // actual values in config.ts (the ones a hosted deployment sets to its + // own internal endpoints) so a future signature change that threads one + // through fails this test by name. + const messages = [ + formatServiceError('Ogmios', 'queryLedgerState/utxo', 503, 'Service Unavailable'), + formatServiceError('Koios', 'address_txs', 500, 'Internal Server Error'), + formatServiceError('Submit API', 'submitTx', 400, 'Bad Request'), + ]; + for (const url of [VECTOR_OGMIOS_URL, VECTOR_KOIOS_URL, VECTOR_SUBMIT_URL]) { + for (const msg of messages) { + assert.ok(!msg.includes(url), `message leaked a configured endpoint URL: ${msg}`); + } + } + }); + + test('never contains a raw response body, only what the caller passes as status/statusText', () => { + // Simulates the shape of a real leak: an upstream failure whose body + // echoes infrastructure detail no caller should see. formatServiceError + // is never given the body, so it cannot reproduce it - this pins that a + // future edit cannot casually splice ${text} back into the template. + const leakyBody = 'upstream connect error: could not connect to ogmios-internal.vector.svc.cluster.local:1337'; + const msg = formatServiceError('Ogmios', 'queryLedgerState/utxo', 502, 'Bad Gateway'); + assert.ok(!msg.includes(leakyBody)); + assert.ok(!msg.includes('cluster.local')); + }); +});