1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
//! Credential redaction for URL-ish strings shared across the binaries.
//!
//! Two related invariants live here, both keyed on the rule that a URL can
//! carry a secret no log or operator-facing error may echo:
//!
//! - **Userinfo** — relay/origin URLs can carry `user:pass@` userinfo. When
//! such a string is named in a parse error or log (config-validate time,
//! [`crate::config`]; node bring-up, the `decdn-node` runtime), the
//! credentials are scrubbed by [`redact_userinfo`], which preserves the host
//! and path so the offending entry is still identifiable.
//! - **Whole URL** — a chain `rpc_url` secret commonly lives in the path or
//! query (Infura/Alchemy keys), which userinfo redaction would NOT scrub. So
//! when an `rpc_url` reaches a *transport/chain error*, [`strip_urls`]
//! removes the URL entirely. The failure class (timeout, connection refused,
//! DNS, TLS) usually lives in the error's `source()` chain, not in its
//! top-level Display. [`sanitize_err_chain`] (for an `anyhow::Error`) and
//! [`sanitize_error_sources`] (for a typed `std::error::Error`) render that
//! chain, so the class survives the strip. [`sanitize_rpc_display`] renders
//! only the top-level Display, so it suits a plain `Display` value such as a
//! `String` reason.
//!
//! Routing every gate through this single module keeps each invariant from
//! drifting between copies.
use Cow;
/// Replace any `user:pass@` userinfo in a URL-ish string with `***@` so a
/// parse-failure log/error can name the offending entry without leaking
/// credentials.
///
/// The input may be an *already-malformed* entry, so we can't lean on a URL
/// parser. The authority begins after the first `://` (a scheme separator) or
/// at the start of the string when there is none — a typo'd entry may drop the
/// scheme yet still carry credentials. Within the authority (up to the host's
/// first `/`, `?`, or `#`), userinfo runs to the *last* `@`, so a password
/// containing a literal `@` is fully redacted, while an `@` in the path/query/
/// fragment is left intact. Strings with no authority `@` pass through
/// unchanged (borrowed, no copy), so non-credential values still appear
/// verbatim in errors.
/// Strip URL-bearing fragments from an error's rendered text so an `rpc_url`
/// secret never reaches a log or operator-facing error. Any failure class in
/// the input text (timeout / connection refused / DNS / TLS) is kept; to
/// include the `source()` chain, call [`sanitize_error_sources`] or
/// [`sanitize_err_chain`].
///
/// Unlike [`redact_userinfo`], which only scrubs `user:pass@` userinfo, this
/// removes the whole URL: an `rpc_url` secret commonly lives in the path or
/// query (Infura/Alchemy keys), matching `config validate`'s policy of never
/// echoing `rpc_url`. Two fragment shapes are scrubbed:
///
/// - `reqwest`'s Display tail ` for url (<url>)` (reqwest 0.13.x; the inner
/// `write!(f, " for url ({url})")`), which `alloy`'s `#[error("{0}")]`-
/// transparent transport error forwards verbatim — the entire ` for url (...)`
/// clause is dropped; and
/// - any remaining bare `<scheme>://` token, replaced with `<redacted-url>` as
/// a backstop for non-`reqwest` renderings.
///
/// Text with no URL scheme passes through borrowed (no copy).
/// Drop every ` for url (...)` clause (reqwest's Display tail) from `s`.
/// Replace each bare `<scheme>://<token>` in `s` with `<redacted-url>`.
///
/// Keys off the `://` separator and walks back over the scheme characters
/// (RFC 3986: ALPHA / DIGIT / `+` / `-` / `.`), so it matches ANY scheme
/// regardless of case — `http`, `HTTPS`, `ws`, `git+ssh`, … — rather than a
/// hard-coded `http`/`https` prefix a future non-`reqwest` renderer (or an
/// uppercased URL) could slip past.
/// Render a plain `Display` value, with URL fragments stripped, for a single
/// log/error line. Use for a value with no `source()` chain, such as a
/// `String` reason.
///
/// For an error, prefer a helper that walks the chain: Display renders only
/// the *outermost* layer, so the failure class beneath it is lost.
/// [`sanitize_err_chain`] takes an `anyhow::Error`, whose `.with_context(…)`
/// layers would otherwise replace the underlying reason.
/// [`sanitize_error_sources`] takes a typed `std::error::Error` (an `alloy`
/// `ContractError`, `RpcError`, or `PendingTransactionError`), whose
/// transport class (timeout, connection refused, DNS, TLS) sits in `source()`.
/// Render a typed error and each layer of its `source()` chain as
/// `outer: cause: cause`, with URL fragments stripped.
///
/// Use for a typed `std::error::Error`, such as an `alloy` transport or
/// contract error. Its top-level Display often names only the operation and
/// the URL, while the failure class (timeout, connection refused, DNS, TLS)
/// is a deeper `source()`. A layer is skipped when its Display is empty, or
/// when the previous non-empty layer's raw Display ends with it at a
/// non-alphanumeric boundary: a wrapper that ends its message with its source
/// (`"{0}"`, `"transport: {0}"`) would otherwise print that source twice. A
/// cause that only appears inside an earlier message or URL (`"timeout"` under
/// `"request timeout exceeded"`) is still printed. Each layer is stripped on
/// its own before the join, so a URL token cannot swallow the `": "` separator
/// after it. For an `anyhow::Error`, use [`sanitize_err_chain`].
/// Render an `anyhow` error's full `context: cause: cause` chain with URL
/// fragments stripped, through [`sanitize_error_sources`]: the context layers
/// and the wrapped error's `source()` chain share one walk, so a repeated
/// layer is printed once and each layer is stripped on its own.
///
/// Use for any propagated chain-RPC error — at the `main()` print boundary so
/// no raw `rpc_url` is echoed, and at watcher `tracing` sites so the reason
/// survives. The latter is not a style preference: a chain read is wrapped by
/// `chain_events::timed`, whose "… timed out after 10s" is the *cause*, and
/// call sites add context above it (`getOrigins(namespace=1)`,
/// `nodeIdOf(0x…)`). Rendering such an error with plain Display prints only
/// that context and drops the timeout entirely — the operator sees a bare
/// restatement of what was attempted, never why it failed.