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:
@@ -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 |
|
||||
|
||||
@@ -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`,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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`,
|
||||
}],
|
||||
};
|
||||
|
||||
@@ -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`,
|
||||
}],
|
||||
|
||||
@@ -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();
|
||||
|
||||
69
packages/shared/test/provider-error-format.test.ts
Normal file
69
packages/shared/test/provider-error-format.test.ts
Normal 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'));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user