fix: keep configured endpoint URLs out of caller-visible error text

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
FilipBl4gojevic
2026-07-31 18:28:28 +02:00
parent 69644ef7ba
commit c0e05422b3
6 changed files with 118 additions and 10 deletions

View File

@@ -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 |

View File

@@ -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`,
}],
};
}

View File

@@ -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`,
}],
};

View File

@@ -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`,
}],

View File

@@ -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();

View File

@@ -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'));
});
});