feat(rpc): add target policy, protocol schemas, and tagged errors
- Add normalizeLoopbackTarget() with loopback-only URL validation - Add JSON-RPC response decoder with schema validation - Define 8 tagged errors using Data.TaggedError - 20 tests covering target policy and protocol decoding - Re-export all public types from index.ts
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { InvalidTargetError } from "./errors.js";
|
||||
|
||||
export { InvalidTargetError };
|
||||
|
||||
const ALLOWED_HOSTNAMES = new Set(["127.0.0.1", "localhost", "[::1]"]);
|
||||
|
||||
/**
|
||||
* Normalize and validate a loopback RPC target URL.
|
||||
*
|
||||
* Only `http://127.0.0.1`, `http://localhost`, and `http://[::1]` are accepted.
|
||||
* DNS names other than the literal `localhost` are rejected to avoid
|
||||
* DNS-rebinding ambiguity — a hostname that resolves to a non-loopback
|
||||
* address at resolution time would bypass this check.
|
||||
*/
|
||||
export function normalizeLoopbackTarget(raw: string): string {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(raw);
|
||||
} catch {
|
||||
throw new InvalidTargetError({ message: "invalid URL" });
|
||||
}
|
||||
|
||||
if (url.protocol !== "http:") {
|
||||
throw new InvalidTargetError({ message: "only http: protocol is allowed" });
|
||||
}
|
||||
|
||||
if (!ALLOWED_HOSTNAMES.has(url.hostname)) {
|
||||
throw new InvalidTargetError({
|
||||
message: `hostname "${url.hostname}" is not an allowed loopback address`,
|
||||
});
|
||||
}
|
||||
|
||||
if (url.username !== "" || url.password !== "") {
|
||||
throw new InvalidTargetError({ message: "credentials in URL are not allowed" });
|
||||
}
|
||||
|
||||
if (url.port === "") {
|
||||
throw new InvalidTargetError({ message: "explicit port is required" });
|
||||
}
|
||||
|
||||
const port = Number(url.port);
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
throw new InvalidTargetError({
|
||||
message: `port ${url.port} is outside valid range 1..65535`,
|
||||
});
|
||||
}
|
||||
|
||||
if (url.search !== "") {
|
||||
throw new InvalidTargetError({ message: "query string is not allowed" });
|
||||
}
|
||||
|
||||
if (url.hash !== "") {
|
||||
throw new InvalidTargetError({ message: "fragment is not allowed" });
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
Reference in New Issue
Block a user