Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions resolver/src/commands/readiness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ const SOROBAN_CONTRACT_RE = /^C[A-Z2-7]{55}$/;

const RPC_TIMEOUT_MS = 4000;

function redactRpcUrl(rawUrl: string): string {
try {
const parsed = new URL(rawUrl);
return `${parsed.protocol}//${parsed.host}`;
} catch {
return "[REDACTED RPC URL]";
}
}

function rpcErrorDetail(error: unknown): string {
const err = error as { shortMessage?: string; message?: string };
const message = err?.shortMessage ?? err?.message ?? String(error);
return message.replace(/\b[a-z][a-z\d+.-]*:\/\/[^\s]+/gi, (url) => redactRpcUrl(url));
}

function checkNetwork(): { network: "testnet" | "mainnet"; check: ReadinessCheck } {
const raw = process.env.NETWORK_MODE ?? "testnet";
if (raw === "testnet" || raw === "mainnet") {
Expand All @@ -71,11 +86,11 @@ function checkNetwork(): { network: "testnet" | "mainnet"; check: ReadinessCheck

async function pingEvmRpc(network: "testnet" | "mainnet"): Promise<{
ok: boolean;
url: string;
chainId: number | null;
detail: string;
}> {
const url = resolveEthereumRpcUrl(network);
const displayUrl = redactRpcUrl(url);
const chain = network === "mainnet" ? mainnet : sepolia;
const expectedChainId = network === "mainnet" ? 1 : 11_155_111;
try {
Expand All @@ -87,35 +102,32 @@ async function pingEvmRpc(network: "testnet" | "mainnet"): Promise<{
if (cid === expectedChainId) {
return {
ok: true,
url,
chainId: cid,
detail: `URL=${url} chainId=${cid}`
detail: `URL=${displayUrl} chainId=${cid}`
};
}
return {
ok: false,
url,
chainId: cid,
detail: `URL=${url} — RPC reported chainId=${cid}, expected ${expectedChainId}`
detail: `URL=${displayUrl} — RPC reported chainId=${cid}, expected ${expectedChainId}`
};
} catch (err: any) {
return {
ok: false,
url,
chainId: null,
detail: `URL=${url} — error: ${err?.shortMessage ?? err?.message ?? String(err)}`
detail: `URL=${displayUrl} — error: ${rpcErrorDetail(err)}`
};
}
}

async function pingSorobanRpc(network: "testnet" | "mainnet"): Promise<{
ok: boolean;
url: string;
detail: string;
}> {
const url =
process.env.SOROBAN_RPC_URL?.trim() ||
(network === "mainnet" ? "https://mainnet.sorobanrpc.com" : "https://soroban-testnet.stellar.org");
const displayUrl = redactRpcUrl(url);
try {
const server = new rpc.Server(url, {
allowHttp: url.startsWith("http://"),
Expand All @@ -125,14 +137,12 @@ async function pingSorobanRpc(network: "testnet" | "mainnet"): Promise<{
const seq = latest?.sequence;
return {
ok: seq !== undefined && seq !== null,
url,
detail: `URL=${url} latestLedger=${seq}`
detail: `URL=${displayUrl} latestLedger=${seq}`
};
} catch (err: any) {
return {
ok: false,
url,
detail: `URL=${url} — error: ${err?.message ?? String(err)}`
detail: `URL=${displayUrl} — error: ${rpcErrorDetail(err)}`
};
}
}
Expand Down Expand Up @@ -263,7 +273,7 @@ export async function assessReadiness(): Promise<ReadinessResult> {

// ===== Informational / dry-run assertion =====
const rawCoordUrl = process.env.COORDINATOR_URL?.trim();
const coordUrlDisplay = rawCoordUrl || "(default http://localhost:3001)";
const coordUrlDisplay = rawCoordUrl ? redactRpcUrl(rawCoordUrl) : "(default http://localhost:3001)";
checks.push({
id: "coordinator-url",
label: "COORDINATOR_URL",
Expand Down
68 changes: 68 additions & 0 deletions resolver/test/readiness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,29 @@ describe("assessReadiness", () => {
expect(findCheck(result, "soroban-rpc").status).toBe("ok");
});

it("redacts credentials from successful RPC diagnostics while preserving hosts and network data", async () => {
const evmSecrets = ["evm-user", "evm-password", "evm-path-key", "evm-query-key"];
const sorobanSecrets = ["soroban-user", "soroban-password", "soroban-path-key", "soroban-query-key"];
setEnv({
...FULL_ENV,
SEPOLIA_RPC_URL:
"https://evm-user:evm-password@eth.example.test/v3/evm-path-key?apiKey=evm-query-key&network=sepolia",
SOROBAN_RPC_URL:
"https://soroban-user:soroban-password@soroban.example.test/rpc/soroban-path-key?token=soroban-query-key&network=testnet"
});

const result = await assessReadiness();
const evmDetail = findCheck(result, "evm-rpc").detail;
const sorobanDetail = findCheck(result, "soroban-rpc").detail;
const diagnostics = `${evmDetail}\n${sorobanDetail}`;

expect(evmDetail).toBe("URL=https://eth.example.test chainId=11155111");
expect(sorobanDetail).toBe("URL=https://soroban.example.test latestLedger=12345");
for (const secret of [...evmSecrets, ...sorobanSecrets]) {
expect(diagnostics).not.toContain(secret);
}
});

it("fails network-mode check when NETWORK_MODE is invalid", async () => {
setEnv({ ...FULL_ENV, NETWORK_MODE: "stagenet" });
const result = await assessReadiness();
Expand Down Expand Up @@ -277,6 +300,20 @@ describe("assessReadiness", () => {
expect(findCheck(result, "coordinator-url").detail).toBe("https://coord.example.test");
});

it("redacts credentials from the coordinator URL diagnostic", async () => {
setEnv({
...FULL_ENV,
COORDINATOR_URL: "https://coord-user:coord-password@coord.example.test/api?token=coord-query-key"
});
const result = await assessReadiness();
const detail = findCheck(result, "coordinator-url").detail;

expect(detail).toBe("https://coord.example.test");
expect(detail).not.toContain("coord-user");
expect(detail).not.toContain("coord-password");
expect(detail).not.toContain("coord-query-key");
});

it("always emits the dry-run assertion check", async () => {
setEnv(FULL_ENV);
const result = await assessReadiness();
Expand Down Expand Up @@ -363,4 +400,35 @@ describe("readinessCommand", () => {
expect(output).not.toContain(TEST_ENV_KEY);
expect(output).not.toContain(TEST_ENV_STELLAR_SECRET);
});

it("redacts credential-bearing RPC URLs echoed by connection errors", async () => {
const evmUrl =
"https://error-user:error-password@eth-error.example.test/v3/error-path-key?access_token=error-query-key";
const sorobanUrl =
"https://stellar-user:stellar-password@soroban-error.example.test/rpc/stellar-path-key?auth=stellar-query-key";
setEnv({ ...FULL_ENV, SEPOLIA_RPC_URL: evmUrl, SOROBAN_RPC_URL: sorobanUrl });
mockGetChainId.mockRejectedValue(new Error(`Request failed for ${evmUrl}`));
mockGetLatestLedger.mockRejectedValue(new Error(`Request failed for ${sorobanUrl}`));

const log = captureLog();
const code = await readinessCommand();
log.restore();
const output = log.calls.map((args) => args.map(String).join(" ")).join("\n");

expect(code).toBe(1);
expect(output).toContain("https://eth-error.example.test");
expect(output).toContain("https://soroban-error.example.test");
for (const secret of [
"error-user",
"error-password",
"error-path-key",
"error-query-key",
"stellar-user",
"stellar-password",
"stellar-path-key",
"stellar-query-key"
]) {
expect(output).not.toContain(secret);
}
});
});